diff --git a/CHANGELOG.md b/CHANGELOG.md index 107d33c4..83b2127f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] -## [0.24.1] - 2024-09-07 +## [0.24.2] - 2024-09-03 + +- Fixes memory leak with the JWKS cache. + +## [0.24.1] - 2024-08-07 - Improves debug logs for error handlers. diff --git a/recipe/session/jwksMemory_test.go b/recipe/session/jwksMemory_test.go new file mode 100644 index 00000000..070fe93c --- /dev/null +++ b/recipe/session/jwksMemory_test.go @@ -0,0 +1,62 @@ +package session + +import ( + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" + "github.com/supertokens/supertokens-golang/supertokens" + "github.com/supertokens/supertokens-golang/test/unittesting" +) + +func TestThatThereIsNoMemoryLeakWithJWKSCache(t *testing.T) { + configValue := supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "SuperTokens", + WebsiteDomain: "supertokens.io", + APIDomain: "api.supertokens.io", + }, + RecipeList: []supertokens.Recipe{ + Init(&sessmodels.TypeInput{ + JWKSRefreshIntervalSec: &[]uint64{0}[0], + }), + }, + } + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + err := supertokens.Init(configValue) + if err != nil { + t.Error(err.Error()) + } + + testServer := GetTestServer(t) + defer func() { + testServer.Close() + }() + + sess, err := CreateNewSessionWithoutRequestResponse("public", "testuser", map[string]interface{}{}, map[string]interface{}{}, nil) + assert.NoError(t, err) + + accessToken := sess.GetAccessToken() + + _, err = GetSessionWithoutRequestResponse(accessToken, nil, nil) + assert.NoError(t, err) + + numGoroutinesBeforeJWKSRefresh := runtime.NumGoroutine() + + for i := 0; i < 100; i++ { + _, err = GetSessionWithoutRequestResponse(accessToken, nil, nil) + assert.NoError(t, err) + + time.Sleep(10 * time.Millisecond) + } + + time.Sleep(1 * time.Second) + assert.Equal(t, numGoroutinesBeforeJWKSRefresh, runtime.NumGoroutine()) +} diff --git a/recipe/session/recipeImplementation.go b/recipe/session/recipeImplementation.go index 2a81024c..cc5f7134 100644 --- a/recipe/session/recipeImplementation.go +++ b/recipe/session/recipeImplementation.go @@ -105,11 +105,11 @@ func getJWKS() (*keyfunc.JWKS, error) { LastFetched: time.Now().UnixNano() / int64(time.Millisecond), } - // Dont add to cache if there is an error to keep the logic of checking cache simple - // - // This also has the added benefit where if initially the request failed because the core - // was down and then it comes back up, the next time it will try to request that core again - // after the cache has expired + // Close any existing JWKS in the cache before replacing it + if jwksCache != nil && jwksCache.JWKS != nil { + jwksCache.JWKS.EndBackground() + } + jwksCache = &jwksResult if supertokens.IsRunningInTestMode() { diff --git a/supertokens/constants.go b/supertokens/constants.go index ead78783..3e95d5f9 100644 --- a/supertokens/constants.go +++ b/supertokens/constants.go @@ -21,7 +21,7 @@ const ( ) // VERSION current version of the lib -const VERSION = "0.24.1" +const VERSION = "0.24.2" var ( cdiSupported = []string{"3.1"}