Repository: snowflakedb/gosnowflake
Pull request #1346 (2025-03-28 13:41:21)

SNOW-2013198 Clear OCSP cache regularly
### Description

SNOW-2013198 Before we never cleared OCSP cache, so in case of contacting with multiple HTTPS hosts, this cache grew endlessly. Now it is regularly cleared.

### Checklist
- [x] Created tests which fail without the change (if possible)
- [x] Extended the README / documentation, if necessary


sfc-gh-snowflakedb-snyk-sa: ![](https://res.cloudinary.com/snyk/image/upload/r-d/scm-platform/snyk-pull-requests/pr-banner-default.svg)

### :tada: **Snyk checks have passed. No issues have been found so far.**

:white_check_mark: **security/snyk** check is complete. No issues have been found. [(View Details)](https://app.snyk.io/org/snowflakedb-sca-scanning-public-repo/pr-checks/5d298dc3-8ba0-4770-816f-10a18249756a?refs=69a49900d9066bdf6af756739576dab836ff2dc8&source=prChecksComment)

:white_check_mark: **license/snyk** check is complete. No issues have been found. [(View Details)](https://app.snyk.io/org/snowflakedb-sca-scanning-public-repo/pr-checks/5d298dc3-8ba0-4770-816f-10a18249756a/license?refs=69a49900d9066bdf6af756739576dab836ff2dc8&source=prChecksComment)



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1346?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) Report
Attention: Patch coverage is `97.14286%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 82.30%. Comparing base [(`36ee21d`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/36ee21d24cc4f580d5e8a652d4e38a06a9a0f9f4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) to head [(`6214013`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/6214013b15049bae095fd6c4e11f18103176b80c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb).

| [Files with missing lines](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1346?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) | Patch % | Lines |
|---|---|---|
| [ocsp.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1346?src=pr&el=tree&filepath=ocsp.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-b2NzcC5nbw==) | 97.14% | [1 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1346?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1346      +/-   ##
==========================================
+ Coverage   82.27%   82.30%   +0.03%     
==========================================
  Files          55       55              
  Lines       13729    13764      +35     
==========================================
+ Hits        11295    11328      +33     
- Misses       2434     2436       +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1346?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/ocsp.go b/ocsp.go
index 14f68ac..a87e125 100644
--- a/ocsp.go
+++ b/ocsp.go
@@ -73,6 +73,9 @@ const (
 	defaultOCSPResponderTimeout = 10 * time.Second
 	// defaultOCSPMaxRetryCount specifies maximum numbere of subsequent retries to OCSP (cache and server)
 	defaultOCSPMaxRetryCount = 2
+
+	// defaultOCSPResponseCacheClearingInterval is the default value for clearing OCSP response cache
+	defaultOCSPResponseCacheClearingInterval = 15 * time.Minute
 )
 
 var (
@@ -87,12 +90,13 @@ var (
 const (
 	cacheFileBaseName = "ocsp_response_cache.json"
 	// cacheExpire specifies cache data expiration time in seconds.
-	cacheExpire           = float64(24 * 60 * 60)
-	defaultCacheServerURL = "http://ocsp.snowflakecomputing.com"
-	cacheServerEnabledEnv = "SF_OCSP_RESPONSE_CACHE_SERVER_ENABLED"
-	cacheServerURLEnv     = "SF_OCSP_RESPONSE_CACHE_SERVER_URL"
-	cacheDirEnv           = "SF_OCSP_RESPONSE_CACHE_DIR"
-	ocspRetryURLEnv       = "SF_OCSP_RESPONSE_RETRY_URL"
+	cacheExpire                                   = float64(24 * 60 * 60)
+	defaultCacheServerURL                         = "http://ocsp.snowflakecomputing.com"
+	cacheServerEnabledEnv                         = "SF_OCSP_RESPONSE_CACHE_SERVER_ENABLED"
+	cacheServerURLEnv                             = "SF_OCSP_RESPONSE_CACHE_SERVER_URL"
+	cacheDirEnv                                   = "SF_OCSP_RESPONSE_CACHE_DIR"
+	ocspRetryURLEnv                               = "SF_OCSP_RESPONSE_RETRY_URL"
+	ocspResponseCacheClearingIntervalInSecondsEnv = "SF_OCSP_RESPONSE_CACHE_CLEARING_INTERVAL_IN_SECONDS"
 )
 
 const (
@@ -109,6 +113,8 @@ const (
 	maxClockSkew           = 900 * time.Second // buffer for clock skew
 )
 
+var stopOCSPCacheClearing = make(chan struct{})
+
 type ocspStatusCode int
 
 type ocspStatus struct {
@@ -1076,10 +1082,56 @@ func createOCSPCacheDir() {
 	logger.Infof("reset OCSP cache file. %v", cacheFileName)
 }
 
+func initOCSPCacheClearer() {
+	interval := defaultOCSPResponseCacheClearingInterval
+	if intervalFromEnv := os.Getenv(ocspResponseCacheClearingIntervalInSecondsEnv); intervalFromEnv != "" {
+		intervalAsSeconds, err := strconv.Atoi(intervalFromEnv)
+		if err != nil {
+			logger.Warnf("unparsable %v value: %v", ocspResponseCacheClearingIntervalInSecondsEnv, intervalFromEnv)
+		} else {
+			interval = time.Duration(intervalAsSeconds) * time.Second
+		}
+	}
+	logger.Debugf("initializing OCSP cache clearer to %v", interval)
+	go GoroutineWrapper(context.Background(), func() {
+		ticker := time.NewTicker(interval)
+		for {
+			select {
+			case <-ticker.C:
+				clearOCSPCaches()
+			case <-stopOCSPCacheClearing:
+				logger.Debug("stopped clearing OCSP cache")
+				ticker.Stop()
+				return
+			}
+		}
+	})
+}
+
+func stopOCSPCacheClearer() {
+	stopOCSPCacheClearing <- struct{}{}
+}
+
+func clearOCSPCaches() {
+	logger.Debugf("clearing OCSP caches")
+	func() {
+		ocspResponseCacheLock.Lock()
+		defer ocspResponseCacheLock.Unlock()
+		ocspResponseCache = make(map[certIDKey]*certCacheValue)
+	}()
+
+	func() {
+		ocspParsedRespCacheLock.Lock()
+		defer ocspParsedRespCacheLock.Unlock()
+		ocspParsedRespCache = make(map[parsedOcspRespKey]*ocspStatus)
+	}()
+}
+
 func init() {
 	readCACerts()
 	createOCSPCacheDir()
 	initOCSPCache()
+	initOCSPCacheClearer()
 }
 
 // snowflakeNoOcspTransport is the transport object that doesn't do certificate revocation check with OCSP.


Test patch:
diff --git a/ocsp_test.go b/ocsp_test.go
index cb67ebc..9544dd4 100644
--- a/ocsp_test.go
+++ b/ocsp_test.go
@@ -229,6 +229,40 @@ func TestUnitCheckOCSPResponseCache(t *testing.T) {
 	}
 }
 
+func TestOcspCacheClearer(t *testing.T) {
+	origValue := os.Getenv(ocspResponseCacheClearingIntervalInSecondsEnv)
+	defer func() {
+		stopOCSPCacheClearer()
+		os.Setenv(ocspResponseCacheClearingIntervalInSecondsEnv, origValue)
+		initOCSPCache()
+		initOCSPCacheClearer()
+	}()
+	func() {
+		ocspResponseCacheLock.Lock()
+		defer ocspResponseCacheLock.Unlock()
+		ocspResponseCache[certIDKey{}] = nil
+	}()
+	func() {
+		ocspParsedRespCacheLock.Lock()
+		defer ocspParsedRespCacheLock.Unlock()
+		ocspParsedRespCache[parsedOcspRespKey{}] = nil
+	}()
+	stopOCSPCacheClearer()
+	os.Setenv(ocspResponseCacheClearingIntervalInSecondsEnv, "1")
+	initOCSPCacheClearer()
+	time.Sleep(2 * time.Second)
+	func() {
+		ocspResponseCacheLock.Lock()
+		defer ocspResponseCacheLock.Unlock()
+		assertEqualE(t, len(ocspResponseCache), 0)
+	}()
+	func() {
+		ocspParsedRespCacheLock.Lock()
+		defer ocspParsedRespCacheLock.Unlock()
+		assertEqualE(t, len(ocspParsedRespCache), 0)
+	}()
+}
+
 func TestUnitValidateOCSP(t *testing.T) {
 	ocspRes := &ocsp.Response{}
 	ost := validateOCSP(ocspRes)