Skip to content

Commit d6fd601

Browse files
authored
fix: Optimize creation of changesets from collections (#335)
Previously, we worked to cache the changset -> collections process as it is used in many different places. There are multiple places where a changeset is created from a collection. With this change, we take advantage of this knowledge, and pre-cache the collection that created the changeset, preventing us from doing the conversion at all.
1 parent 378fadc commit d6fd601

File tree

4 files changed

+160
-62
lines changed

4 files changed

+160
-62
lines changed

internal/datasourcev2/fdv1_polling_data_source.go

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ package datasourcev2
22

33
import (
44
"context"
5-
"encoding/json"
65
"net/http"
76

87
"github.com/launchdarkly/go-sdk-common/v3/ldlog"
9-
"github.com/launchdarkly/go-server-sdk/v7/internal/datakinds"
108
"github.com/launchdarkly/go-server-sdk/v7/internal/datasource"
119
"github.com/launchdarkly/go-server-sdk/v7/subsystems"
1210
)
@@ -33,38 +31,22 @@ func (r *fdv1ToFDv2Requester) Request(
3331
reason = "cached"
3432
}
3533

36-
changeSetBuilder := subsystems.NewChangeSetBuilder()
37-
changeSetBuilder.Start(subsystems.ServerIntent{
34+
intent := subsystems.ServerIntent{
3835
Payload: subsystems.Payload{
3936
ID: "arbitrary-id",
4037
Target: 0,
4138
Code: code,
4239
Reason: reason,
4340
},
44-
})
45-
46-
for _, item := range data {
47-
kind := subsystems.FlagKind
48-
if item.Kind == datakinds.Segments {
49-
kind = subsystems.SegmentKind
50-
}
51-
52-
for _, keyedItem := range item.Items {
53-
if keyedItem.Item.Item == nil {
54-
continue
55-
}
56-
57-
bytes, err := json.Marshal(keyedItem.Item.Item)
58-
if err != nil {
59-
r.loggers.Warn("Error marshalling v1 item to JSON: %s", err)
60-
return nil, headers, err
61-
}
62-
63-
changeSetBuilder.AddPut(kind, keyedItem.Key, keyedItem.Item.Version, bytes)
64-
}
6541
}
6642

67-
changeset, err := changeSetBuilder.Finish(subsystems.NewSelector("state", 1))
43+
// Data is already in Collection format, use NewChangeSetFromCollections
44+
// to avoid the overhead of converting Collections -> Changes -> Collections
45+
changeset, err := subsystems.NewChangeSetFromCollections(
46+
intent,
47+
subsystems.NewSelector("state", 1),
48+
data,
49+
)
6850
return changeset, headers, err
6951
}
7052

ldfiledatav2/file_data_source_impl.go

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/launchdarkly/go-server-sdk/v7/interfaces"
2121
"github.com/launchdarkly/go-server-sdk/v7/internal"
2222
"github.com/launchdarkly/go-server-sdk/v7/subsystems"
23+
"github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoreimpl"
2324
"github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes"
2425

2526
"gopkg.in/ghodss/yaml.v1"
@@ -247,12 +248,11 @@ type fileData struct {
247248
Segments *map[string]ldmodel.Segment
248249
}
249250

250-
func insertData(
251+
func insertDataIntoCollection(
252+
items *[]ldstoretypes.KeyedItemDescriptor,
251253
seenKeys map[subsystems.ObjectKind]map[string]bool,
252-
builder *subsystems.ChangeSetBuilder,
253254
objectKind subsystems.ObjectKind,
254255
key string,
255-
version int,
256256
data ldstoretypes.ItemDescriptor,
257257
duplicateKeysHandling DuplicateKeysHandling,
258258
) error {
@@ -265,12 +265,10 @@ func insertData(
265265
}
266266
}
267267

268-
json, err := json.Marshal(data.Item)
269-
if err != nil {
270-
return fmt.Errorf("error serializing %s '%s': %w", objectKind, key, err)
271-
}
272-
273-
builder.AddPut(objectKind, key, version, json)
268+
*items = append(*items, ldstoretypes.KeyedItemDescriptor{
269+
Key: key,
270+
Item: data,
271+
})
274272
seenKeys[objectKind][key] = true
275273

276274
return nil
@@ -304,25 +302,29 @@ func mergeFileData(
304302
version int,
305303
allFileData ...fileData,
306304
) (*subsystems.ChangeSet, error) {
307-
builder := subsystems.NewChangeSetBuilder()
308-
builder.Start(subsystems.ServerIntent{
305+
intent := subsystems.ServerIntent{
309306
Payload: subsystems.Payload{
310307
ID: "",
311308
Target: version,
312309
Code: subsystems.IntentTransferFull,
313310
Reason: "payload-missing",
314311
},
315-
})
312+
}
313+
314+
// Build collections directly instead of using ChangeSetBuilder
315+
flagItems := make([]ldstoretypes.KeyedItemDescriptor, 0)
316+
segmentItems := make([]ldstoretypes.KeyedItemDescriptor, 0)
316317

317318
seenKeys := map[subsystems.ObjectKind]map[string]bool{
318319
subsystems.FlagKind: {},
319320
subsystems.SegmentKind: {},
320321
}
322+
321323
for _, d := range allFileData {
322324
if d.Flags != nil {
323325
for key, f := range *d.Flags {
324326
data := ldstoretypes.ItemDescriptor{Version: f.Version, Item: &f}
325-
err := insertData(seenKeys, builder, subsystems.FlagKind, key, f.Version, data, duplicateKeysHandling)
327+
err := insertDataIntoCollection(&flagItems, seenKeys, subsystems.FlagKind, key, data, duplicateKeysHandling)
326328
if err != nil {
327329
return nil, err
328330
}
@@ -332,7 +334,7 @@ func mergeFileData(
332334
for key, value := range *d.FlagValues {
333335
flag := makeFlagWithValue(key, value)
334336
data := ldstoretypes.ItemDescriptor{Version: flag.Version, Item: flag}
335-
err := insertData(seenKeys, builder, subsystems.FlagKind, key, flag.Version, data, duplicateKeysHandling)
337+
err := insertDataIntoCollection(&flagItems, seenKeys, subsystems.FlagKind, key, data, duplicateKeysHandling)
336338
if err != nil {
337339
return nil, err
338340
}
@@ -341,19 +343,34 @@ func mergeFileData(
341343
if d.Segments != nil {
342344
for key, s := range *d.Segments {
343345
data := ldstoretypes.ItemDescriptor{Version: s.Version, Item: &s}
344-
err := insertData(seenKeys, builder, subsystems.SegmentKind, key, s.Version, data, duplicateKeysHandling)
346+
err := insertDataIntoCollection(&segmentItems, seenKeys, subsystems.SegmentKind, key, data, duplicateKeysHandling)
345347
if err != nil {
346348
return nil, err
347349
}
348350
}
349351
}
350352
}
351353

354+
// Build collections
355+
collections := make([]ldstoretypes.Collection, 0, 2)
356+
if len(flagItems) > 0 {
357+
collections = append(collections, ldstoretypes.Collection{
358+
Kind: ldstoreimpl.Features(),
359+
Items: flagItems,
360+
})
361+
}
362+
if len(segmentItems) > 0 {
363+
collections = append(collections, ldstoretypes.Collection{
364+
Kind: ldstoreimpl.Segments(),
365+
Items: segmentItems,
366+
})
367+
}
368+
352369
// File data source will not have a selector for now.
353370
// NOTE: If we start supporting FDv2 data from file then this statement might change.
354371
// When that happens we will construct the selector the same way that we construct it
355372
// in the FDv2 polling data source.
356-
return builder.Finish(subsystems.NoSelector())
373+
return subsystems.NewChangeSetFromCollections(intent, subsystems.NoSelector(), collections)
357374
}
358375

359376
func makeFlagWithValue(key string, v interface{}) *ldmodel.FeatureFlag {

subsystems/changeset.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,86 @@ func NewChangeSetBuilder() *ChangeSetBuilder {
160160
return &ChangeSetBuilder{}
161161
}
162162

163+
// NewChangeSetFromCollections creates a ChangeSet directly from collections,
164+
// pre-populating the collections cache to avoid redundant conversions.
165+
//
166+
// This is useful when data is already in Collection format (e.g., from file data sources
167+
// or offline mode) and avoids the overhead of converting Collections -> Changes -> Collections.
168+
//
169+
// This function is not stable, and not subject to any backwards
170+
// compatibility guarantees or semantic versioning. It is not suitable for production usage.
171+
//
172+
// Do not use it.
173+
// You have been warned.
174+
func NewChangeSetFromCollections(
175+
intent ServerIntent,
176+
selector Selector,
177+
collections []ldstoretypes.Collection,
178+
) (*ChangeSet, error) {
179+
// Build Changes from collections for the ChangeSet API
180+
changes, err := collectionsToChanges(collections)
181+
if err != nil {
182+
return nil, err
183+
}
184+
185+
return &ChangeSet{
186+
intentCode: intent.Payload.Code,
187+
selector: selector,
188+
changes: changes,
189+
collection: collections, // Pre-populate the cache!
190+
mu: &sync.Mutex{},
191+
}, nil
192+
}
193+
194+
// collectionsToChanges converts Collections to Changes.
195+
// This is the inverse of toStorableItems.
196+
func collectionsToChanges(collections []ldstoretypes.Collection) ([]Change, error) {
197+
var changes []Change
198+
199+
for _, coll := range collections {
200+
// Map from FDv1 DataKind to ObjectKind
201+
// Features -> FlagKind, Segments -> SegmentKind
202+
kindName := coll.Kind.GetName()
203+
var kind ObjectKind
204+
switch kindName {
205+
case "features":
206+
kind = FlagKind
207+
case "segments":
208+
kind = SegmentKind
209+
default:
210+
// Unknown kind, skip for forward compatibility
211+
continue
212+
}
213+
214+
for _, item := range coll.Items {
215+
if item.Item.Item != nil {
216+
// This is a Put operation
217+
jsonData, err := json.Marshal(item.Item.Item)
218+
if err != nil {
219+
return nil, err
220+
}
221+
changes = append(changes, Change{
222+
Action: ChangeTypePut,
223+
Kind: kind,
224+
Key: item.Key,
225+
Version: item.Item.Version,
226+
Object: jsonData,
227+
})
228+
} else {
229+
// This is a Delete operation (tombstone)
230+
changes = append(changes, Change{
231+
Action: ChangeTypeDelete,
232+
Kind: kind,
233+
Key: item.Key,
234+
Version: item.Item.Version,
235+
})
236+
}
237+
}
238+
}
239+
240+
return changes, nil
241+
}
242+
163243
// NoChanges represents an intent that the current data is up-to-date and doesn't
164244
// require changes.
165245
func (c *ChangeSetBuilder) NoChanges() *ChangeSet {

testhelpers/ldtestdatav2/test_data_source.go

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/launchdarkly/go-server-sdk/v7/interfaces"
1111
"github.com/launchdarkly/go-server-sdk/v7/internal"
1212
"github.com/launchdarkly/go-server-sdk/v7/subsystems"
13+
"github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoreimpl"
1314
"github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes"
1415
)
1516

@@ -205,33 +206,51 @@ func (t *TestDataSynchronizer) makeFullTransferChangeset() (*subsystems.ChangeSe
205206
version := t.version
206207
t.version++
207208

208-
builder := subsystems.NewChangeSetBuilder().
209-
Start(subsystems.ServerIntent{
210-
Payload: subsystems.Payload{
211-
ID: "",
212-
Target: version,
213-
Code: subsystems.IntentTransferFull,
214-
Reason: "payload-missing",
215-
},
216-
})
209+
intent := subsystems.ServerIntent{
210+
Payload: subsystems.Payload{
211+
ID: "",
212+
Target: version,
213+
Code: subsystems.IntentTransferFull,
214+
Reason: "payload-missing",
215+
},
216+
}
217217

218+
// Build collections directly from the current state
219+
flagItems := make([]ldstoretypes.KeyedItemDescriptor, 0, len(t.currentFlags))
218220
for key, item := range t.currentFlags {
219-
json, err := json.Marshal(item.Item)
220-
if err != nil {
221-
return nil, err
222-
}
223-
builder.AddPut(subsystems.FlagKind, key, item.Version, json)
221+
flagItems = append(flagItems, ldstoretypes.KeyedItemDescriptor{
222+
Key: key,
223+
Item: item,
224+
})
224225
}
225226

227+
segmentItems := make([]ldstoretypes.KeyedItemDescriptor, 0, len(t.currentSegments))
226228
for key, item := range t.currentSegments {
227-
json, err := json.Marshal(item.Item)
228-
if err != nil {
229-
return nil, err
230-
}
231-
builder.AddPut(subsystems.SegmentKind, key, item.Version, json)
229+
segmentItems = append(segmentItems, ldstoretypes.KeyedItemDescriptor{
230+
Key: key,
231+
Item: item,
232+
})
232233
}
233234

234-
return builder.Finish(subsystems.NewSelector(strconv.Itoa(version), version))
235+
collections := make([]ldstoretypes.Collection, 0, 2)
236+
if len(flagItems) > 0 {
237+
collections = append(collections, ldstoretypes.Collection{
238+
Kind: ldstoreimpl.Features(),
239+
Items: flagItems,
240+
})
241+
}
242+
if len(segmentItems) > 0 {
243+
collections = append(collections, ldstoretypes.Collection{
244+
Kind: ldstoreimpl.Segments(),
245+
Items: segmentItems,
246+
})
247+
}
248+
249+
return subsystems.NewChangeSetFromCollections(
250+
intent,
251+
subsystems.NewSelector(strconv.Itoa(version), version),
252+
collections,
253+
)
235254
}
236255

237256
func (t *TestDataSynchronizer) makeTransferChangesForObject(

0 commit comments

Comments
 (0)