Skip to content

Commit 26bf7cf

Browse files
committed
fix: dont check idempotency keys for dag steps
1 parent 1d44d9f commit 26bf7cf

2 files changed

Lines changed: 45 additions & 80 deletions

File tree

‎pkg/repository/durable_events.go‎

Lines changed: 37 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -683,29 +683,28 @@ 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 skips the idempotency check against an existing entry: the engine orchestrates the DAG
696+
// itself from a pinned workflow version, so its replays are deterministic by construction, whereas the key
697+
// hashes the run's input bytes, which change with the engine version that serialised them.
698+
IsDagStepTrigger bool
699+
ChildTaskExternalId uuid.UUID
700+
ShouldSkip bool
698701
// IsTriggered creates the entry with triggered_at already set: the child's task rows exist
699702
// (an in-place replay), so there is no run left for the trigger path to spawn.
700703
IsTriggered bool
701704
}
702705

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)
706+
func (o GetOrCreateLogEntryOpt) matchesExistingEntry(existing *sqlcv1.BulkGetDurableEventLogEntriesRow) bool {
707+
return o.IsDagStepTrigger || bytes.Equal(o.IdempotencyKey, existing.IdempotencyKey)
709708
}
710709

711710
type GetOrCreateLogEntryOpts struct {
@@ -822,26 +821,14 @@ func satisfiedOrderPtr(v pgtype.Int8) *int64 {
822821
}
823822

824823
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) {
837824
// note: can't use additional metadata here because it's not stable, since we store trace information in it w/ the otel instrumentors
838825
dataToHash := []byte(kind)
839826

840827
if triggerOpts != nil {
841828
dataToHash = append(dataToHash, triggerOpts.Data...)
842829
dataToHash = append(dataToHash, []byte(triggerOpts.WorkflowName)...)
843830

844-
if includeDagStepIdentity && triggerOpts.IsDagStepTrigger {
831+
if triggerOpts.IsDagStepTrigger {
845832
if triggerOpts.TargetActionId != nil {
846833
dataToHash = append(dataToHash, []byte(*triggerOpts.TargetActionId)...)
847834
}
@@ -1129,7 +1116,7 @@ func (r *durableEventsRepository) getOrCreateEventLogEntriesForTasks(
11291116
state.newEntryByKey[key] = o
11301117
continue
11311118
}
1132-
if !o.idempotencyKeyMatches(e.IdempotencyKey) {
1119+
if !o.matchesExistingEntry(e) {
11331120
nonDeterminismErr = &NonDeterminismError{
11341121
BranchId: o.BranchId,
11351122
NodeId: o.NodeId,
@@ -1288,7 +1275,7 @@ func (r *durableEventsRepository) getOrCreateEventLogEntriesForTasks(
12881275
return nil, nil, nil, fmt.Errorf("expected to find log entry for skipped child task external id %s", o.ChildTaskExternalId)
12891276
}
12901277

1291-
if len(o.IdempotencyKey) > 0 && !o.idempotencyKeyMatches(e.IdempotencyKey) {
1278+
if len(o.IdempotencyKey) > 0 && !o.matchesExistingEntry(e) {
12921279
return nil, nil, nil, &NonDeterminismError{
12931280
BranchId: e.BranchID,
12941281
NodeId: e.NodeID,
@@ -2112,31 +2099,25 @@ func (r *durableEventsRepository) planDurableEventLogAppend(
21122099
// only index-based dedupe is validated against the existing entry's
21132100
// idempotency key: an explicit child_key intentionally reuses the
21142101
// cached child even when the inputs differ
2115-
var idempotencyKey, legacyIdempotencyKey []byte
2102+
var idempotencyKey []byte
21162103
if triggerOpts.ChildKey == nil {
21172104
key, keyErr := r.createIdempotencyKey(sqlcv1.V1DurableEventLogKindRUN, triggerOpts, nil)
21182105
if keyErr != nil {
21192106
return nil, fmt.Errorf("failed to create idempotency key: %w", keyErr), nil
21202107
}
21212108
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
21282109
}
21292110

21302111
if _, exists := childExternalIdToTriggerOpts[triggerOpts.ExternalId]; !exists {
21312112
childExternalIdToTriggerOpts[triggerOpts.ExternalId] = triggerOpts
21322113
}
21332114

21342115
innerOpts[i] = GetOrCreateLogEntryOpt{
2135-
Kind: sqlcv1.V1DurableEventLogKindRUN,
2136-
ChildTaskExternalId: triggerOpts.ExternalId,
2137-
IdempotencyKey: idempotencyKey,
2138-
LegacyIdempotencyKey: legacyIdempotencyKey,
2139-
ShouldSkip: true,
2116+
Kind: sqlcv1.V1DurableEventLogKindRUN,
2117+
ChildTaskExternalId: triggerOpts.ExternalId,
2118+
IdempotencyKey: idempotencyKey,
2119+
IsDagStepTrigger: triggerOpts.IsDagStepTrigger,
2120+
ShouldSkip: true,
21402121
}
21412122
continue
21422123
}
@@ -2159,29 +2140,24 @@ func (r *durableEventsRepository) planDurableEventLogAppend(
21592140
return nil, fmt.Errorf("failed to create idempotency key: %w", keyErr), nil
21602141
}
21612142

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-
21672143
// A child that is being cancelled or skipped never runs, so no completion event
21682144
// will arrive for it (trigger.go creates it directly in a terminal state,
21692145
// bypassing the match pipeline): its entry is terminal at creation regardless of
21702146
// whether this is a first run or a replay.
21712147
isSatisfied := triggerOpts.IsCancelled || triggerOpts.IsSkipped
21722148

21732149
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],
2150+
Kind: sqlcv1.V1DurableEventLogKindRUN,
2151+
NodeId: nodeId,
2152+
BranchId: branchId,
2153+
ChildTaskExternalId: triggerOpts.ExternalId,
2154+
IdempotencyKey: idempotencyKey,
2155+
InputPayload: inputPayload,
2156+
WaitData: marshalWaitData(waitDataFromTriggerOpt(triggerOpts)),
2157+
UserMessage: triggerOpts.UserMessage,
2158+
IsDagStepTrigger: triggerOpts.IsDagStepTrigger,
2159+
IsSatisfied: isSatisfied,
2160+
IsTriggered: childrenToReplay[triggerOpts.ExternalId],
21852161
}
21862162
}
21872163

‎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)