Skip to content

Commit f2f0ef0

Browse files
authored
inmem: allow passing triggers (AST) data without conversion (#7959)
Fixes #7958 Signed-off-by: Anders Eknert <[email protected]>
1 parent 3188e04 commit f2f0ef0

File tree

4 files changed

+146
-15
lines changed

4 files changed

+146
-15
lines changed

v1/storage/inmem/inmem.go

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -349,36 +349,47 @@ func (h *handle) Unregister(_ context.Context, txn storage.Transaction) {
349349
}
350350

351351
func (db *store) runOnCommitTriggers(ctx context.Context, txn storage.Transaction, event storage.TriggerEvent) {
352-
if db.returnASTValuesOnRead && len(db.triggers) > 0 {
353-
// FIXME: Not very performant for large data.
352+
// While it's unlikely, the API allows one trigger to be configured to want
353+
// data conversion, and another that doesn't. So let's handle that properly.
354+
var wantsDataConversion bool
355+
if db.returnASTValuesOnRead && len(event.Data) > 0 {
356+
for _, t := range db.triggers {
357+
if !t.SkipDataConversion {
358+
wantsDataConversion = true
359+
break
360+
}
361+
}
362+
}
354363

355-
dataEvents := make([]storage.DataEvent, 0, len(event.Data))
364+
var converted storage.TriggerEvent
365+
if wantsDataConversion {
366+
converted = storage.TriggerEvent{
367+
Policy: event.Policy,
368+
Data: make([]storage.DataEvent, 0, len(event.Data)),
369+
Context: event.Context,
370+
}
356371

357372
for _, dataEvent := range event.Data {
358373
if astData, ok := dataEvent.Data.(ast.Value); ok {
359374
jsn, err := ast.ValueToInterface(astData, illegalResolver{})
360375
if err != nil {
361376
panic(err)
362377
}
363-
dataEvents = append(dataEvents, storage.DataEvent{
378+
converted.Data = append(converted.Data, storage.DataEvent{
364379
Path: dataEvent.Path,
365380
Data: jsn,
366381
Removed: dataEvent.Removed,
367382
})
368-
} else {
369-
dataEvents = append(dataEvents, dataEvent)
370383
}
371384
}
372-
373-
event = storage.TriggerEvent{
374-
Policy: event.Policy,
375-
Data: dataEvents,
376-
Context: event.Context,
377-
}
378385
}
379386

380387
for _, t := range db.triggers {
381-
t.OnCommit(ctx, txn, event)
388+
if wantsDataConversion && !t.SkipDataConversion {
389+
t.OnCommit(ctx, txn, converted)
390+
} else {
391+
t.OnCommit(ctx, txn, event)
392+
}
382393
}
383394
}
384395

v1/storage/inmem/inmem_bench_test.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,8 @@ func BenchmarkWriteAndCommit(b *testing.B) {
211211
AllStores(map[string]any{}).Bench(b, operation)
212212
}
213213

214-
// Go 48750 ns/op 27040 B/op 311 allocs/op (no additional cost of triggers)
215-
// AST 191501 ns/op 37585 B/op 616 allocs/op (extra cost du to converting back to Go values.. why?)
214+
// Go 48399 ns/op 16209 B/op 304 allocs/op (no additional cost of triggers)
215+
// AST 191501 ns/op 26673 B/op 605 allocs/op (extra cost due to converting back to Go values)
216216
func BenchmarkWriteAndCommitWithTriggers(b *testing.B) {
217217
paths := make([]storage.Path, 100)
218218
for i := range 100 {
@@ -239,6 +239,62 @@ func BenchmarkWriteAndCommitWithTriggers(b *testing.B) {
239239
Bench(b, operation)
240240
}
241241

242+
// AST 189485 ns/op 17011 B/op 304 allocs/op
243+
func BenchmarkWriteAndCommitWithTriggersSkipConversion(b *testing.B) {
244+
paths := make([]storage.Path, 100)
245+
for i := range 100 {
246+
paths[i] = storage.Path{strconv.Itoa(i)}
247+
}
248+
values := make([]ast.Value, 100)
249+
for i := range 100 {
250+
values[i] = ast.String(paths[i][0])
251+
}
252+
253+
operation := func(ctx context.Context, target *target) error {
254+
txn, _ := target.store.NewTransaction(b.Context(), storage.WriteParams)
255+
for i := range 100 {
256+
if err := target.store.Write(b.Context(), txn, storage.AddOp, paths[i], values[i]); err != nil {
257+
return err
258+
}
259+
}
260+
261+
return target.store.Commit(b.Context(), txn)
262+
}
263+
264+
triggerCount := 0
265+
266+
trigger := storage.TriggerConfig{
267+
SkipDataConversion: true,
268+
OnCommit: func(ctx context.Context, txn storage.Transaction, event storage.TriggerEvent) {
269+
if event.DataChanged() {
270+
if len(event.Data) != 100 {
271+
b.Fatalf("Expected 100 data changes but got: %d", len(event.Data))
272+
}
273+
if _, ok := event.Data[0].Data.(ast.Value); !ok {
274+
b.Fatalf("Expected ast.Value data but got: %T", event.Data[0].Data)
275+
}
276+
triggerCount++
277+
}
278+
},
279+
}
280+
281+
onlyAstStores := targets{{
282+
name: "AST",
283+
store: inmem.NewFromObjectWithOpts(map[string]any{}, inmem.OptReturnASTValuesOnRead(true)),
284+
isAST: true,
285+
}}
286+
287+
onlyAstStores.
288+
SetupWithTxn(b, writeTxn, func(ctx context.Context, target *target) error {
289+
return onlyError(target.store.Register(b.Context(), target.txn, trigger))
290+
}).
291+
Bench(b, operation)
292+
293+
if triggerCount == 0 {
294+
b.Fatalf("Expected trigger to be called at least once")
295+
}
296+
}
297+
242298
func (t targets) VerifyRead(b *testing.B, path storage.Path, expected any) targets {
243299
b.Helper()
244300
for _, target := range t {

v1/storage/inmem/inmem_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,66 @@ func TestInMemoryTriggers(t *testing.T) {
11501150
}
11511151
}
11521152

1153+
func TestASTInMemoryTriggersDataConversion(t *testing.T) {
1154+
ctx := t.Context()
1155+
store := NewFromObjectWithOpts(loadSmallTestData(), OptReturnASTValuesOnRead(true))
1156+
1157+
noConversionTriggerCount := 0
1158+
conversionTriggerCount := 0
1159+
1160+
// Register a trigger that doesn't want data conversion
1161+
err := storage.Txn(t.Context(), store, storage.WriteParams, func(txn storage.Transaction) error {
1162+
_, err := store.Register(ctx, txn, storage.TriggerConfig{
1163+
SkipDataConversion: true,
1164+
OnCommit: func(ctx context.Context, txn storage.Transaction, event storage.TriggerEvent) {
1165+
if event.DataChanged() {
1166+
if _, ok := event.Data[0].Data.(ast.Value); !ok {
1167+
t.Fatalf("Expected ast.Value data but got: %T", event.Data[0].Data)
1168+
}
1169+
noConversionTriggerCount++
1170+
}
1171+
},
1172+
})
1173+
return err
1174+
})
1175+
if err != nil {
1176+
t.Fatalf("Failed to register trigger: %v", err)
1177+
}
1178+
1179+
// Register a trigger that wants data conversion (skip data conversion not set, as is the default)
1180+
err = storage.Txn(t.Context(), store, storage.WriteParams, func(txn storage.Transaction) error {
1181+
_, err := store.Register(ctx, txn, storage.TriggerConfig{
1182+
OnCommit: func(ctx context.Context, txn storage.Transaction, event storage.TriggerEvent) {
1183+
if event.DataChanged() {
1184+
if _, ok := event.Data[0].Data.(ast.Value); ok {
1185+
t.Fatalf("Expected non-ast.Value data but got: %T", event.Data[0].Data)
1186+
}
1187+
conversionTriggerCount++
1188+
}
1189+
},
1190+
})
1191+
return err
1192+
})
1193+
if err != nil {
1194+
t.Fatalf("Failed to register trigger: %v", err)
1195+
}
1196+
1197+
err = storage.Txn(t.Context(), store, storage.WriteParams, func(txn storage.Transaction) error {
1198+
return store.Write(ctx, txn, storage.ReplaceOp, storage.MustParsePath("/a"), "hello")
1199+
})
1200+
if err != nil {
1201+
t.Fatalf("Failed to write data: %v", err)
1202+
}
1203+
1204+
if noConversionTriggerCount != 1 {
1205+
t.Fatalf("Expected no conversion trigger to be called once but got: %d", noConversionTriggerCount)
1206+
}
1207+
1208+
if conversionTriggerCount != 1 {
1209+
t.Fatalf("Expected conversion trigger to be called once but got: %d", conversionTriggerCount)
1210+
}
1211+
}
1212+
11531213
func TestInMemoryTriggersUnregister(t *testing.T) {
11541214
ctx := t.Context()
11551215
store := NewFromObject(loadSmallTestData())

v1/storage/interface.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ func (e TriggerEvent) DataChanged() bool {
210210

211211
// TriggerConfig contains the trigger registration configuration.
212212
type TriggerConfig struct {
213+
// SkipDataConversion when set to true, avoids converting data passed to
214+
// trigger functions from the store to Go types, and instead passes the
215+
// original representation (e.g., ast.Value).
216+
SkipDataConversion bool
213217

214218
// OnCommit is invoked when a transaction is successfully committed. The
215219
// callback is invoked with a handle to the write transaction that

0 commit comments

Comments
 (0)