From e445be3f9c08cdf384550ef94643c696280d9d40 Mon Sep 17 00:00:00 2001 From: Tit Petric Date: Wed, 11 Sep 2024 19:59:54 +0200 Subject: [PATCH] [TT-13041] [release-5.3] backport (#6499) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### **User description** Backport of issue on master, two conflicts resolved: 1. session.Touch doesn't exist on 5.3 (api compatibility) 2. some internal gateway function renames (initSystem) ___ ### **PR Type** Bug fix, Enhancement, Tests ___ ### **Description** - Refactored the `RedisQuotaExceeded` function to improve quota handling logic, including better management of key expiration and renewal. - Enhanced logging with additional context to aid in debugging and monitoring of quota operations. - Removed the distributed lock during quota increment and added it during quota reset to optimize performance. - Updated test cases in `middleware_test.go` to use `t.Run` for better organization and added additional test cases for comprehensive coverage. - Introduced `test.Exclusive` function to limit parallelism in tests that use `DeleteAllKeys`. - Replaced `initialiseSystem` with `initSystem` and added a global mutex for synchronization. - Enhanced CI workflow by adding steps to print CPU info and Go environment. ___ ### **Changes walkthrough** 📝
Relevant files
Enhancement
4 files
session_manager.go
Refactor and improve quota handling logic                               

gateway/session_manager.go
  • Refactored RedisQuotaExceeded function to improve quota handling.
  • Enhanced logging for better debugging and monitoring.
  • Removed distributed lock during quota increment and added it during
    quota reset.
  • +81/-50 
    server.go
    Synchronize system initialization with global mutex           

    gateway/server.go
  • Replaced initialiseSystem with initSystem.
  • Added a global mutex for synchronization.
  • +6/-4     
    testutil.go
    Update test utility for system initialization                       

    gateway/testutil.go - Replaced `initialiseSystem` with `initSystem`.
    +1/-1     
    util.go
    Add exclusive test execution utility                                         

    test/util.go - Introduced `Exclusive` function to gate single test execution.
    +15/-0   
    Tests
    6 files
    middleware_test.go
    Refactor and extend test cases for session limiter             

    gateway/middleware_test.go
  • Added test.Exclusive to limit parallelism in tests.
  • Refactored test cases to use t.Run for better organization.
  • Added additional test cases for comprehensive coverage.
  • +27/-24 
    api_test.go
    Add exclusive test handling for API tests                               

    gateway/api_test.go
  • Added test.Exclusive to limit parallelism in tests.
  • Added comments to DeleteAllKeys for clarity.
  • +11/-3   
    gateway_test.go
    Add exclusive test handling for quota tests                           

    gateway/gateway_test.go - Added `test.Exclusive` to limit parallelism in quota tests.
    +2/-0     
    mw_external_oauth_test.go
    Add exclusive test handling for OAuth middleware tests     

    gateway/mw_external_oauth_test.go
  • Added test.Exclusive to limit parallelism in OAuth middleware tests.
  • +4/-2     
    mw_rate_limiting_test.go
    Add exclusive test handling for rate limiting tests           

    gateway/mw_rate_limiting_test.go
  • Added test.Exclusive to limit parallelism in rate limiting tests.
  • +4/-1     
    rpc_storage_handler_test.go
    Add exclusive test handling for RPC storage handler tests

    gateway/rpc_storage_handler_test.go
  • Added test.Exclusive to limit parallelism in RPC storage handler
    tests.
  • +11/-6   
    Configuration changes
    1 files
    ci-tests.yml
    Enhance CI workflow with additional environment information

    .github/workflows/ci-tests.yml - Added steps to print CPU info and Go environment in CI workflow.
    +6/-0     
    ___ > 💡 **PR-Agent usage**: >Comment `/help` on the PR to get a list of all available PR-Agent tools and their descriptions --------- Co-authored-by: Tit Petric Co-authored-by: Esteban Ricardo Mirizio --- .github/workflows/ci-tests.yml | 6 ++ gateway/api_test.go | 14 ++- gateway/gateway_test.go | 2 + gateway/middleware_test.go | 51 ++++++----- gateway/mw_external_oauth_test.go | 6 +- gateway/mw_rate_limiting_test.go | 5 +- gateway/rpc_storage_handler_test.go | 17 ++-- gateway/server.go | 10 ++- gateway/session_manager.go | 131 +++++++++++++++++----------- gateway/testutil.go | 2 +- test/util.go | 15 ++++ 11 files changed, 168 insertions(+), 91 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 1e10da991ce..f33d3ab15ff 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -108,6 +108,12 @@ jobs: if: ${{ github.event_name == 'pull_request' }} run: git fetch origin ${{ github.base_ref }} + - name: Print CPU info + run: grep '^model name' /proc/cpuinfo + + - name: Print Go env + run: go env + - name: Run Gateway Tests id: ci-tests run: | diff --git a/gateway/api_test.go b/gateway/api_test.go index 40866fac86a..a1644eaa56f 100644 --- a/gateway/api_test.go +++ b/gateway/api_test.go @@ -219,6 +219,8 @@ func TestApiHandlerPostDupPath(t *testing.T) { } func TestKeyHandler(t *testing.T) { + test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism. + ts := StartTest(nil) defer ts.Close() @@ -338,7 +340,7 @@ func TestKeyHandler(t *testing.T) { }, }...) - ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() + ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive }) _, knownKey := ts.CreateSession(func(s *user.SessionState) { @@ -644,6 +646,8 @@ func BenchmarkKeyHandler_CreateKeyHandler(b *testing.B) { } func TestKeyHandler_DeleteKeyWithQuota(t *testing.T) { + test.Exclusive(t) // Uses quota, need to limit parallelism due to DeleteAllKeys. + const testAPIID = "testAPIID" const orgId = "default" @@ -826,6 +830,8 @@ func TestUpdateKeyWithCert(t *testing.T) { } func TestKeyHandler_CheckKeysNotDuplicateOnUpdate(t *testing.T) { + test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism. + ts := StartTest(nil) defer ts.Close() @@ -878,7 +884,7 @@ func TestKeyHandler_CheckKeysNotDuplicateOnUpdate(t *testing.T) { for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() + ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive session := CreateStandardSession() session.AccessRights = map[string]user.AccessDefinition{"test": { APIID: "test", Versions: []string{"v1"}, @@ -906,6 +912,8 @@ func TestKeyHandler_CheckKeysNotDuplicateOnUpdate(t *testing.T) { } func TestHashKeyHandler(t *testing.T) { + test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism. + conf := func(globalConf *config.Config) { // make it to use hashes for Redis keys globalConf.HashKeys = true @@ -932,7 +940,7 @@ func TestHashKeyHandler(t *testing.T) { gwConf := ts.Gw.GetConfig() gwConf.HashKeyFunction = tc.hashFunction ts.Gw.SetConfig(gwConf) - ok := ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() + ok := ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive assert.True(t, ok) t.Run(fmt.Sprintf("%sHash fn: %s", tc.desc, tc.hashFunction), func(t *testing.T) { diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index 4df00e1f482..aff6ba78db9 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -323,6 +323,8 @@ func TestSkipTargetPassEscapingOffWithSkipURLCleaningTrue(t *testing.T) { } func TestQuota(t *testing.T) { + test.Exclusive(t) // Uses quota, need to limit parallelism due to DeleteAllKeys. + ts := StartTest(nil) defer ts.Close() diff --git a/gateway/middleware_test.go b/gateway/middleware_test.go index dd83a4ef2cc..7040e1cc08a 100644 --- a/gateway/middleware_test.go +++ b/gateway/middleware_test.go @@ -12,6 +12,7 @@ import ( "github.com/TykTechnologies/tyk/apidef" headers2 "github.com/TykTechnologies/tyk/header" "github.com/TykTechnologies/tyk/internal/cache" + "github.com/TykTechnologies/tyk/internal/uuid" "github.com/TykTechnologies/tyk/test" "github.com/TykTechnologies/tyk/config" @@ -258,24 +259,20 @@ func TestBaseMiddleware_getAuthToken(t *testing.T) { } func TestSessionLimiter_RedisQuotaExceeded_PerAPI(t *testing.T) { + test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism. + g := StartTest(nil) defer g.Close() - g.Gw.GlobalSessionManager.Store().DeleteAllKeys() - defer g.Gw.GlobalSessionManager.Store().DeleteAllKeys() - apis := BuildAPI(func(spec *APISpec) { - spec.APIID = "api1" - spec.UseKeylessAccess = false - spec.Proxy.ListenPath = "/api1/" - }, func(spec *APISpec) { - spec.APIID = "api2" - spec.UseKeylessAccess = false - spec.Proxy.ListenPath = "/api2/" - }, func(spec *APISpec) { - spec.APIID = "api3" + g.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive + defer g.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive + + api := func(spec *APISpec) { + spec.APIID = uuid.New() spec.UseKeylessAccess = false - spec.Proxy.ListenPath = "/api3/" - }) + spec.Proxy.ListenPath = fmt.Sprintf("/%s/", spec.APIID) + } + apis := BuildAPI(api, api, api) g.Gw.LoadAPI(apis...) @@ -330,18 +327,24 @@ func TestSessionLimiter_RedisQuotaExceeded_PerAPI(t *testing.T) { } } - // for api1 - per api - sendReqAndCheckQuota(t, apis[0].APIID, 9, true) - sendReqAndCheckQuota(t, apis[0].APIID, 8, true) - sendReqAndCheckQuota(t, apis[0].APIID, 7, true) + t.Run("For api1 - per api", func(t *testing.T) { + sendReqAndCheckQuota(t, apis[0].APIID, 9, true) + sendReqAndCheckQuota(t, apis[0].APIID, 8, true) + sendReqAndCheckQuota(t, apis[0].APIID, 7, true) + }) - // for api2 - per api - sendReqAndCheckQuota(t, apis[1].APIID, 1, true) - sendReqAndCheckQuota(t, apis[1].APIID, 0, true) + t.Run("For api2 - per api", func(t *testing.T) { + sendReqAndCheckQuota(t, apis[1].APIID, 1, true) + sendReqAndCheckQuota(t, apis[1].APIID, 0, true) + }) - // for api3 - global - sendReqAndCheckQuota(t, apis[2].APIID, 24, false) - sendReqAndCheckQuota(t, apis[2].APIID, 23, false) + t.Run("For api3 - global", func(t *testing.T) { + sendReqAndCheckQuota(t, apis[2].APIID, 24, false) + sendReqAndCheckQuota(t, apis[2].APIID, 23, false) + sendReqAndCheckQuota(t, apis[2].APIID, 22, false) + sendReqAndCheckQuota(t, apis[2].APIID, 21, false) + sendReqAndCheckQuota(t, apis[2].APIID, 20, false) + }) } func TestCopyAllowedURLs(t *testing.T) { diff --git a/gateway/mw_external_oauth_test.go b/gateway/mw_external_oauth_test.go index 92bfe709e8c..ac42c48cd29 100644 --- a/gateway/mw_external_oauth_test.go +++ b/gateway/mw_external_oauth_test.go @@ -367,6 +367,8 @@ func TestExternalOAuthMiddleware_introspection(t *testing.T) { }...) t.Run("cache", func(t *testing.T) { + test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism. + api.ExternalOAuth.Providers[0].Introspection.Cache.Enabled = true api.ExternalOAuth.Providers[0].Introspection.Cache.Timeout = 0 ts.Gw.LoadAPI(api) @@ -382,13 +384,13 @@ func TestExternalOAuthMiddleware_introspection(t *testing.T) { }...) // invalidate cache - externalOAuthIntrospectionCache.DeleteAllKeys() + externalOAuthIntrospectionCache.DeleteAllKeys() // exclusive _, _ = ts.Run(t, []test.TestCase{ {Path: "/get", Headers: headers, BodyMatch: "access token is not valid", Code: http.StatusUnauthorized}, }...) t.Run("expired", func(t *testing.T) { - externalOAuthIntrospectionCache.DeleteAllKeys() + externalOAuthIntrospectionCache.DeleteAllKeys() // exclusive // normally for expired token, the introspection returns active false // this is to get rid of putting delay to wait until expiration diff --git a/gateway/mw_rate_limiting_test.go b/gateway/mw_rate_limiting_test.go index f0efcb4cd31..408c3a4aeb8 100644 --- a/gateway/mw_rate_limiting_test.go +++ b/gateway/mw_rate_limiting_test.go @@ -66,6 +66,7 @@ func TestRateLimit_Unlimited(t *testing.T) { } func TestNeverRenewQuota(t *testing.T) { + test.Exclusive(t) // Uses quota, need to limit parallelism due to DeleteAllKeys. g := StartTest(nil) defer g.Close() @@ -192,6 +193,8 @@ func TestMwRateLimiting_DepthLimit(t *testing.T) { } func providerCustomRatelimitKey(t *testing.T, limiter string) { + test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism. + t.Helper() tcs := []struct { @@ -246,7 +249,7 @@ func providerCustomRatelimitKey(t *testing.T, limiter string) { ts.Gw.SetConfig(globalConf) - ok := ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() + ok := ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive assert.True(t, ok) customRateLimitKey := "portal-developer-1" + tc.hashAlgo + limiter diff --git a/gateway/rpc_storage_handler_test.go b/gateway/rpc_storage_handler_test.go index cd52b3fb600..d5750d5c5d0 100644 --- a/gateway/rpc_storage_handler_test.go +++ b/gateway/rpc_storage_handler_test.go @@ -59,6 +59,7 @@ func getRefreshToken(td tokenData) string { } func TestProcessKeySpaceChangesForOauth(t *testing.T) { + test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism. cases := []struct { TestName string @@ -135,7 +136,7 @@ func TestProcessKeySpaceChangesForOauth(t *testing.T) { } } else { getKeyFromStore = ts.Gw.GlobalSessionManager.Store().GetKey - ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() + ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive err := ts.Gw.GlobalSessionManager.Store().SetRawKey(token, token, 100) assert.NoError(t, err) _, err = ts.Gw.GlobalSessionManager.Store().GetRawKey(token) @@ -155,6 +156,7 @@ func TestProcessKeySpaceChangesForOauth(t *testing.T) { } func TestProcessKeySpaceChanges_ResetQuota(t *testing.T) { + test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism. g := StartTest(nil) defer g.Close() @@ -166,8 +168,8 @@ func TestProcessKeySpaceChanges_ResetQuota(t *testing.T) { Gw: g.Gw, } - g.Gw.GlobalSessionManager.Store().DeleteAllKeys() - defer g.Gw.GlobalSessionManager.Store().DeleteAllKeys() + g.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive + defer g.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive api := g.Gw.BuildAndLoadAPI(func(spec *APISpec) { spec.UseKeylessAccess = false @@ -217,6 +219,7 @@ func TestProcessKeySpaceChanges_ResetQuota(t *testing.T) { // TestRPCUpdateKey check that on update key event the key still exist in worker redis func TestRPCUpdateKey(t *testing.T) { + test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism. cases := []struct { TestName string @@ -248,8 +251,8 @@ func TestRPCUpdateKey(t *testing.T) { Gw: g.Gw, } - g.Gw.GlobalSessionManager.Store().DeleteAllKeys() - defer g.Gw.GlobalSessionManager.Store().DeleteAllKeys() + g.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive + defer g.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive api := g.Gw.BuildAndLoadAPI(func(spec *APISpec) { spec.UseKeylessAccess = false @@ -289,6 +292,8 @@ func TestRPCUpdateKey(t *testing.T) { } func TestGetGroupLoginCallback(t *testing.T) { + test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism. + tcs := []struct { testName string syncEnabled bool @@ -318,7 +323,7 @@ func TestGetGroupLoginCallback(t *testing.T) { globalConf.SlaveOptions.SynchroniserEnabled = tc.syncEnabled }) defer ts.Close() - defer ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() + defer ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive rpcListener := RPCStorageHandler{ KeyPrefix: "rpc.listener.", diff --git a/gateway/server.go b/gateway/server.go index 94d471d5ac2..0d15678a6e3 100644 --- a/gateway/server.go +++ b/gateway/server.go @@ -68,6 +68,8 @@ import ( ) var ( + globalMu sync.Mutex + log = logger.Get() mainLog = log.WithField("prefix", "main") pubSubLog = log.WithField("prefix", "pub-sub") @@ -334,8 +336,8 @@ func (gw *Gateway) apisByIDLen() int { // Create all globals and init connection handlers func (gw *Gateway) setupGlobals() { - gw.reloadMu.Lock() - defer gw.reloadMu.Unlock() + globalMu.Lock() + defer globalMu.Unlock() defaultTykErrors() @@ -1211,7 +1213,7 @@ func (gw *Gateway) setupLogger() { } } -func (gw *Gateway) initialiseSystem() error { +func (gw *Gateway) initSystem() error { if gw.isRunningTests() && os.Getenv("TYK_LOGLEVEL") == "" { // `go test` without TYK_LOGLEVEL set defaults to no log // output @@ -1621,7 +1623,7 @@ func Start() { gw := NewGateway(gwConfig, ctx) - if err := gw.initialiseSystem(); err != nil { + if err := gw.initSystem(); err != nil { mainLog.Fatalf("Error initialising system: %v", err) } diff --git a/gateway/session_manager.go b/gateway/session_manager.go index 300091804bd..eedbe90af8d 100644 --- a/gateway/session_manager.go +++ b/gateway/session_manager.go @@ -332,7 +332,13 @@ func (l *SessionLimiter) ForwardMessage(r *http.Request, currentSession *user.Se // RedisQuotaExceeded returns true if the request should be blocked as over quota. func (l *SessionLimiter) RedisQuotaExceeded(r *http.Request, session *user.SessionState, quotaKey, scope string, limit *user.APILimit, store storage.Handler, hashKeys bool) bool { + logger := log.WithFields(logrus.Fields{ + "quotaMax": limit.QuotaMax, + "quotaRenewalRate": limit.QuotaRenewalRate, + }) + if limit.QuotaMax <= 0 { + logger.Error("Quota disabled: quota max <= 0") return false } @@ -349,82 +355,107 @@ func (l *SessionLimiter) RedisQuotaExceeded(r *http.Request, session *user.Sessi } key := session.KeyID - if hashKeys { key = storage.HashStr(session.KeyID) } - if quotaKey != "" { key = quotaKey } - now := time.Now().Truncate(0) + now := time.Now() + + // rawKey is the redis key for quota rawKey := QuotaKeyPrefix + quotaScope + key - quotaRenewalRate := time.Second * time.Duration(limit.QuotaRenewalRate) - quotaMax := limit.QuotaMax - // First, ensure a distributed lock + var quotaRenewalRate time.Duration + if limit.QuotaRenewalRate > 0 { + quotaRenewalRate = time.Second * time.Duration(limit.QuotaRenewalRate) + } + conn := l.limiterStorage - locker := limiter.NewLimiter(conn).Locker(rawKey) - if err := locker.Lock(ctx); err != nil { - log.WithError(err).Error("error locking quota key, blocking") + var expired, exists bool + var expiredAt time.Time + + dur, err := conn.PTTL(ctx, rawKey).Result() + if err != nil && !errors.Is(err, redis.Nil) { + logger.WithError(err).Error("error getting key TTL, blocking") return true } - defer func() { - if err := locker.Unlock(ctx); err != nil { - log.WithError(err).Error("error unlocking quota key") + + // The command returns -2 if the key does not exist. + // The command returns -1 if the key exists but has no associated expire. + expired = dur < 0 + exists = dur != -2 + + expiredAt = now.Add(dur) + + logger = logger.WithFields(logrus.Fields{ + "exists": exists, + "expired": expired, + "rawKey": rawKey, + }) + + increment := func() bool { + var res *redis.IntCmd + _, err := conn.Pipelined(ctx, func(pipe redis.Pipeliner) error { + res = pipe.Incr(ctx, rawKey) + if res.Val() == 1 && quotaRenewalRate > 0 { + pipe.Expire(ctx, rawKey, quotaRenewalRate) + } + return nil + }) + if err != nil { + logger.WithError(err).Error("error incrementing quota key") + return true } - }() - var expired bool - var expiredAt time.Time - dur, err := conn.PTTL(ctx, rawKey).Result() - if err == nil || errors.Is(err, redis.Nil) { - if err == nil { - expiredAt = now.Add(dur) - } else { - expired = true - expiredAt = now.Add(quotaRenewalRate) - conn.Set(ctx, rawKey, 0, quotaRenewalRate) + quota := res.Val() + blocked := quota-1 >= limit.QuotaMax + remaining := limit.QuotaMax - quota + if blocked { + remaining = 0 } - } else { - log.WithError(err).Warn("error getting key TTL, blocking") - return true - } - qInt, err := conn.Incr(ctx, rawKey).Result() - if err != nil && !errors.Is(err, redis.Nil) { - log.WithError(err).Error("can't update quota, blocking") - return true + logger = logger.WithField("quota", quota-1) + logger = logger.WithField("blocked", blocked) + logger = logger.WithField("remaining", remaining) + logger.Debug("[QUOTA] Update quota key") + + l.updateSessionQuota(session, scope, remaining, expiredAt.Unix()) + return blocked } - logFields := logrus.Fields{ - "quota": qInt - 1, - "quotaMax": quotaMax, - "expired": expired, - "expiredAt": expiredAt, + // If exists and not expired, just increment it. + if exists && !expired { + return increment() } - log.WithFields(logFields).Debug("[QUOTA] Request") + // if key is expired and can't renew, update the counter and + // block traffic going forward. + if limit.QuotaRenewalRate <= 0 { + return increment() + } - if qInt-1 >= quotaMax { - log.WithFields(logFields).Debug("[QUOTA] Limits reached") + // First, ensure a distributed lock + locker := limiter.NewLimiter(conn).Locker(rawKey) - if expired { - if quotaRenewalRate <= 0 { - return true - } + // Lock the key + if err := locker.Lock(ctx); err != nil { + // Increment the key if lock fails + return increment() + } - go store.DeleteRawKey(rawKey) - qInt = 1 - } else { - return true + // Unlock the key when done + defer func() { + if err := locker.Unlock(ctx); err != nil { + logger.WithError(err).Error("error unlocking quota key") } - } + }() - l.updateSessionQuota(session, scope, quotaMax-qInt, expiredAt.Unix()) - return false + // locked: reset quota + increment + conn.Set(ctx, rawKey, 0, quotaRenewalRate) + return increment() } func GetAccessDefinitionByAPIIDOrSession(currentSession *user.SessionState, api *APISpec) (accessDef *user.AccessDefinition, allowanceScope string, err error) { diff --git a/gateway/testutil.go b/gateway/testutil.go index 481af926fce..669e2a2846c 100644 --- a/gateway/testutil.go +++ b/gateway/testutil.go @@ -1148,7 +1148,7 @@ func (s *Test) newGateway(genConf func(globalConf *config.Config)) *Gateway { cli.Init(confPaths) - err = gw.initialiseSystem() + err = gw.initSystem() if err != nil { panic(err) } diff --git a/test/util.go b/test/util.go index 82d25dd2ffc..87f69049580 100644 --- a/test/util.go +++ b/test/util.go @@ -2,9 +2,15 @@ package test import ( "os" + "sync" "testing" ) +var ( + // exclusiveTestMu is used by Exclusive() + exclusiveTestMu sync.Mutex +) + // CI returns true when a non-empty CI env is present func CI() bool { return os.Getenv("CI") != "" @@ -20,6 +26,15 @@ func Racy(t *testing.T, fake ...func() (bool, func(...interface{}))) { skipCI(t, "Skipping Racy test", fake...) } +// Exclusive uses a lock to gate only a single test running. +func Exclusive(t *testing.T) { + t.Log("# Test marked as exclusive") + exclusiveTestMu.Lock() + t.Cleanup(func() { + exclusiveTestMu.Unlock() + }) +} + func skipCI(t *testing.T, message string, fake ...func() (bool, func(...interface{}))) { var ( ci = CI()