Skip to content

Commit 4eae4fd

Browse files
committed
fix: dont check idempotency keys for dag steps
fix: tests + cleanup fix: naming fix: naming
1 parent 1d44d9f commit 4eae4fd

3 files changed

Lines changed: 57 additions & 91 deletions

File tree

‎pkg/repository/durable_events.go‎

Lines changed: 34 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -683,29 +683,25 @@ func nonDeterminismDetail(opts IngestDurableTaskEventOpts, expectedKind sqlcv1.V
683683
}
684684

685685
type GetOrCreateLogEntryOpt struct {
686-
Kind sqlcv1.V1DurableEventLogKind
687-
IdempotencyKey []byte
688-
LegacyIdempotencyKey []byte
689-
InputPayload []byte
690-
ResultPayload []byte
691-
NodeId int64
692-
BranchId int64
693-
IsSatisfied bool
694-
UserMessage *string
695-
WaitData string // JSON-encoded WaitData, empty string means no wait data
696-
ChildTaskExternalId uuid.UUID
697-
ShouldSkip bool
686+
Kind sqlcv1.V1DurableEventLogKind
687+
IdempotencyKey []byte
688+
InputPayload []byte
689+
ResultPayload []byte
690+
NodeId int64
691+
BranchId int64
692+
IsSatisfied bool
693+
UserMessage *string
694+
WaitData string // JSON-encoded WaitData, empty string means no wait data
695+
IsDagStepTrigger bool
696+
ChildTaskExternalId uuid.UUID
697+
ShouldSkip bool
698698
// IsTriggered creates the entry with triggered_at already set: the child's task rows exist
699699
// (an in-place replay), so there is no run left for the trigger path to spawn.
700700
IsTriggered bool
701701
}
702702

703-
func (o GetOrCreateLogEntryOpt) idempotencyKeyMatches(existingKey []byte) bool {
704-
if bytes.Equal(o.IdempotencyKey, existingKey) {
705-
return true
706-
}
707-
708-
return len(o.LegacyIdempotencyKey) > 0 && bytes.Equal(o.LegacyIdempotencyKey, existingKey)
703+
func (o GetOrCreateLogEntryOpt) matchesExistingEntry(existing *sqlcv1.BulkGetDurableEventLogEntriesRow) bool {
704+
return o.IsDagStepTrigger || bytes.Equal(o.IdempotencyKey, existing.IdempotencyKey)
709705
}
710706

711707
type GetOrCreateLogEntryOpts struct {
@@ -822,26 +818,14 @@ func satisfiedOrderPtr(v pgtype.Int8) *int64 {
822818
}
823819

824820
func (r *durableEventsRepository) createIdempotencyKey(kind sqlcv1.V1DurableEventLogKind, triggerOpts *WorkflowNameTriggerOpts, waitForConditions []CreateExternalSignalConditionOpt) ([]byte, error) {
825-
return hashIdempotencyKey(kind, triggerOpts, waitForConditions, true)
826-
}
827-
828-
func (r *durableEventsRepository) createLegacyDagStepIdempotencyKey(triggerOpts *WorkflowNameTriggerOpts) ([]byte, error) {
829-
if !triggerOpts.IsDagStepTrigger {
830-
return nil, nil
831-
}
832-
833-
return hashIdempotencyKey(sqlcv1.V1DurableEventLogKindRUN, triggerOpts, nil, false)
834-
}
835-
836-
func hashIdempotencyKey(kind sqlcv1.V1DurableEventLogKind, triggerOpts *WorkflowNameTriggerOpts, waitForConditions []CreateExternalSignalConditionOpt, includeDagStepIdentity bool) ([]byte, error) {
837821
// note: can't use additional metadata here because it's not stable, since we store trace information in it w/ the otel instrumentors
838822
dataToHash := []byte(kind)
839823

840824
if triggerOpts != nil {
841825
dataToHash = append(dataToHash, triggerOpts.Data...)
842826
dataToHash = append(dataToHash, []byte(triggerOpts.WorkflowName)...)
843827

844-
if includeDagStepIdentity && triggerOpts.IsDagStepTrigger {
828+
if triggerOpts.IsDagStepTrigger {
845829
if triggerOpts.TargetActionId != nil {
846830
dataToHash = append(dataToHash, []byte(*triggerOpts.TargetActionId)...)
847831
}
@@ -1129,7 +1113,7 @@ func (r *durableEventsRepository) getOrCreateEventLogEntriesForTasks(
11291113
state.newEntryByKey[key] = o
11301114
continue
11311115
}
1132-
if !o.idempotencyKeyMatches(e.IdempotencyKey) {
1116+
if !o.matchesExistingEntry(e) {
11331117
nonDeterminismErr = &NonDeterminismError{
11341118
BranchId: o.BranchId,
11351119
NodeId: o.NodeId,
@@ -1288,7 +1272,7 @@ func (r *durableEventsRepository) getOrCreateEventLogEntriesForTasks(
12881272
return nil, nil, nil, fmt.Errorf("expected to find log entry for skipped child task external id %s", o.ChildTaskExternalId)
12891273
}
12901274

1291-
if len(o.IdempotencyKey) > 0 && !o.idempotencyKeyMatches(e.IdempotencyKey) {
1275+
if len(o.IdempotencyKey) > 0 && !o.matchesExistingEntry(e) {
12921276
return nil, nil, nil, &NonDeterminismError{
12931277
BranchId: e.BranchID,
12941278
NodeId: e.NodeID,
@@ -2112,31 +2096,25 @@ func (r *durableEventsRepository) planDurableEventLogAppend(
21122096
// only index-based dedupe is validated against the existing entry's
21132097
// idempotency key: an explicit child_key intentionally reuses the
21142098
// cached child even when the inputs differ
2115-
var idempotencyKey, legacyIdempotencyKey []byte
2099+
var idempotencyKey []byte
21162100
if triggerOpts.ChildKey == nil {
21172101
key, keyErr := r.createIdempotencyKey(sqlcv1.V1DurableEventLogKindRUN, triggerOpts, nil)
21182102
if keyErr != nil {
21192103
return nil, fmt.Errorf("failed to create idempotency key: %w", keyErr), nil
21202104
}
21212105
idempotencyKey = key
2122-
2123-
legacyKey, legacyKeyErr := r.createLegacyDagStepIdempotencyKey(triggerOpts)
2124-
if legacyKeyErr != nil {
2125-
return nil, fmt.Errorf("failed to create legacy idempotency key: %w", legacyKeyErr), nil
2126-
}
2127-
legacyIdempotencyKey = legacyKey
21282106
}
21292107

21302108
if _, exists := childExternalIdToTriggerOpts[triggerOpts.ExternalId]; !exists {
21312109
childExternalIdToTriggerOpts[triggerOpts.ExternalId] = triggerOpts
21322110
}
21332111

21342112
innerOpts[i] = GetOrCreateLogEntryOpt{
2135-
Kind: sqlcv1.V1DurableEventLogKindRUN,
2136-
ChildTaskExternalId: triggerOpts.ExternalId,
2137-
IdempotencyKey: idempotencyKey,
2138-
LegacyIdempotencyKey: legacyIdempotencyKey,
2139-
ShouldSkip: true,
2113+
Kind: sqlcv1.V1DurableEventLogKindRUN,
2114+
ChildTaskExternalId: triggerOpts.ExternalId,
2115+
IdempotencyKey: idempotencyKey,
2116+
IsDagStepTrigger: triggerOpts.IsDagStepTrigger,
2117+
ShouldSkip: true,
21402118
}
21412119
continue
21422120
}
@@ -2159,29 +2137,24 @@ func (r *durableEventsRepository) planDurableEventLogAppend(
21592137
return nil, fmt.Errorf("failed to create idempotency key: %w", keyErr), nil
21602138
}
21612139

2162-
legacyIdempotencyKey, legacyKeyErr := r.createLegacyDagStepIdempotencyKey(triggerOpts)
2163-
if legacyKeyErr != nil {
2164-
return nil, fmt.Errorf("failed to create legacy idempotency key: %w", legacyKeyErr), nil
2165-
}
2166-
21672140
// A child that is being cancelled or skipped never runs, so no completion event
21682141
// will arrive for it (trigger.go creates it directly in a terminal state,
21692142
// bypassing the match pipeline): its entry is terminal at creation regardless of
21702143
// whether this is a first run or a replay.
21712144
isSatisfied := triggerOpts.IsCancelled || triggerOpts.IsSkipped
21722145

21732146
innerOpts[i] = GetOrCreateLogEntryOpt{
2174-
Kind: sqlcv1.V1DurableEventLogKindRUN,
2175-
NodeId: nodeId,
2176-
BranchId: branchId,
2177-
ChildTaskExternalId: triggerOpts.ExternalId,
2178-
IdempotencyKey: idempotencyKey,
2179-
LegacyIdempotencyKey: legacyIdempotencyKey,
2180-
InputPayload: inputPayload,
2181-
WaitData: marshalWaitData(waitDataFromTriggerOpt(triggerOpts)),
2182-
UserMessage: triggerOpts.UserMessage,
2183-
IsSatisfied: isSatisfied,
2184-
IsTriggered: childrenToReplay[triggerOpts.ExternalId],
2147+
Kind: sqlcv1.V1DurableEventLogKindRUN,
2148+
NodeId: nodeId,
2149+
BranchId: branchId,
2150+
ChildTaskExternalId: triggerOpts.ExternalId,
2151+
IdempotencyKey: idempotencyKey,
2152+
InputPayload: inputPayload,
2153+
WaitData: marshalWaitData(waitDataFromTriggerOpt(triggerOpts)),
2154+
UserMessage: triggerOpts.UserMessage,
2155+
IsDagStepTrigger: triggerOpts.IsDagStepTrigger,
2156+
IsSatisfied: isSatisfied,
2157+
IsTriggered: childrenToReplay[triggerOpts.ExternalId],
21852158
}
21862159
}
21872160

‎pkg/repository/durable_events_dag_replay_test.go‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func ingestDagStepTrigger(
2525
) *IngestTriggerRunsEntry {
2626
t.Helper()
2727

28-
result, err := ingestDagStepTriggerResult(ctx, repo, tenantID, task, invocationCount, actionId, childIndex, false)
28+
result, err := ingestDagStepTriggerResult(ctx, repo, tenantID, task, invocationCount, actionId, childIndex, []byte(`{"x":1}`), false)
2929
require.NoError(t, err)
3030
require.NotNil(t, result.TriggerRunsResult)
3131
require.Len(t, result.TriggerRunsResult.Entries, 1)
@@ -41,6 +41,7 @@ func ingestDagStepTriggerResult(
4141
invocationCount int32,
4242
actionId string,
4343
childIndex int64,
44+
input []byte,
4445
isSkipped bool,
4546
) (*IngestDurableTaskEventResult, error) {
4647
parentExternalId := task.ExternalID
@@ -61,7 +62,7 @@ func ingestDagStepTriggerResult(
6162
TriggerTaskData: &TriggerTaskData{
6263
IsSkipped: isSkipped,
6364
WorkflowName: "my-dag",
64-
Data: []byte(`{"x":1}`),
65+
Data: input,
6566
TargetActionId: &actionId,
6667
ParentExternalId: &parentExternalId,
6768
ParentTaskId: &parentTaskId,
@@ -84,7 +85,7 @@ func reinvokeDurableTask(t *testing.T, ctx context.Context, repos userEventScope
8485
require.NoError(t, err)
8586
}
8687

87-
func TestDagStepReplayAtAnotherStepsNodeIsNondeterministic(t *testing.T) {
88+
func TestDagStepReplayReusesTheEntryEvenWhenTheInputBytesDiffer(t *testing.T) {
8889
pool, cleanup := setupPostgresWithMigration(t)
8990
defer cleanup()
9091

@@ -93,17 +94,20 @@ func TestDagStepReplayAtAnotherStepsNodeIsNondeterministic(t *testing.T) {
9394
repos := newUserEventScopeTestRepositories(t, pool)
9495
task := createUserEventScopeTestTask(t, ctx, repos, tenantID, 201)
9596

96-
dedupe := ingestDagStepTrigger(t, ctx, repos.durable, tenantID, task, 1, "my-dag:dedupe-action-items", 13)
97-
require.False(t, dedupe.AlreadyExisted)
98-
require.EqualValues(t, 1, dedupe.NodeId)
97+
first := ingestDagStepTrigger(t, ctx, repos.durable, tenantID, task, 1, "my-dag:step-a", 1)
98+
require.False(t, first.AlreadyExisted)
99+
require.EqualValues(t, 1, first.NodeId)
99100

100101
reinvokeDurableTask(t, ctx, repos, tenantID, task)
101102

102-
_, err := ingestDagStepTriggerResult(ctx, repos.durable, tenantID, task, 2, "my-dag:bold-dates-and-quantities", 9, false)
103+
result, err := ingestDagStepTriggerResult(ctx, repos.durable, tenantID, task, 2, "my-dag:step-a", 1, []byte(`{"x": 1}`), false)
104+
require.NoError(t, err)
105+
require.Len(t, result.TriggerRunsResult.Entries, 1)
103106

104-
var nonDeterminismErr *NonDeterminismError
105-
require.ErrorAs(t, err, &nonDeterminismErr)
106-
require.EqualValues(t, 1, nonDeterminismErr.NodeId)
107+
replayed := result.TriggerRunsResult.Entries[0]
108+
require.True(t, replayed.AlreadyExisted)
109+
require.EqualValues(t, 1, replayed.NodeId)
110+
require.Equal(t, first.WorkflowRunExternalId, replayed.WorkflowRunExternalId)
107111
}
108112

109113
func TestDagStepEntriesOfSameWorkflowHaveDistinctIdempotencyKeys(t *testing.T) {
@@ -141,7 +145,7 @@ func TestDagStepCreatedSkippedTakesNextSatisfiedOrder(t *testing.T) {
141145
require.False(t, running.IsSatisfied)
142146
require.Nil(t, running.SatisfiedOrder)
143147

144-
result, err := ingestDagStepTriggerResult(ctx, repos.durable, tenantID, task, 1, "my-dag:step-b", 2, true)
148+
result, err := ingestDagStepTriggerResult(ctx, repos.durable, tenantID, task, 1, "my-dag:step-b", 2, []byte(`{"x":1}`), true)
145149
require.NoError(t, err)
146150

147151
skipped := result.TriggerRunsResult.Entries[0]

‎pkg/repository/durable_events_test.go‎

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -307,28 +307,17 @@ func TestCreateIdempotencyKey_DagStepsOfSameWorkflowDiffer(t *testing.T) {
307307
assert.NotEqual(t, stepA, stepB)
308308
}
309309

310-
func TestIdempotencyKeyMatches_AcceptsLegacyDagStepKey(t *testing.T) {
311-
r := &durableEventsRepository{}
312-
triggerOpts := dagStepTriggerOpts("my-dag:step-a", 9)
313-
314-
current, err := r.createIdempotencyKey(sqlcv1.V1DurableEventLogKindRUN, triggerOpts, nil)
315-
assert.NoError(t, err)
316-
317-
legacy, err := r.createLegacyDagStepIdempotencyKey(triggerOpts)
318-
assert.NoError(t, err)
319-
320-
preUpgrade := keyFromKind(t, sqlcv1.V1DurableEventLogKindRUN, &WorkflowNameTriggerOpts{
321-
TriggerTaskData: &TriggerTaskData{WorkflowName: "my-dag", Data: []byte(`{"x":1}`)},
322-
}, nil)
310+
func TestMatchesExistingEntry_DagStepIsNeverCheckedForNonDeterminism(t *testing.T) {
311+
opt := GetOrCreateLogEntryOpt{IdempotencyKey: []byte("key-hashed-from-compact-input"), IsDagStepTrigger: true}
323312

324-
assert.Equal(t, preUpgrade, string(legacy))
325-
assert.NotEqual(t, string(current), string(legacy))
313+
assert.True(t, opt.matchesExistingEntry(&sqlcv1.BulkGetDurableEventLogEntriesRow{IdempotencyKey: []byte("key-hashed-from-spaced-input")}))
314+
}
326315

327-
opt := GetOrCreateLogEntryOpt{IdempotencyKey: current, LegacyIdempotencyKey: legacy}
316+
func TestMatchesExistingEntry_ChildRunMatchesByKey(t *testing.T) {
317+
opt := GetOrCreateLogEntryOpt{IdempotencyKey: []byte("key")}
328318

329-
assert.True(t, opt.idempotencyKeyMatches(current))
330-
assert.True(t, opt.idempotencyKeyMatches(legacy))
331-
assert.False(t, opt.idempotencyKeyMatches([]byte(keyFromKind(t, sqlcv1.V1DurableEventLogKindRUN, dagStepTriggerOpts("my-dag:step-b", 13), nil))))
319+
assert.True(t, opt.matchesExistingEntry(&sqlcv1.BulkGetDurableEventLogEntriesRow{IdempotencyKey: []byte("key")}))
320+
assert.False(t, opt.matchesExistingEntry(&sqlcv1.BulkGetDurableEventLogEntriesRow{IdempotencyKey: []byte("other key")}))
332321
}
333322

334323
func int32Ptr(i int32) *int32 { return &i }

0 commit comments

Comments
 (0)