From 4999eb8f048c4631e836f655529d3ee7dc1e5e30 Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:59:27 -0500 Subject: [PATCH 1/7] Bound generated scheduler request IDs --- chasm/lib/scheduler/internal/request_id.go | 40 ++++++++++++ .../lib/scheduler/internal/request_id_test.go | 63 +++++++++++++++++++ chasm/lib/scheduler/migration/migration.go | 7 ++- chasm/lib/scheduler/util.go | 4 +- common/schedules/id.go | 30 --------- common/schedules/id_test.go | 38 ----------- 6 files changed, 109 insertions(+), 73 deletions(-) create mode 100644 chasm/lib/scheduler/internal/request_id.go create mode 100644 chasm/lib/scheduler/internal/request_id_test.go diff --git a/chasm/lib/scheduler/internal/request_id.go b/chasm/lib/scheduler/internal/request_id.go new file mode 100644 index 00000000000..5f0eec335f1 --- /dev/null +++ b/chasm/lib/scheduler/internal/request_id.go @@ -0,0 +1,40 @@ +package internal + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "time" +) + +// GenerateRequestID generates a deterministic request ID for a buffered action's +// time. The request ID is deterministic because the jittered actual time (as +// well as the spec's nominal time) is, in turn, also deterministic. +// +// backfillID should be left blank for actions that are being started +// automatically, based on the schedule spec. It must be set for backfills, +// as backfills may generate buffered actions that overlap with both +// automatically-buffered actions, as well as other requested backfills. +func GenerateRequestID( + namespaceID string, + scheduleID string, + conflictToken int64, + backfillID string, + nominal time.Time, + actual time.Time, +) string { + if backfillID == "" { + backfillID = "auto" + } + scheduleIDHash := sha256.Sum256([]byte(scheduleID)) + + return fmt.Sprintf( + "sched-%s-%s-%s-%d-%d-%d", + backfillID, + namespaceID, + hex.EncodeToString(scheduleIDHash[:16]), + conflictToken, + nominal.UnixMilli(), + actual.UnixMilli(), + ) +} diff --git a/chasm/lib/scheduler/internal/request_id_test.go b/chasm/lib/scheduler/internal/request_id_test.go new file mode 100644 index 00000000000..c2c5a3536e5 --- /dev/null +++ b/chasm/lib/scheduler/internal/request_id_test.go @@ -0,0 +1,63 @@ +package internal + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestGenerateRequestID(t *testing.T) { + nominalTime := time.Now() + actualTime := time.Now() + scheduleIDHashSum := sha256.Sum256([]byte("mysched")) + scheduleIDHash := hex.EncodeToString(scheduleIDHashSum[:16]) + + actual := GenerateRequestID("nsid", "mysched", 10, "", nominalTime, actualTime) + expected := fmt.Sprintf( + "sched-auto-nsid-%s-10-%d-%d", + scheduleIDHash, + nominalTime.UnixMilli(), + actualTime.UnixMilli(), + ) + require.Equal(t, expected, actual) + + actual = GenerateRequestID("nsid", "mysched", 10, "backfillid", nominalTime, actualTime) + expected = fmt.Sprintf( + "sched-backfillid-nsid-%s-10-%d-%d", + scheduleIDHash, + nominalTime.UnixMilli(), + actualTime.UnixMilli(), + ) + require.Equal(t, expected, actual) +} + +func TestGenerateRequestIDHashesLongScheduleID(t *testing.T) { + nominalTime := time.UnixMilli(1_700_000_000_000) + actualTime := time.UnixMilli(1_700_000_000_001) + scheduleID := strings.Repeat("a", 1000) + scheduleIDHashSum := sha256.Sum256([]byte(scheduleID)) + scheduleIDHash := hex.EncodeToString(scheduleIDHashSum[:16]) + + for _, tc := range []struct { + name string + backfillID string + prefix string + }{ + {name: "automatic", prefix: "auto"}, + {name: "backfill", backfillID: "backfill-id", prefix: "backfill-id"}, + } { + t.Run(tc.name, func(t *testing.T) { + requestID := GenerateRequestID("nsid", scheduleID, 1, tc.backfillID, nominalTime, actualTime) + require.Contains(t, requestID, fmt.Sprintf("sched-%s-nsid-%s-1-", tc.prefix, scheduleIDHash)) + require.NotContains(t, requestID, scheduleID) + + otherRequestID := GenerateRequestID("nsid", scheduleID+"b", 1, tc.backfillID, nominalTime, actualTime) + require.NotEqual(t, requestID, otherRequestID) + }) + } +} diff --git a/chasm/lib/scheduler/migration/migration.go b/chasm/lib/scheduler/migration/migration.go index a7e42378fd7..be7a19d900c 100644 --- a/chasm/lib/scheduler/migration/migration.go +++ b/chasm/lib/scheduler/migration/migration.go @@ -10,6 +10,7 @@ import ( schedulepb "go.temporal.io/api/schedule/v1" schedulespb "go.temporal.io/server/api/schedule/v1" schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" + schedulerinternal "go.temporal.io/server/chasm/lib/scheduler/internal" "go.temporal.io/server/common" schedulescommon "go.temporal.io/server/common/schedules" "go.temporal.io/server/common/searchattribute/sadefs" @@ -216,7 +217,7 @@ func convertBufferedStartsLegacyToCHASM( v2Start := common.CloneProto(v1Start) if v2Start.RequestId == "" { - v2Start.RequestId = schedulescommon.GenerateRequestID( + v2Start.RequestId = schedulerinternal.GenerateRequestID( namespaceID, scheduleID, conflictToken, @@ -268,7 +269,7 @@ func convertRunningWorkflowsToBufferedStarts( // Include the RunId in the tag to ensure each running workflow // gets a unique RequestId (important for ALLOW_ALL overlap // policy where multiple workflows may be running concurrently). - RequestId: schedulescommon.GenerateRequestID( + RequestId: schedulerinternal.GenerateRequestID( namespaceID, scheduleID, conflictToken, @@ -332,7 +333,7 @@ func convertRecentActionsToBufferedStarts( StartTime: action.ActualTime, WorkflowId: action.StartWorkflowResult.WorkflowId, RunId: action.StartWorkflowResult.RunId, - RequestId: schedulescommon.GenerateRequestID( + RequestId: schedulerinternal.GenerateRequestID( namespaceID, scheduleID, conflictToken, diff --git a/chasm/lib/scheduler/util.go b/chasm/lib/scheduler/util.go index 450102d2752..9a8a8f88919 100644 --- a/chasm/lib/scheduler/util.go +++ b/chasm/lib/scheduler/util.go @@ -4,17 +4,17 @@ import ( "encoding/binary" "time" + schedulerinternal "go.temporal.io/server/chasm/lib/scheduler/internal" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" - schedulescommon "go.temporal.io/server/common/schedules" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) func generateRequestID(scheduler *Scheduler, backfillID string, nominal, actual time.Time) string { - return schedulescommon.GenerateRequestID( + return schedulerinternal.GenerateRequestID( scheduler.NamespaceId, scheduler.ScheduleId, scheduler.ConflictToken, diff --git a/common/schedules/id.go b/common/schedules/id.go index b461b440903..1e01d8b4218 100644 --- a/common/schedules/id.go +++ b/common/schedules/id.go @@ -7,36 +7,6 @@ import ( "github.com/google/uuid" ) -// GenerateRequestID generates a deterministic request ID for a buffered action's -// time. The request ID is deterministic because the jittered actual time (as -// well as the spec's nominal time) is, in turn, also deterministic. -// -// backfillID should be left blank for actions that are being started -// automatically, based on the schedule spec. It must be set for backfills, -// as backfills may generate buffered actions that overlap with both -// automatically-buffered actions, as well as other requested backfills. -func GenerateRequestID( - namespaceID string, - scheduleID string, - conflictToken int64, - backfillID string, - nominal time.Time, - actual time.Time, -) string { - if backfillID == "" { - backfillID = "auto" - } - return fmt.Sprintf( - "sched-%s-%s-%s-%d-%d-%d", - backfillID, - namespaceID, - scheduleID, - conflictToken, - nominal.UnixMilli(), - actual.UnixMilli(), - ) -} - // GenerateWorkflowID generates a deterministic workflow ID for a buffered // action by combining the base workflow ID with the truncated nominal time. func GenerateWorkflowID(baseWorkflowID string, nominalTime time.Time) string { diff --git a/common/schedules/id_test.go b/common/schedules/id_test.go index 7d486fdb664..d08ae905d44 100644 --- a/common/schedules/id_test.go +++ b/common/schedules/id_test.go @@ -1,7 +1,6 @@ package schedules import ( - "fmt" "testing" "time" @@ -15,40 +14,3 @@ func TestGenerateWorkflowID(t *testing.T) { actual := GenerateWorkflowID(baseWorkflowID, nominalTime) require.Equal(t, "my-workflow-2024-06-15T10:30:45Z", actual) } - -func TestGenerateRequestID(t *testing.T) { - nominalTime := time.Now() - actualTime := time.Now() - - // No backfill ID given. - actual := GenerateRequestID( - "nsid", - "mysched", - 10, - "", - nominalTime, - actualTime, - ) - expected := fmt.Sprintf( - "sched-auto-nsid-mysched-10-%d-%d", - nominalTime.UnixMilli(), - actualTime.UnixMilli(), - ) - require.Equal(t, expected, actual) - - // Backfill ID given. - actual = GenerateRequestID( - "nsid", - "mysched", - 10, - "backfillid", - nominalTime, - actualTime, - ) - expected = fmt.Sprintf( - "sched-backfillid-nsid-mysched-10-%d-%d", - nominalTime.UnixMilli(), - actualTime.UnixMilli(), - ) - require.Equal(t, expected, actual) -} From 28f28bc6c23777dc2a548c482fde75016be3a71a Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:28:42 -0500 Subject: [PATCH 2/7] Refine scheduler request IDs --- chasm/lib/scheduler/backfiller.go | 4 +- chasm/lib/scheduler/backfiller_tasks.go | 4 +- chasm/lib/scheduler/internal/request_id.go | 32 ++++++++++++- .../lib/scheduler/internal/request_id_test.go | 45 +++++++++++++++++-- chasm/lib/scheduler/migration/migration.go | 5 +-- chasm/lib/scheduler/spec_processor.go | 4 +- common/schedules/id.go | 21 --------- common/schedules/id_test.go | 16 ------- 8 files changed, 80 insertions(+), 51 deletions(-) delete mode 100644 common/schedules/id.go delete mode 100644 common/schedules/id_test.go diff --git a/chasm/lib/scheduler/backfiller.go b/chasm/lib/scheduler/backfiller.go index b2f18b0268d..4873df237ca 100644 --- a/chasm/lib/scheduler/backfiller.go +++ b/chasm/lib/scheduler/backfiller.go @@ -7,7 +7,7 @@ import ( schedulespb "go.temporal.io/server/api/schedule/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" - schedulescommon "go.temporal.io/server/common/schedules" + schedulerinternal "go.temporal.io/server/chasm/lib/scheduler/internal" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -36,7 +36,7 @@ func addBackfiller( ctx chasm.MutableContext, scheduler *Scheduler, ) *Backfiller { - id := schedulescommon.GenerateBackfillerID() + id := schedulerinternal.GenerateBackfillerID() backfiller := newBackfillerWithState(ctx, &schedulerpb.BackfillerState{ BackfillId: id, LastProcessedTime: timestamppb.New(ctx.Now(scheduler)), diff --git a/chasm/lib/scheduler/backfiller_tasks.go b/chasm/lib/scheduler/backfiller_tasks.go index 0800b585128..f4d11d76a00 100644 --- a/chasm/lib/scheduler/backfiller_tasks.go +++ b/chasm/lib/scheduler/backfiller_tasks.go @@ -7,10 +7,10 @@ import ( schedulespb "go.temporal.io/server/api/schedule/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" + schedulerinternal "go.temporal.io/server/chasm/lib/scheduler/internal" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" - schedulescommon "go.temporal.io/server/common/schedules" queueerrors "go.temporal.io/server/service/history/queues/errors" "go.uber.org/fx" "google.golang.org/protobuf/types/known/timestamppb" @@ -219,7 +219,7 @@ func (b *BackfillerTaskHandler) processTrigger( nowpb := backfiller.GetLastProcessedTime() now := nowpb.AsTime() requestID := generateRequestID(scheduler, backfiller.GetBackfillId(), now, now) - workflowID := schedulescommon.GenerateWorkflowID(scheduler.WorkflowID(), now) + workflowID := schedulerinternal.GenerateWorkflowID(scheduler.WorkflowID(), now) result.BufferedStarts = []*schedulespb.BufferedStart{ { NominalTime: nowpb, diff --git a/chasm/lib/scheduler/internal/request_id.go b/chasm/lib/scheduler/internal/request_id.go index 5f0eec335f1..2cec47863ea 100644 --- a/chasm/lib/scheduler/internal/request_id.go +++ b/chasm/lib/scheduler/internal/request_id.go @@ -5,8 +5,12 @@ import ( "encoding/hex" "fmt" "time" + + "github.com/google/uuid" ) +const lowestKnownSchemaRequestIDColumnLimit = 255 + // GenerateRequestID generates a deterministic request ID for a buffered action's // time. The request ID is deterministic because the jittered actual time (as // well as the spec's nominal time) is, in turn, also deterministic. @@ -26,8 +30,21 @@ func GenerateRequestID( if backfillID == "" { backfillID = "auto" } - scheduleIDHash := sha256.Sum256([]byte(scheduleID)) + requestID := fmt.Sprintf( + "sched-%s-%s-%s-%d-%d-%d", + backfillID, + namespaceID, + scheduleID, + conflictToken, + nominal.UnixMilli(), + actual.UnixMilli(), + ) + if len(requestID) <= lowestKnownSchemaRequestIDColumnLimit { + return requestID + } + + scheduleIDHash := sha256.Sum256([]byte(scheduleID)) return fmt.Sprintf( "sched-%s-%s-%s-%d-%d-%d", backfillID, @@ -38,3 +55,16 @@ func GenerateRequestID( actual.UnixMilli(), ) } + +// GenerateWorkflowID generates a deterministic workflow ID for a buffered +// action by combining the base workflow ID with the truncated nominal time. +func GenerateWorkflowID(baseWorkflowID string, nominalTime time.Time) string { + nominalTimeSec := nominalTime.Truncate(time.Second) + return fmt.Sprintf("%s-%s", baseWorkflowID, nominalTimeSec.UTC().Format(time.RFC3339)) +} + +// GenerateBackfillerID generates a unique ID for a Backfiller component. +// This ID is used to identify and deduplicate backfill requests. +func GenerateBackfillerID() string { + return uuid.NewString() +} diff --git a/chasm/lib/scheduler/internal/request_id_test.go b/chasm/lib/scheduler/internal/request_id_test.go index c2c5a3536e5..52e2881e82f 100644 --- a/chasm/lib/scheduler/internal/request_id_test.go +++ b/chasm/lib/scheduler/internal/request_id_test.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "math" "strings" "testing" "time" @@ -11,16 +12,22 @@ import ( "github.com/stretchr/testify/require" ) +func TestGenerateWorkflowID(t *testing.T) { + baseWorkflowID := "my-workflow" + nominalTime := time.Date(2024, 6, 15, 10, 30, 45, 123456789, time.UTC) + + actual := GenerateWorkflowID(baseWorkflowID, nominalTime) + require.Equal(t, "my-workflow-2024-06-15T10:30:45Z", actual) +} + func TestGenerateRequestID(t *testing.T) { nominalTime := time.Now() actualTime := time.Now() - scheduleIDHashSum := sha256.Sum256([]byte("mysched")) - scheduleIDHash := hex.EncodeToString(scheduleIDHashSum[:16]) actual := GenerateRequestID("nsid", "mysched", 10, "", nominalTime, actualTime) expected := fmt.Sprintf( "sched-auto-nsid-%s-10-%d-%d", - scheduleIDHash, + "mysched", nominalTime.UnixMilli(), actualTime.UnixMilli(), ) @@ -29,7 +36,7 @@ func TestGenerateRequestID(t *testing.T) { actual = GenerateRequestID("nsid", "mysched", 10, "backfillid", nominalTime, actualTime) expected = fmt.Sprintf( "sched-backfillid-nsid-%s-10-%d-%d", - scheduleIDHash, + "mysched", nominalTime.UnixMilli(), actualTime.UnixMilli(), ) @@ -55,9 +62,39 @@ func TestGenerateRequestIDHashesLongScheduleID(t *testing.T) { requestID := GenerateRequestID("nsid", scheduleID, 1, tc.backfillID, nominalTime, actualTime) require.Contains(t, requestID, fmt.Sprintf("sched-%s-nsid-%s-1-", tc.prefix, scheduleIDHash)) require.NotContains(t, requestID, scheduleID) + require.LessOrEqual(t, len(requestID), lowestKnownSchemaRequestIDColumnLimit) otherRequestID := GenerateRequestID("nsid", scheduleID+"b", 1, tc.backfillID, nominalTime, actualTime) require.NotEqual(t, requestID, otherRequestID) }) } } + +func TestGenerateRequestIDOnlyHashesWhenOverSQLColumnLimit(t *testing.T) { + nominalTime := time.UnixMilli(1_700_000_000_000) + actualTime := time.UnixMilli(1_700_000_000_001) + requestIDWithoutScheduleID := GenerateRequestID("nsid", "", 1, "", nominalTime, actualTime) + scheduleIDAtLimit := strings.Repeat("a", lowestKnownSchemaRequestIDColumnLimit-len(requestIDWithoutScheduleID)) + + requestID := GenerateRequestID("nsid", scheduleIDAtLimit, 1, "", nominalTime, actualTime) + require.Len(t, requestID, lowestKnownSchemaRequestIDColumnLimit) + require.Contains(t, requestID, scheduleIDAtLimit) + + overLimitScheduleID := scheduleIDAtLimit + "a" + requestID = GenerateRequestID("nsid", overLimitScheduleID, 1, "", nominalTime, actualTime) + require.LessOrEqual(t, len(requestID), lowestKnownSchemaRequestIDColumnLimit) + require.NotContains(t, requestID, overLimitScheduleID) +} + +func TestGenerateRequestIDFitsSQLColumn(t *testing.T) { + requestID := GenerateRequestID( + strings.Repeat("a", 36), + "schedule-id", + math.MinInt64, + strings.Repeat("b", 36), + time.UnixMilli(math.MinInt64), + time.UnixMilli(math.MaxInt64), + ) + + require.LessOrEqual(t, len(requestID), lowestKnownSchemaRequestIDColumnLimit) +} diff --git a/chasm/lib/scheduler/migration/migration.go b/chasm/lib/scheduler/migration/migration.go index be7a19d900c..2acba8cb52d 100644 --- a/chasm/lib/scheduler/migration/migration.go +++ b/chasm/lib/scheduler/migration/migration.go @@ -12,7 +12,6 @@ import ( schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" schedulerinternal "go.temporal.io/server/chasm/lib/scheduler/internal" "go.temporal.io/server/common" - schedulescommon "go.temporal.io/server/common/schedules" "go.temporal.io/server/common/searchattribute/sadefs" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -228,7 +227,7 @@ func convertBufferedStartsLegacyToCHASM( } if v2Start.WorkflowId == "" { - v2Start.WorkflowId = schedulescommon.GenerateWorkflowID( + v2Start.WorkflowId = schedulerinternal.GenerateWorkflowID( baseWorkflowID, v1Start.GetNominalTime().AsTime(), ) @@ -360,7 +359,7 @@ func convertBackfillsLegacyToCHASM( backfillers := make(map[string]*schedulerpb.BackfillerState, len(legacyBackfills)) for _, v1Backfill := range legacyBackfills { - backfillID := schedulescommon.GenerateBackfillerID() + backfillID := schedulerinternal.GenerateBackfillerID() backfillers[backfillID] = &schedulerpb.BackfillerState{ Request: &schedulerpb.BackfillerState_BackfillRequest{ diff --git a/chasm/lib/scheduler/spec_processor.go b/chasm/lib/scheduler/spec_processor.go index 40dfd0fecfc..ec584f49021 100644 --- a/chasm/lib/scheduler/spec_processor.go +++ b/chasm/lib/scheduler/spec_processor.go @@ -6,10 +6,10 @@ import ( enumspb "go.temporal.io/api/enums/v1" schedulespb "go.temporal.io/server/api/schedule/v1" + schedulerinternal "go.temporal.io/server/chasm/lib/scheduler/internal" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" - schedulescommon "go.temporal.io/server/common/schedules" legacyscheduler "go.temporal.io/server/service/worker/scheduler" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -179,7 +179,7 @@ func (s *SpecProcessorImpl) ProcessTimeRange( OverlapPolicy: overlapPolicy, Manual: manual, RequestId: generateRequestID(scheduler, backfillID, next.Nominal, next.Next), - WorkflowId: schedulescommon.GenerateWorkflowID(workflowID, next.Nominal), + WorkflowId: schedulerinternal.GenerateWorkflowID(workflowID, next.Nominal), }) if limit != nil { diff --git a/common/schedules/id.go b/common/schedules/id.go deleted file mode 100644 index 1e01d8b4218..00000000000 --- a/common/schedules/id.go +++ /dev/null @@ -1,21 +0,0 @@ -package schedules - -import ( - "fmt" - "time" - - "github.com/google/uuid" -) - -// GenerateWorkflowID generates a deterministic workflow ID for a buffered -// action by combining the base workflow ID with the truncated nominal time. -func GenerateWorkflowID(baseWorkflowID string, nominalTime time.Time) string { - nominalTimeSec := nominalTime.Truncate(time.Second) - return fmt.Sprintf("%s-%s", baseWorkflowID, nominalTimeSec.UTC().Format(time.RFC3339)) -} - -// GenerateBackfillerID generates a unique ID for a Backfiller component. -// This ID is used to identify and deduplicate backfill requests. -func GenerateBackfillerID() string { - return uuid.NewString() -} diff --git a/common/schedules/id_test.go b/common/schedules/id_test.go deleted file mode 100644 index d08ae905d44..00000000000 --- a/common/schedules/id_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package schedules - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestGenerateWorkflowID(t *testing.T) { - baseWorkflowID := "my-workflow" - nominalTime := time.Date(2024, 6, 15, 10, 30, 45, 123456789, time.UTC) - - actual := GenerateWorkflowID(baseWorkflowID, nominalTime) - require.Equal(t, "my-workflow-2024-06-15T10:30:45Z", actual) -} From ed5e5092545b9edffc4a2bd3103a85ef2c28ba2c Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:14:01 -0500 Subject: [PATCH 3/7] Use UUIDv5 for scheduler request IDs --- chasm/lib/scheduler/internal/request_id.go | 22 +++-------- .../lib/scheduler/internal/request_id_test.go | 37 ++++++------------- 2 files changed, 16 insertions(+), 43 deletions(-) diff --git a/chasm/lib/scheduler/internal/request_id.go b/chasm/lib/scheduler/internal/request_id.go index 2cec47863ea..ba57e855f8f 100644 --- a/chasm/lib/scheduler/internal/request_id.go +++ b/chasm/lib/scheduler/internal/request_id.go @@ -1,8 +1,6 @@ package internal import ( - "crypto/sha256" - "encoding/hex" "fmt" "time" @@ -14,6 +12,8 @@ const lowestKnownSchemaRequestIDColumnLimit = 255 // GenerateRequestID generates a deterministic request ID for a buffered action's // time. The request ID is deterministic because the jittered actual time (as // well as the spec's nominal time) is, in turn, also deterministic. +// Its total length must not exceed SQLite's request ID VARCHAR size, the +// smallest known persistence limit. // // backfillID should be left blank for actions that are being started // automatically, based on the schedule spec. It must be set for backfills, @@ -31,25 +31,13 @@ func GenerateRequestID( backfillID = "auto" } - requestID := fmt.Sprintf( - "sched-%s-%s-%s-%d-%d-%d", - backfillID, - namespaceID, - scheduleID, - conflictToken, - nominal.UnixMilli(), - actual.UnixMilli(), - ) - if len(requestID) <= lowestKnownSchemaRequestIDColumnLimit { - return requestID - } - - scheduleIDHash := sha256.Sum256([]byte(scheduleID)) + // Keep request IDs bounded and deterministic even when schedule IDs are long. + scheduleIDUUID := uuid.NewSHA1(uuid.NameSpaceOID, []byte(scheduleID)) return fmt.Sprintf( "sched-%s-%s-%s-%d-%d-%d", backfillID, namespaceID, - hex.EncodeToString(scheduleIDHash[:16]), + scheduleIDUUID, conflictToken, nominal.UnixMilli(), actual.UnixMilli(), diff --git a/chasm/lib/scheduler/internal/request_id_test.go b/chasm/lib/scheduler/internal/request_id_test.go index 52e2881e82f..f981a8fb6de 100644 --- a/chasm/lib/scheduler/internal/request_id_test.go +++ b/chasm/lib/scheduler/internal/request_id_test.go @@ -1,14 +1,13 @@ package internal import ( - "crypto/sha256" - "encoding/hex" "fmt" "math" "strings" "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" ) @@ -23,32 +22,34 @@ func TestGenerateWorkflowID(t *testing.T) { func TestGenerateRequestID(t *testing.T) { nominalTime := time.Now() actualTime := time.Now() + scheduleID := "mysched" + scheduleIDUUID := uuid.NewSHA1(uuid.NameSpaceOID, []byte(scheduleID)) - actual := GenerateRequestID("nsid", "mysched", 10, "", nominalTime, actualTime) + actual := GenerateRequestID("nsid", scheduleID, 10, "", nominalTime, actualTime) expected := fmt.Sprintf( "sched-auto-nsid-%s-10-%d-%d", - "mysched", + scheduleIDUUID, nominalTime.UnixMilli(), actualTime.UnixMilli(), ) require.Equal(t, expected, actual) - actual = GenerateRequestID("nsid", "mysched", 10, "backfillid", nominalTime, actualTime) + actual = GenerateRequestID("nsid", scheduleID, 10, "backfillid", nominalTime, actualTime) expected = fmt.Sprintf( "sched-backfillid-nsid-%s-10-%d-%d", - "mysched", + scheduleIDUUID, nominalTime.UnixMilli(), actualTime.UnixMilli(), ) require.Equal(t, expected, actual) } -func TestGenerateRequestIDHashesLongScheduleID(t *testing.T) { +func TestGenerateRequestIDUsesUUIDv5ForScheduleID(t *testing.T) { nominalTime := time.UnixMilli(1_700_000_000_000) actualTime := time.UnixMilli(1_700_000_000_001) scheduleID := strings.Repeat("a", 1000) - scheduleIDHashSum := sha256.Sum256([]byte(scheduleID)) - scheduleIDHash := hex.EncodeToString(scheduleIDHashSum[:16]) + scheduleIDUUID := uuid.NewSHA1(uuid.NameSpaceOID, []byte(scheduleID)) + require.Equal(t, uuid.Version(5), scheduleIDUUID.Version()) for _, tc := range []struct { name string @@ -60,7 +61,7 @@ func TestGenerateRequestIDHashesLongScheduleID(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { requestID := GenerateRequestID("nsid", scheduleID, 1, tc.backfillID, nominalTime, actualTime) - require.Contains(t, requestID, fmt.Sprintf("sched-%s-nsid-%s-1-", tc.prefix, scheduleIDHash)) + require.Contains(t, requestID, fmt.Sprintf("sched-%s-nsid-%s-1-", tc.prefix, scheduleIDUUID)) require.NotContains(t, requestID, scheduleID) require.LessOrEqual(t, len(requestID), lowestKnownSchemaRequestIDColumnLimit) @@ -70,22 +71,6 @@ func TestGenerateRequestIDHashesLongScheduleID(t *testing.T) { } } -func TestGenerateRequestIDOnlyHashesWhenOverSQLColumnLimit(t *testing.T) { - nominalTime := time.UnixMilli(1_700_000_000_000) - actualTime := time.UnixMilli(1_700_000_000_001) - requestIDWithoutScheduleID := GenerateRequestID("nsid", "", 1, "", nominalTime, actualTime) - scheduleIDAtLimit := strings.Repeat("a", lowestKnownSchemaRequestIDColumnLimit-len(requestIDWithoutScheduleID)) - - requestID := GenerateRequestID("nsid", scheduleIDAtLimit, 1, "", nominalTime, actualTime) - require.Len(t, requestID, lowestKnownSchemaRequestIDColumnLimit) - require.Contains(t, requestID, scheduleIDAtLimit) - - overLimitScheduleID := scheduleIDAtLimit + "a" - requestID = GenerateRequestID("nsid", overLimitScheduleID, 1, "", nominalTime, actualTime) - require.LessOrEqual(t, len(requestID), lowestKnownSchemaRequestIDColumnLimit) - require.NotContains(t, requestID, overLimitScheduleID) -} - func TestGenerateRequestIDFitsSQLColumn(t *testing.T) { requestID := GenerateRequestID( strings.Repeat("a", 36), From 42be25fb6ca52da3d2172e83ec80f6e7efd2fc90 Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:30:00 -0500 Subject: [PATCH 4/7] Test large CHASM schedule IDs --- tests/schedule_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/schedule_test.go b/tests/schedule_test.go index dc179979557..5d6f04da49c 100644 --- a/tests/schedule_test.go +++ b/tests/schedule_test.go @@ -416,6 +416,7 @@ func TestScheduleCHASM(t *testing.T) { t.Run("TestMigrationCallbackAttach", func(t *testing.T) { t.Parallel(); testMigrationCallbackAttach(t, newContext) }) t.Run("TestCreatesWorkflowSentinel", func(t *testing.T) { t.Parallel(); testCreatesWorkflowSentinel(t, newContext) }) t.Run("TestSkipsWorkflowSentinelWhenDisabled", func(t *testing.T) { t.Parallel(); testSkipsWorkflowSentinelWhenDisabled(t, newContext) }) + t.Run("TestLargeScheduleID", func(t *testing.T) { t.Parallel(); testLargeScheduleID(t, newContext) }) t.Run("TestUpdateScheduleMemo", func(t *testing.T) { t.Parallel(); testUpdateScheduleMemo(t, newContext) }) t.Run("TestUpdateScheduleMemoOnly", func(t *testing.T) { t.Parallel(); testUpdateScheduleMemoOnly(t, newContext) }) t.Run("TestStateSizeBytesReported", func(t *testing.T) { t.Parallel(); testStateSizeBytesReported(t, newContext) }) @@ -4604,6 +4605,30 @@ func testUpdateScheduleRequestIDTooLong(t *testing.T, newContext contextFactory) require.ErrorAs(t, err, &invalidArgReqID) } +func testLargeScheduleID(t *testing.T, newContext contextFactory) { + s := newScheduleEnv(t, scheduleCommonOpts(t)...) + ctx := newContext(testcore.NewContext()) + + // The V1 sentinel shares the SQL workflow ID limit with the schedule ID + // prefix, so this is the largest schedule ID supported by every SQL backend. + const workflowIDColumnLimit = 255 + scheduleIDLength := workflowIDColumnLimit - len(scheduler.WorkflowIDPrefix) + sid := strings.Repeat("a", scheduleIDLength) + wid := testcore.RandomizeStr("sched-large-id-wf") + wt := testcore.RandomizeStr("sched-large-id-wt") + + var runs atomic.Int32 + registerCountingWorkflow(s, wt, &runs) + + createSchedule(ctx, t, s, sid, &schedulepb.Schedule{ + Spec: intervalSpec(fastInterval), + Action: startWorkflowAction(s, wid, wt), + }) + + await.RequireTruef(t, func() bool { return runs.Load() > 0 }, awaitTimeout, pollInterval, + "schedule ID of length %d should start a workflow", scheduleIDLength) +} + func testUpdateScheduleBlobSizeLimit(t *testing.T, newContext contextFactory) { s := newScheduleEnv(t, append(scheduleCommonOpts(t), From dc1a58f5e39d39c0b95450158297814149bf9563 Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:34:42 -0500 Subject: [PATCH 5/7] Align scheduler UUID namespace convention --- chasm/lib/scheduler/internal/request_id.go | 2 +- chasm/lib/scheduler/internal/request_id_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chasm/lib/scheduler/internal/request_id.go b/chasm/lib/scheduler/internal/request_id.go index ba57e855f8f..76bf921d45f 100644 --- a/chasm/lib/scheduler/internal/request_id.go +++ b/chasm/lib/scheduler/internal/request_id.go @@ -32,7 +32,7 @@ func GenerateRequestID( } // Keep request IDs bounded and deterministic even when schedule IDs are long. - scheduleIDUUID := uuid.NewSHA1(uuid.NameSpaceOID, []byte(scheduleID)) + scheduleIDUUID := uuid.NewSHA1(uuid.Nil, []byte(scheduleID)) return fmt.Sprintf( "sched-%s-%s-%s-%d-%d-%d", backfillID, diff --git a/chasm/lib/scheduler/internal/request_id_test.go b/chasm/lib/scheduler/internal/request_id_test.go index f981a8fb6de..8d7fd9e994f 100644 --- a/chasm/lib/scheduler/internal/request_id_test.go +++ b/chasm/lib/scheduler/internal/request_id_test.go @@ -23,7 +23,7 @@ func TestGenerateRequestID(t *testing.T) { nominalTime := time.Now() actualTime := time.Now() scheduleID := "mysched" - scheduleIDUUID := uuid.NewSHA1(uuid.NameSpaceOID, []byte(scheduleID)) + scheduleIDUUID := uuid.NewSHA1(uuid.Nil, []byte(scheduleID)) actual := GenerateRequestID("nsid", scheduleID, 10, "", nominalTime, actualTime) expected := fmt.Sprintf( @@ -48,7 +48,7 @@ func TestGenerateRequestIDUsesUUIDv5ForScheduleID(t *testing.T) { nominalTime := time.UnixMilli(1_700_000_000_000) actualTime := time.UnixMilli(1_700_000_000_001) scheduleID := strings.Repeat("a", 1000) - scheduleIDUUID := uuid.NewSHA1(uuid.NameSpaceOID, []byte(scheduleID)) + scheduleIDUUID := uuid.NewSHA1(uuid.Nil, []byte(scheduleID)) require.Equal(t, uuid.Version(5), scheduleIDUUID.Version()) for _, tc := range []struct { From 60f03c619860f8f0aeee4d911f6914619ec203f1 Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:06:42 -0500 Subject: [PATCH 6/7] Hash scheduler request ID inputs --- chasm/lib/scheduler/internal/request_id.go | 21 +++---- .../lib/scheduler/internal/request_id_test.go | 57 ++++--------------- 2 files changed, 19 insertions(+), 59 deletions(-) diff --git a/chasm/lib/scheduler/internal/request_id.go b/chasm/lib/scheduler/internal/request_id.go index 76bf921d45f..6c192c99ae1 100644 --- a/chasm/lib/scheduler/internal/request_id.go +++ b/chasm/lib/scheduler/internal/request_id.go @@ -7,13 +7,9 @@ import ( "github.com/google/uuid" ) -const lowestKnownSchemaRequestIDColumnLimit = 255 - // GenerateRequestID generates a deterministic request ID for a buffered action's // time. The request ID is deterministic because the jittered actual time (as // well as the spec's nominal time) is, in turn, also deterministic. -// Its total length must not exceed SQLite's request ID VARCHAR size, the -// smallest known persistence limit. // // backfillID should be left blank for actions that are being started // automatically, based on the schedule spec. It must be set for backfills, @@ -31,16 +27,17 @@ func GenerateRequestID( backfillID = "auto" } - // Keep request IDs bounded and deterministic even when schedule IDs are long. - scheduleIDUUID := uuid.NewSHA1(uuid.Nil, []byte(scheduleID)) return fmt.Sprintf( - "sched-%s-%s-%s-%d-%d-%d", + "sched-%s-%s", backfillID, - namespaceID, - scheduleIDUUID, - conflictToken, - nominal.UnixMilli(), - actual.UnixMilli(), + uuid.NewSHA1(uuid.Nil, []byte(fmt.Sprintf( + "%q-%q-%d-%d-%d", + namespaceID, + scheduleID, + conflictToken, + nominal.UnixMilli(), + actual.UnixMilli(), + ))), ) } diff --git a/chasm/lib/scheduler/internal/request_id_test.go b/chasm/lib/scheduler/internal/request_id_test.go index 8d7fd9e994f..be795767dd7 100644 --- a/chasm/lib/scheduler/internal/request_id_test.go +++ b/chasm/lib/scheduler/internal/request_id_test.go @@ -2,7 +2,6 @@ package internal import ( "fmt" - "math" "strings" "testing" "time" @@ -20,36 +19,17 @@ func TestGenerateWorkflowID(t *testing.T) { } func TestGenerateRequestID(t *testing.T) { - nominalTime := time.Now() - actualTime := time.Now() - scheduleID := "mysched" - scheduleIDUUID := uuid.NewSHA1(uuid.Nil, []byte(scheduleID)) - - actual := GenerateRequestID("nsid", scheduleID, 10, "", nominalTime, actualTime) - expected := fmt.Sprintf( - "sched-auto-nsid-%s-10-%d-%d", - scheduleIDUUID, - nominalTime.UnixMilli(), - actualTime.UnixMilli(), - ) - require.Equal(t, expected, actual) - - actual = GenerateRequestID("nsid", scheduleID, 10, "backfillid", nominalTime, actualTime) - expected = fmt.Sprintf( - "sched-backfillid-nsid-%s-10-%d-%d", - scheduleIDUUID, - nominalTime.UnixMilli(), - actualTime.UnixMilli(), - ) - require.Equal(t, expected, actual) -} - -func TestGenerateRequestIDUsesUUIDv5ForScheduleID(t *testing.T) { nominalTime := time.UnixMilli(1_700_000_000_000) actualTime := time.UnixMilli(1_700_000_000_001) scheduleID := strings.Repeat("a", 1000) - scheduleIDUUID := uuid.NewSHA1(uuid.Nil, []byte(scheduleID)) - require.Equal(t, uuid.Version(5), scheduleIDUUID.Version()) + requestIDUUID := uuid.NewSHA1(uuid.Nil, []byte(fmt.Sprintf( + "%q-%q-%d-%d-%d", + "nsid", + scheduleID, + 10, + nominalTime.UnixMilli(), + actualTime.UnixMilli(), + ))) for _, tc := range []struct { name string @@ -60,26 +40,9 @@ func TestGenerateRequestIDUsesUUIDv5ForScheduleID(t *testing.T) { {name: "backfill", backfillID: "backfill-id", prefix: "backfill-id"}, } { t.Run(tc.name, func(t *testing.T) { - requestID := GenerateRequestID("nsid", scheduleID, 1, tc.backfillID, nominalTime, actualTime) - require.Contains(t, requestID, fmt.Sprintf("sched-%s-nsid-%s-1-", tc.prefix, scheduleIDUUID)) + requestID := GenerateRequestID("nsid", scheduleID, 10, tc.backfillID, nominalTime, actualTime) + require.Equal(t, fmt.Sprintf("sched-%s-%s", tc.prefix, requestIDUUID), requestID) require.NotContains(t, requestID, scheduleID) - require.LessOrEqual(t, len(requestID), lowestKnownSchemaRequestIDColumnLimit) - - otherRequestID := GenerateRequestID("nsid", scheduleID+"b", 1, tc.backfillID, nominalTime, actualTime) - require.NotEqual(t, requestID, otherRequestID) }) } } - -func TestGenerateRequestIDFitsSQLColumn(t *testing.T) { - requestID := GenerateRequestID( - strings.Repeat("a", 36), - "schedule-id", - math.MinInt64, - strings.Repeat("b", 36), - time.UnixMilli(math.MinInt64), - time.UnixMilli(math.MaxInt64), - ) - - require.LessOrEqual(t, len(requestID), lowestKnownSchemaRequestIDColumnLimit) -} From 42f414b89af690ac7169aa006babbc392790329e Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:16:12 -0500 Subject: [PATCH 7/7] Use append formatting for scheduler request IDs --- chasm/lib/scheduler/internal/request_id.go | 4 ++-- chasm/lib/scheduler/internal/request_id_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chasm/lib/scheduler/internal/request_id.go b/chasm/lib/scheduler/internal/request_id.go index 6c192c99ae1..d84c29abd2a 100644 --- a/chasm/lib/scheduler/internal/request_id.go +++ b/chasm/lib/scheduler/internal/request_id.go @@ -30,14 +30,14 @@ func GenerateRequestID( return fmt.Sprintf( "sched-%s-%s", backfillID, - uuid.NewSHA1(uuid.Nil, []byte(fmt.Sprintf( + uuid.NewSHA1(uuid.Nil, fmt.Appendf(nil, "%q-%q-%d-%d-%d", namespaceID, scheduleID, conflictToken, nominal.UnixMilli(), actual.UnixMilli(), - ))), + )), ) } diff --git a/chasm/lib/scheduler/internal/request_id_test.go b/chasm/lib/scheduler/internal/request_id_test.go index be795767dd7..2739ce3fc49 100644 --- a/chasm/lib/scheduler/internal/request_id_test.go +++ b/chasm/lib/scheduler/internal/request_id_test.go @@ -22,14 +22,14 @@ func TestGenerateRequestID(t *testing.T) { nominalTime := time.UnixMilli(1_700_000_000_000) actualTime := time.UnixMilli(1_700_000_000_001) scheduleID := strings.Repeat("a", 1000) - requestIDUUID := uuid.NewSHA1(uuid.Nil, []byte(fmt.Sprintf( + requestIDUUID := uuid.NewSHA1(uuid.Nil, fmt.Appendf(nil, "%q-%q-%d-%d-%d", "nsid", scheduleID, 10, nominalTime.UnixMilli(), actualTime.UnixMilli(), - ))) + )) for _, tc := range []struct { name string