diff --git a/go.mod b/go.mod index 3384116d..a9287f0f 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/gin-gonic/gin v1.10.1 github.com/google/uuid v1.3.0 github.com/splitio/gincache v1.0.1 - github.com/splitio/go-split-commons/v8 v8.0.0-20251028203151-2b6d18a2f657 + github.com/splitio/go-split-commons/v8 v8.0.0-20251029203719-4fdb9d7a1ff2 github.com/splitio/go-toolkit/v5 v5.4.1 github.com/stretchr/testify v1.11.1 go.etcd.io/bbolt v1.3.6 diff --git a/go.sum b/go.sum index 0f54bde1..85736fbf 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,8 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/splitio/gincache v1.0.1 h1:dLYdANY/BqH4KcUMCe/LluLyV5WtuE/LEdQWRE06IXU= github.com/splitio/gincache v1.0.1/go.mod h1:CcgJDSM9Af75kyBH0724v55URVwMBuSj5x1eCWIOECY= -github.com/splitio/go-split-commons/v8 v8.0.0-20251028203151-2b6d18a2f657 h1:FYT0P+uFnXzALLgWOTIAJS6P4J1NpMGNi+rWsv2ZIkU= -github.com/splitio/go-split-commons/v8 v8.0.0-20251028203151-2b6d18a2f657/go.mod h1:vgRGPn0s4RC9/zp1nIn4KeeIEj/K3iXE2fxYQbCk/WI= +github.com/splitio/go-split-commons/v8 v8.0.0-20251029203719-4fdb9d7a1ff2 h1:M2+G0qWJhi5UC4yfQ8MePtDMmfCPlMXbL7+oJPKjGL8= +github.com/splitio/go-split-commons/v8 v8.0.0-20251029203719-4fdb9d7a1ff2/go.mod h1:vgRGPn0s4RC9/zp1nIn4KeeIEj/K3iXE2fxYQbCk/WI= github.com/splitio/go-toolkit/v5 v5.4.1 h1:srTyvDBJZMUcJ/KiiQDMyjCuELVgTBh2TGRVn0sOXEE= github.com/splitio/go-toolkit/v5 v5.4.1/go.mod h1:SifzysrOVDbzMcOE8zjX02+FG5az4FrR3Us/i5SeStw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/splitio/commitversion.go b/splitio/commitversion.go index 03f6673b..9e007c48 100644 --- a/splitio/commitversion.go +++ b/splitio/commitversion.go @@ -5,4 +5,4 @@ This file is created automatically, please do not edit */ // CommitVersion is the version of the last commit previous to release -const CommitVersion = "1807589" +const CommitVersion = "f8ab000" diff --git a/splitio/proxy/caching/workers_test.go b/splitio/proxy/caching/workers_test.go index 206832e7..e3893e3f 100644 --- a/splitio/proxy/caching/workers_test.go +++ b/splitio/proxy/caching/workers_test.go @@ -6,6 +6,7 @@ import ( "github.com/splitio/split-synchronizer/v5/splitio/proxy/caching/mocks" "github.com/splitio/go-split-commons/v8/dtos" + commons "github.com/splitio/go-split-commons/v8/storage/mocks" "github.com/splitio/go-split-commons/v8/synchronizer/worker/segment" "github.com/splitio/go-split-commons/v8/synchronizer/worker/split" "github.com/splitio/go-toolkit/v5/datastructures/set" @@ -19,9 +20,12 @@ func TestCacheAwareSplitSyncNoChanges(t *testing.T) { var cacheFlusherMock mocks.CacheFlusherMock var storageMock mocks.SplitStorageMock storageMock.On("ChangeNumber").Return(int64(-1), error(nil)) + var rbsStorage commons.MockRuleBasedSegmentStorage + rbsStorage.On("ChangeNumber").Return(int64(-1), error(nil)) css := CacheAwareSplitSynchronizer{ splitStorage: &storageMock, + rbStorage: &rbsStorage, wrapped: &splitSyncMock, cacheFlusher: &cacheFlusherMock, } @@ -39,6 +43,9 @@ func TestCacheAwareSplitSyncChanges(t *testing.T) { var splitSyncMock mocks.SplitUpdaterMock splitSyncMock.On("SynchronizeSplits", (*int64)(nil)).Return((*split.UpdateResult)(nil), error(nil)).Times(2) + var rbsStorage commons.MockRuleBasedSegmentStorage + rbsStorage.On("ChangeNumber").Return(int64(-1), error(nil)) + var cacheFlusherMock mocks.CacheFlusherMock cacheFlusherMock.On("EvictBySurrogate", SplitSurrogate).Times(3) @@ -49,6 +56,7 @@ func TestCacheAwareSplitSyncChanges(t *testing.T) { css := CacheAwareSplitSynchronizer{ splitStorage: &storageMock, wrapped: &splitSyncMock, + rbStorage: &rbsStorage, cacheFlusher: &cacheFlusherMock, } @@ -78,6 +86,9 @@ func TestCacheAwareSplitSyncChangesNewMethod(t *testing.T) { var splitSyncMock mocks.SplitUpdaterMock splitSyncMock.On("SynchronizeFeatureFlags", (*dtos.SplitChangeUpdate)(nil)).Return((*split.UpdateResult)(nil), error(nil)).Times(2) + var rbsStorage commons.MockRuleBasedSegmentStorage + rbsStorage.On("ChangeNumber").Return(int64(-1), error(nil)) + var cacheFlusherMock mocks.CacheFlusherMock cacheFlusherMock.On("EvictBySurrogate", SplitSurrogate).Times(2) @@ -87,6 +98,7 @@ func TestCacheAwareSplitSyncChangesNewMethod(t *testing.T) { css := CacheAwareSplitSynchronizer{ splitStorage: &storageMock, + rbStorage: &rbsStorage, wrapped: &splitSyncMock, cacheFlusher: &cacheFlusherMock, } diff --git a/splitio/proxy/controllers/sdk_test.go b/splitio/proxy/controllers/sdk_test.go index a8d7fecf..78284069 100644 --- a/splitio/proxy/controllers/sdk_test.go +++ b/splitio/proxy/controllers/sdk_test.go @@ -35,6 +35,8 @@ func TestSplitChangesImpressionsDisabled(t *testing.T) { splitStorage.On("ChangesSince", int64(-1), []string(nil)). Return(&dtos.SplitChangesDTO{Since: -1, Till: 1, Splits: []dtos.SplitDTO{{Name: "s1", Status: "ACTIVE", ImpressionsDisabled: true}, {Name: "s2", Status: "ACTIVE"}}}, nil). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() splitFetcher := &mocks.MockSplitFetcher{} var largeSegmentStorageMock largeSegmentStorageMock @@ -48,7 +50,7 @@ func TestSplitChangesImpressionsDisabled(t *testing.T) { splitFetcher, &splitStorage, nil, - nil, + &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2, @@ -88,6 +90,9 @@ func TestSplitChangesRecentSince(t *testing.T) { Return(&dtos.SplitChangesDTO{Since: -1, Till: 1, Splits: []dtos.SplitDTO{{Name: "s1", Status: "ACTIVE"}, {Name: "s2", Status: "ACTIVE"}}}, nil). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() + splitFetcher := &mocks.MockSplitFetcher{} var largeSegmentStorageMock largeSegmentStorageMock @@ -100,7 +105,7 @@ func TestSplitChangesRecentSince(t *testing.T) { splitFetcher, &splitStorage, nil, - nil, + &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2, @@ -137,9 +142,11 @@ func TestSplitChangesOlderSince(t *testing.T) { splitStorage.On("ChangesSince", int64(-1), []string(nil)). Return((*dtos.SplitChangesDTO)(nil), storage.ErrSinceParamTooOld). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() splitFetcher := &mocks.MockSplitFetcher{} - splitFetcher.On("Fetch", ref(*service.MakeFlagRequestParams().WithChangeNumber(-1).WithSpecVersion(common.StringRef(specs.FLAG_V1_2)))).Return( + splitFetcher.On("Fetch", ref(*service.MakeFlagRequestParams().WithChangeNumber(-1).WithChangeNumberRB(-1).WithSpecVersion(common.StringRef(specs.FLAG_V1_2)))).Return( &dtos.FFResponseLegacy{ SplitChanges: dtos.SplitChangesDTO{ Since: -1, Till: 1, Splits: []dtos.SplitDTO{{Name: "s1", Status: "ACTIVE"}, {Name: "s2", Status: "ACTIVE"}}, @@ -159,7 +166,7 @@ func TestSplitChangesOlderSince(t *testing.T) { splitFetcher, &splitStorage, nil, - nil, + &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2, @@ -197,8 +204,11 @@ func TestSplitChangesOlderSinceFetchFails(t *testing.T) { Return((*dtos.SplitChangesDTO)(nil), storage.ErrSinceParamTooOld). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() + splitFetcher := &mocks.MockSplitFetcher{} - splitFetcher.On("Fetch", ref(*service.MakeFlagRequestParams().WithChangeNumber(-1).WithSpecVersion(common.StringRef(specs.FLAG_V1_2)))). + splitFetcher.On("Fetch", ref(*service.MakeFlagRequestParams().WithChangeNumber(-1).WithChangeNumberRB(-1).WithSpecVersion(common.StringRef(specs.FLAG_V1_2)))). Return(nil, errors.New("something")). Once() @@ -215,7 +225,7 @@ func TestSplitChangesOlderSinceFetchFails(t *testing.T) { splitFetcher, &splitStorage, nil, - nil, + &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2, @@ -243,6 +253,9 @@ func TestSplitChangesWithFlagSets(t *testing.T) { Return(&dtos.SplitChangesDTO{Since: -1, Till: 1, Splits: []dtos.SplitDTO{{Name: "s1", Status: "ACTIVE"}, {Name: "s2", Status: "ACTIVE"}}}, nil). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() + splitFetcher := &mocks.MockSplitFetcher{} var largeSegmentStorageMock largeSegmentStorageMock @@ -257,7 +270,7 @@ func TestSplitChangesWithFlagSets(t *testing.T) { splitFetcher, &splitStorage, nil, - nil, + &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2, @@ -294,6 +307,9 @@ func TestSplitChangesWithFlagSetsStrict(t *testing.T) { Return(&dtos.SplitChangesDTO{Since: -1, Till: 1, Splits: []dtos.SplitDTO{{Name: "s1", Status: "ACTIVE"}, {Name: "s2", Status: "ACTIVE"}}}, nil). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() + splitFetcher := &mocks.MockSplitFetcher{} var largeSegmentStorageMock largeSegmentStorageMock @@ -308,7 +324,7 @@ func TestSplitChangesWithFlagSetsStrict(t *testing.T) { splitFetcher, &splitStorage, nil, - nil, + &rbsStorage, flagsets.NewMatcher(true, []string{"a", "c"}), &largeSegmentStorageMock, specs.FLAG_V1_2, @@ -365,6 +381,9 @@ func TestSplitChangesNewMatcherOldSpec(t *testing.T) { }, nil). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() + splitFetcher := &mocks.MockSplitFetcher{} var largeSegmentStorageMock largeSegmentStorageMock @@ -377,7 +396,7 @@ func TestSplitChangesNewMatcherOldSpec(t *testing.T) { splitFetcher, &splitStorage, nil, - nil, + &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2, @@ -436,6 +455,8 @@ func TestSplitChangesNewMatcherNewSpec(t *testing.T) { }, }, nil). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() splitFetcher := &mocks.MockSplitFetcher{} var largeSegmentStorageMock largeSegmentStorageMock @@ -449,7 +470,7 @@ func TestSplitChangesNewMatcherNewSpec(t *testing.T) { splitFetcher, &splitStorage, nil, - nil, + &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2, @@ -497,6 +518,9 @@ func TestSegmentChanges(t *testing.T) { Return(&dtos.SegmentChangesDTO{Name: "someSegment", Added: []string{"k1", "k2"}, Removed: []string{}, Since: -1, Till: 1}, nil). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() + var largeSegmentStorageMock largeSegmentStorageMock resp := httptest.NewRecorder() @@ -505,7 +529,7 @@ func TestSegmentChanges(t *testing.T) { logger := logging.NewLogger(nil) group := router.Group("/api") - controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, nil, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) + controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) controller.Register(group) ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/segmentChanges/someSegment?since=-1", nil) @@ -540,6 +564,8 @@ func TestSegmentChangesNotFound(t *testing.T) { segmentStorage.On("ChangesSince", "someSegment", int64(-1)). Return((*dtos.SegmentChangesDTO)(nil), storage.ErrSegmentNotFound). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() var largeSegmentStorageMock largeSegmentStorageMock @@ -549,7 +575,7 @@ func TestSegmentChangesNotFound(t *testing.T) { logger := logging.NewLogger(nil) group := router.Group("/api") - controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, nil, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) + controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) controller.Register(group) ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/segmentChanges/someSegment?since=-1", nil) @@ -575,6 +601,9 @@ func TestMySegments(t *testing.T) { Return([]string{"segment1", "segment2"}, nil). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() + var largeSegmentStorageMock largeSegmentStorageMock resp := httptest.NewRecorder() @@ -583,7 +612,7 @@ func TestMySegments(t *testing.T) { logger := logging.NewLogger(nil) group := router.Group("/api") - controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, nil, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) + controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) controller.Register(group) ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/mySegments/someKey", nil) @@ -618,6 +647,9 @@ func TestMySegmentsError(t *testing.T) { Return([]string(nil), errors.New("something")). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() + var largeSegmentStorageMock largeSegmentStorageMock resp := httptest.NewRecorder() @@ -626,7 +658,7 @@ func TestMySegmentsError(t *testing.T) { logger := logging.NewLogger(nil) group := router.Group("/api") - controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, nil, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) + controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) controller.Register(group) ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/mySegments/someKey", nil) @@ -651,6 +683,8 @@ func TestMemberships(t *testing.T) { segmentStorage.On("SegmentsFor", "keyTest"). Return([]string{"segment1", "segment2"}, nil). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() var largeSegmentStorageMock largeSegmentStorageMock largeSegmentStorageMock.On("LargeSegmentsForUser", "keyTest"). @@ -663,7 +697,7 @@ func TestMemberships(t *testing.T) { logger := logging.NewLogger(nil) group := router.Group("/api") - controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, nil, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) + controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) controller.Register(group) ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/memberships/keyTest", nil) @@ -706,6 +740,8 @@ func TestMembershipsError(t *testing.T) { segmentStorage.On("SegmentsFor", "keyTest"). Return([]string{}, errors.New("error message.")). Once() + var rbsStorage psmocks.MockProxyRuleBasedSegmentStorage + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() resp := httptest.NewRecorder() ctx, router := gin.CreateTestContext(resp) @@ -713,7 +749,7 @@ func TestMembershipsError(t *testing.T) { logger := logging.NewLogger(nil) group := router.Group("/api") - controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, nil, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) + controller := NewSdkServerController(logger, splitFetcher, &splitStorage, &segmentStorage, &rbsStorage, flagsets.NewMatcher(false, nil), &largeSegmentStorageMock, specs.FLAG_V1_2) controller.Register(group) ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/memberships/keyTest", nil) diff --git a/splitio/proxy/proxy_test.go b/splitio/proxy/proxy_test.go index fe8c8cbb..a2d63f97 100644 --- a/splitio/proxy/proxy_test.go +++ b/splitio/proxy/proxy_test.go @@ -26,6 +26,8 @@ func TestSplitChangesEndpoints(t *testing.T) { opts := makeOpts() var splitStorage pstorageMocks.ProxySplitStorageMock opts.ProxySplitStorage = &splitStorage + var rbsStorage pstorageMocks.MockProxyRuleBasedSegmentStorage + opts.ProxyRBSegmentStorage = &rbsStorage proxy := New(opts) go proxy.Start() time.Sleep(1 * time.Second) // Let the scheduler switch the current thread/gr and start the server @@ -37,6 +39,7 @@ func TestSplitChangesEndpoints(t *testing.T) { splitStorage.On("ChangesSince", int64(-1), []string(nil)). Return(&dtos.SplitChangesDTO{Since: -1, Till: 1, Splits: []dtos.SplitDTO{{Name: "split1", ImpressionsDisabled: true}}}, nil). Once() + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() // Make a proper request status, body, headers := get("splitChanges?since=-1", opts.Port, map[string]string{"Authorization": "Bearer someApiKey"}) @@ -65,6 +68,8 @@ func TestSplitChangesEndpoints(t *testing.T) { Return(&dtos.SplitChangesDTO{Since: -1, Till: 2, Splits: []dtos.SplitDTO{{Name: "split2"}}}, nil). Once() + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() + opts.Cache.EvictBySurrogate(caching.SplitSurrogate) _, body, headers = get("splitChanges?since=-1", opts.Port, map[string]string{"Authorization": "Bearer someApiKey"}) @@ -81,6 +86,8 @@ func TestSplitChangesWithFlagsetsCaching(t *testing.T) { opts := makeOpts() var splitStorage pstorageMocks.ProxySplitStorageMock opts.ProxySplitStorage = &splitStorage + var rbsStorage pstorageMocks.MockProxyRuleBasedSegmentStorage + opts.ProxyRBSegmentStorage = &rbsStorage proxy := New(opts) go proxy.Start() time.Sleep(1 * time.Second) // Let the scheduler switch the current thread/gr and start the server @@ -88,6 +95,7 @@ func TestSplitChangesWithFlagsetsCaching(t *testing.T) { splitStorage.On("ChangesSince", int64(-1), []string{"set1", "set2"}). Return(&dtos.SplitChangesDTO{Since: -1, Till: 1, Splits: []dtos.SplitDTO{{Name: "split1"}}}, nil). Once() + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() // Make a proper request status, body, headers := get("splitChanges?since=-1&sets=set2,set1", opts.Port, map[string]string{"Authorization": "Bearer someApiKey"}) @@ -113,6 +121,7 @@ func TestSplitChangesWithFlagsetsCaching(t *testing.T) { splitStorage.On("ChangesSince", int64(-1), []string{"set1", "set2", "set3"}). Return(&dtos.SplitChangesDTO{Since: -1, Till: 1, Splits: []dtos.SplitDTO{{Name: "split1"}}}, nil). Once() + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() status, body, headers = get("splitChanges?since=-1&sets=set2,set1,set3", opts.Port, map[string]string{"Authorization": "Bearer someApiKey"}) assert.Equal(t, 200, status) @@ -129,10 +138,12 @@ func TestSplitChangesWithFlagsetsCaching(t *testing.T) { splitStorage.On("ChangesSince", int64(-1), []string{"set1", "set2"}). Return(&dtos.SplitChangesDTO{Since: -1, Till: 1, Splits: []dtos.SplitDTO{{Name: "split1"}}}, nil). Once() + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() splitStorage.On("ChangesSince", int64(-1), []string{"set1", "set2", "set3"}). Return(&dtos.SplitChangesDTO{Since: -1, Till: 1, Splits: []dtos.SplitDTO{{Name: "split1"}}}, nil). Once() + rbsStorage.On("ChangesSince", int64(-1)).Return(&dtos.RuleBasedSegmentsDTO{}).Once() status, body, headers = get("splitChanges?since=-1&sets=set2,set1", opts.Port, map[string]string{"Authorization": "Bearer someApiKey"}) assert.Equal(t, 200, status) diff --git a/splitio/proxy/storage/mocks/rbsstorage.go b/splitio/proxy/storage/mocks/rbsstorage.go new file mode 100644 index 00000000..14c56fb3 --- /dev/null +++ b/splitio/proxy/storage/mocks/rbsstorage.go @@ -0,0 +1,19 @@ +package mocks + +import ( + "github.com/splitio/go-split-commons/v8/dtos" + "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage" + "github.com/stretchr/testify/mock" +) + +type MockProxyRuleBasedSegmentStorage struct { + mock.Mock +} + +// ChangeNumber mock +func (m *MockProxyRuleBasedSegmentStorage) ChangesSince(since int64) (*dtos.RuleBasedSegmentsDTO, error) { + args := m.Called(since) + return args.Get(0).(*dtos.RuleBasedSegmentsDTO), nil +} + +var _ storage.ProxyRuleBasedSegmentsStorage = (*MockProxyRuleBasedSegmentStorage)(nil) diff --git a/splitio/proxy/storage/optimized/rbshistoric.go b/splitio/proxy/storage/optimized/rbshistoric.go new file mode 100644 index 00000000..61d2063b --- /dev/null +++ b/splitio/proxy/storage/optimized/rbshistoric.go @@ -0,0 +1,120 @@ +package optimized + +import ( + "sort" + "sync" + + "github.com/splitio/go-split-commons/v8/dtos" +) + +type HistoricChangesRB interface { + GetUpdatedSince(since int64) []RBView + Update(toAdd []dtos.RuleBasedSegmentDTO, toRemove []dtos.RuleBasedSegmentDTO, newCN int64) +} + +type HistoricChangesRBImpl struct { + data []RBView + mutex sync.RWMutex +} + +func NewHistoricRBChanges(capacity int) *HistoricChangesRBImpl { + return &HistoricChangesRBImpl{ + data: make([]RBView, 0, capacity), + } +} + +func (h *HistoricChangesRBImpl) GetUpdatedSince(since int64) []RBView { + h.mutex.RLock() + views := h.findNewerThan(since) + toRet := copyAndFilterRB(views, since) + h.mutex.RUnlock() + return toRet +} + +func (h *HistoricChangesRBImpl) Update(toAdd []dtos.RuleBasedSegmentDTO, toRemove []dtos.RuleBasedSegmentDTO, newCN int64) { + h.mutex.Lock() + h.updateFrom(toAdd) + h.updateFrom(toRemove) + sort.Slice(h.data, func(i, j int) bool { return h.data[i].LastUpdated < h.data[j].LastUpdated }) + h.mutex.Unlock() +} + +// public interface ends here + +func (h *HistoricChangesRBImpl) updateFrom(source []dtos.RuleBasedSegmentDTO) { + for idx := range source { + if current := h.findByName(source[idx].Name); current != nil { + current.updateFrom(&source[idx]) + } else { + var toAdd RBView + toAdd.updateFrom(&source[idx]) + h.data = append(h.data, toAdd) + } + } +} + +func (h *HistoricChangesRBImpl) findByName(name string) *RBView { + // yes, it's linear search because features are sorted by CN, but it's only used + // when processing an update coming from the BE. it's off the critical path of incoming + // requests. + for idx := range h.data { + if h.data[idx].Name == name { + return &h.data[idx] + } + } + return nil +} + +func (h *HistoricChangesRBImpl) findNewerThan(since int64) []RBView { + // precondition: h.data is sorted by CN + start := sort.Search(len(h.data), func(i int) bool { return h.data[i].LastUpdated > since }) + if start == len(h.data) { + return nil + } + return h.data[start:] +} + +type RBView struct { + Name string + Active bool + LastUpdated int64 +} + +func (f *RBView) updateFrom(s *dtos.RuleBasedSegmentDTO) { + f.Name = s.Name + f.Active = s.Status == "ACTIVE" + f.LastUpdated = s.ChangeNumber +} + +func (f *RBView) clone() RBView { + toRet := RBView{ + Name: f.Name, + Active: f.Active, + LastUpdated: f.LastUpdated, + } + return toRet + +} + +func copyAndFilterRB(views []RBView, since int64) []RBView { + toRet := make([]RBView, 0, len(views)) + + // this code computes the intersection in o(views * ) + for idx := range views { + if featureShouldBeReturnedRB(&views[idx], since) { + toRet = append(toRet, views[idx].clone()) + } + } + return toRet +} + +func featureShouldBeReturnedRB(view *RBView, since int64) bool { + // if fetching from sratch & the feature is not active, + // or it hasn't been updated since `since`, it shouldn't even be considered for being returned + if since == -1 && !view.Active || view.LastUpdated < since { + return false + } + return true +} + +var _ HistoricChangesRB = (*HistoricChangesRBImpl)(nil) diff --git a/splitio/proxy/storage/optimized/rbshistoric_test.go b/splitio/proxy/storage/optimized/rbshistoric_test.go new file mode 100644 index 00000000..e1cc289a --- /dev/null +++ b/splitio/proxy/storage/optimized/rbshistoric_test.go @@ -0,0 +1,109 @@ +package optimized + +import ( + "testing" + + "github.com/splitio/go-split-commons/v8/dtos" + + "github.com/stretchr/testify/assert" +) + +func TestHistoricRuleBasedSegmentStorage(t *testing.T) { + var historic HistoricChangesRBImpl + historic.Update([]dtos.RuleBasedSegmentDTO{ + {Name: "rbs1", Status: "ACTIVE", ChangeNumber: 1, TrafficTypeName: "tt1"}, + }, []dtos.RuleBasedSegmentDTO{}, 1) + assert.Equal(t, + []RBView{ + {Name: "rbs1", Active: true, LastUpdated: 1}, + }, + historic.GetUpdatedSince(-1)) + + // process an update with no change in rule-based segments status + // - fetching from -1 && 1 should return the same paylaod as before with only `lastUpdated` bumped to 2 + // - fetching from 2 should return empty + historic.Update([]dtos.RuleBasedSegmentDTO{ + {Name: "rbs1", Status: "ACTIVE", ChangeNumber: 2, TrafficTypeName: "tt1"}, + }, []dtos.RuleBasedSegmentDTO{}, 1) + + // no filter + assert.Equal(t, + []RBView{ + {Name: "rbs1", Active: true, LastUpdated: 2}, + }, + historic.GetUpdatedSince(-1)) + assert.Equal(t, + []RBView{ + {Name: "rbs1", Active: true, LastUpdated: 2}, + }, + historic.GetUpdatedSince(1)) + assert.Equal(t, []RBView{}, historic.GetUpdatedSince(2)) + + // ------------------- + + // process an update with one extra rule-based + // - fetching from -1, & 1 should return the same payload + // - fetching from 2 shuold only return rbs2 + // - fetching from 3 should return empty + historic.Update([]dtos.RuleBasedSegmentDTO{ + {Name: "rbs2", Status: "ACTIVE", ChangeNumber: 3, TrafficTypeName: "tt1"}, + }, []dtos.RuleBasedSegmentDTO{}, 1) + + // assert correct behaviours for CN == 1..3 and no flag sets filter + assert.Equal(t, + []RBView{ + {Name: "rbs1", Active: true, LastUpdated: 2}, + {Name: "rbs2", Active: true, LastUpdated: 3}, + }, + historic.GetUpdatedSince(-1)) + assert.Equal(t, + []RBView{ + {Name: "rbs1", Active: true, LastUpdated: 2}, + {Name: "rbs2", Active: true, LastUpdated: 3}, + }, + historic.GetUpdatedSince(1)) + assert.Equal(t, + []RBView{ + {Name: "rbs2", Active: true, LastUpdated: 3}, + }, + historic.GetUpdatedSince(2)) + assert.Equal(t, []RBView{}, historic.GetUpdatedSince(3)) + + // process an update that removes rbs2 (archives the rule-based) + // fetching from -1 should not return rbs2 + // fetching from any intermediate CN should return rbs2 as archived + // fetching from cn=5 should return empty + historic.Update([]dtos.RuleBasedSegmentDTO{ + {Name: "rbs2", Status: "ARCHIVED", ChangeNumber: 5, TrafficTypeName: "tt1"}, + }, []dtos.RuleBasedSegmentDTO{}, 1) + + // without filter + assert.Equal(t, + []RBView{ + {Name: "rbs1", Active: true, LastUpdated: 2}, + }, + historic.GetUpdatedSince(-1)) + assert.Equal(t, + []RBView{ + {Name: "rbs1", Active: true, LastUpdated: 2}, + {Name: "rbs2", Active: false, LastUpdated: 5}, + }, + historic.GetUpdatedSince(1)) + assert.Equal(t, + []RBView{ + {Name: "rbs2", Active: false, LastUpdated: 5}, + }, + historic.GetUpdatedSince(2)) + assert.Equal(t, + []RBView{ + {Name: "rbs2", Active: false, LastUpdated: 5}, + }, + historic.GetUpdatedSince(3)) + assert.Equal(t, + []RBView{ + {Name: "rbs2", Active: false, LastUpdated: 5}, + }, + historic.GetUpdatedSince(4)) + assert.Equal(t, []RBView{}, historic.GetUpdatedSince(5)) + +} diff --git a/splitio/proxy/storage/rulebasedsegments.go b/splitio/proxy/storage/rulebasedsegments.go index 8e9736e3..73b831b5 100644 --- a/splitio/proxy/storage/rulebasedsegments.go +++ b/splitio/proxy/storage/rulebasedsegments.go @@ -1,11 +1,16 @@ package storage import ( + "fmt" + "sync" + "github.com/splitio/go-split-commons/v8/dtos" + "github.com/splitio/go-split-commons/v8/engine/grammar/constants" "github.com/splitio/go-split-commons/v8/storage" "github.com/splitio/go-split-commons/v8/storage/inmemory/mutexmap" "github.com/splitio/go-toolkit/v5/datastructures/set" "github.com/splitio/go-toolkit/v5/logging" + "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage/optimized" ) // ProxyRuleBasedSegmentsStorage defines the interface of a storage that can be used for serving payloads @@ -14,31 +19,96 @@ type ProxyRuleBasedSegmentsStorage interface { ChangesSince(since int64) (*dtos.RuleBasedSegmentsDTO, error) } -// ProxyRuleBasedSegmentsStorageImpl implements the ProxyRuleBasedSegmentsStorage interface and the SplitProducer interface +// ProxyRuleBasedSegmentsStorageImpl implements the ProxyRuleBasedSegmentsStorage interface and the RuleBasedSegmentProducer interface type ProxyRuleBasedSegmentsStorageImpl struct { - snapshot mutexmap.RuleBasedSegmentsStorageImpl - logger logging.LoggerInterface - // mtx sync.Mutex + snapshot mutexmap.RuleBasedSegmentsStorageImpl + logger logging.LoggerInterface + oldestKnownCN int64 + mtx sync.Mutex + historic optimized.HistoricChangesRB } // NewProxyRuleBasedSegmentsStorage instantiates a new proxy storage that wraps an in-memory snapshot of the last known // flag configuration func NewProxyRuleBasedSegmentsStorage(logger logging.LoggerInterface) *ProxyRuleBasedSegmentsStorageImpl { snapshot := mutexmap.NewRuleBasedSegmentsStorage() + historic := optimized.NewHistoricRBChanges(1000) + var initialCN int64 = -1 return &ProxyRuleBasedSegmentsStorageImpl{ - snapshot: *snapshot, - logger: logger, + snapshot: *snapshot, + logger: logger, + oldestKnownCN: initialCN, + historic: historic, + } +} + +func (p *ProxyRuleBasedSegmentsStorageImpl) sinceIsTooOld(since int64) bool { + if since == -1 { + return false + } + + p.mtx.Lock() + defer p.mtx.Unlock() + return since < p.oldestKnownCN +} + +func archivedRBDTOForView(view *optimized.RBView) dtos.RuleBasedSegmentDTO { + return dtos.RuleBasedSegmentDTO{ + ChangeNumber: view.LastUpdated, + Name: view.Name, + Status: constants.SplitStatusActive, } } // ChangesSince retrieves the rule-based segments changes since the given change number func (p *ProxyRuleBasedSegmentsStorageImpl) ChangesSince(since int64) (*dtos.RuleBasedSegmentsDTO, error) { - if since > -1 { - return &dtos.RuleBasedSegmentsDTO{Since: since, Till: since, RuleBasedSegments: []dtos.RuleBasedSegmentDTO{}}, nil + // No flagsets and fetching from -1, return the current snapshot + if since == -1 { + cn, err := p.snapshot.ChangeNumber() + if err != nil { + return nil, fmt.Errorf("error fetching changeNumber from snapshot: %w", err) + } + all := p.snapshot.All() + return &dtos.RuleBasedSegmentsDTO{Since: since, Till: cn, RuleBasedSegments: all}, nil + } + + if p.sinceIsTooOld(since) { + return nil, ErrSinceParamTooOld + } + + views := p.historic.GetUpdatedSince(since) + namesToFetch := make([]string, 0, len(views)) + all := make([]dtos.RuleBasedSegmentDTO, 0, len(views)) + var till int64 = since + for idx := range views { + if t := views[idx].LastUpdated; t > till { + till = t + } + if views[idx].Active { + namesToFetch = append(namesToFetch, views[idx].Name) + } else { + all = append(all, archivedRBDTOForView(&views[idx])) + } + } + + for name, rbSegments := range p.snapshot.FetchMany(namesToFetch) { + if rbSegments == nil { + p.logger.Warning(fmt.Sprintf( + "possible inconsistency between historic & snapshot storages. Rule-based segment `%s` is missing in the latter", + name, + )) + continue + } + all = append(all, *rbSegments) } - cn, _ := p.snapshot.ChangeNumber() - return &dtos.RuleBasedSegmentsDTO{Since: since, Till: cn, RuleBasedSegments: p.snapshot.All()}, nil + return &dtos.RuleBasedSegmentsDTO{Since: since, Till: till, RuleBasedSegments: all}, nil + + // if since > -1 { + // return &dtos.RuleBasedSegmentsDTO{Since: since, Till: since, RuleBasedSegments: []dtos.RuleBasedSegmentDTO{}}, nil + // } + // cn, _ := p.snapshot.ChangeNumber() + // return &dtos.RuleBasedSegmentsDTO{Since: since, Till: cn, RuleBasedSegments: p.snapshot.All()}, nil } // All call is forwarded to the snapshot @@ -87,23 +157,31 @@ func (p *ProxyRuleBasedSegmentsStorageImpl) SetChangeNumber(cn int64) error { } // Update -func (p *ProxyRuleBasedSegmentsStorageImpl) Update(toAdd []dtos.RuleBasedSegmentDTO, toRemove []dtos.RuleBasedSegmentDTO, cn int64) error { +func (p *ProxyRuleBasedSegmentsStorageImpl) Update(toAdd []dtos.RuleBasedSegmentDTO, toRemove []dtos.RuleBasedSegmentDTO, changeNumber int64) error { // TODO Add the other logic - // p.setStartingPoint(changeNumber) // will be executed only the first time this method is called + p.setStartingPoint(changeNumber) // will be executed only the first time this method is called - // if len(toAdd) == 0 && len(toRemove) == 0 { - // return - // } + if len(toAdd) == 0 && len(toRemove) == 0 { + return nil + } - // p.mtx.Lock() - // p.snapshot.Update(toAdd, toRemove, changeNumber) - // p.historic.Update(toAdd, toRemove, changeNumber) + p.mtx.Lock() + p.snapshot.Update(toAdd, toRemove, changeNumber) + p.historic.Update(toAdd, toRemove, changeNumber) // p.db.Update(toAdd, toRemove, changeNumber) - // p.mtx.Unlock() - - p.snapshot.Update(toAdd, toRemove, cn) + p.mtx.Unlock() return nil } +func (p *ProxyRuleBasedSegmentsStorageImpl) setStartingPoint(cn int64) { + p.mtx.Lock() + // will be executed only the first time this method is called or when + // an older change is registered + if p.oldestKnownCN == -1 || cn < p.oldestKnownCN { + p.oldestKnownCN = cn + } + p.mtx.Unlock() +} + var _ ProxyRuleBasedSegmentsStorage = (*ProxyRuleBasedSegmentsStorageImpl)(nil) var _ storage.RuleBasedSegmentsStorage = (*ProxyRuleBasedSegmentsStorageImpl)(nil) diff --git a/splitio/proxy/storage/splits.go b/splitio/proxy/storage/splits.go index cefc7688..1e4960f4 100644 --- a/splitio/proxy/storage/splits.go +++ b/splitio/proxy/storage/splits.go @@ -10,6 +10,7 @@ import ( "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage/persistent" "github.com/splitio/go-split-commons/v8/dtos" + "github.com/splitio/go-split-commons/v8/engine/grammar/constants" "github.com/splitio/go-split-commons/v8/flagsets" "github.com/splitio/go-split-commons/v8/storage" "github.com/splitio/go-split-commons/v8/storage/inmemory/mutexmap" @@ -198,7 +199,7 @@ func (p *ProxySplitStorageImpl) ReplaceAll(splits []dtos.SplitDTO, changeNumber } func (p *ProxySplitStorageImpl) RuleBasedSegmentNames() *set.ThreadUnsafeSet { - panic("not implemented") + return p.snapshot.RuleBasedSegmentNames() } func (p *ProxySplitStorageImpl) sinceIsTooOld(since int64) bool { @@ -249,7 +250,7 @@ func archivedDTOForView(view *optimized.FeatureView) dtos.SplitDTO { TrafficAllocation: 100, TrafficAllocationSeed: 0, Seed: 0, - Status: "ARCHIVED", + Status: constants.SplitStatusActive, Killed: false, DefaultTreatment: "off", Algo: 1,