Skip to content

Commit 5bd467c

Browse files
test(dispatcher): guard tenant-stream msg IDs against drift (#4547)
* test(dispatcher): guard tenant-stream msg IDs against drift The pub/sub split (#4480) made the implicit durable-send mirror to tenant streams an explicit allowlist (msgqueue.tenantStreamMsgIDs), which nearly dropped task-cancelled on the API cancellation path during review. Nothing kept that allowlist in sync with what the dispatcher's streams consume. Make the dispatcher the checkable source of truth: msgsToWorkflowEvent's switch becomes a lookup in a package-level workflowEventConverters map and isMatchingWorkflowRunV1's switch a lookup in workflowRunMatchers, preserving behavior. msgqueue exports the allowlist via TenantStreamMsgIDs(), and TestTenantStreamMsgIDsInSync asserts set equality between the two, with failure messages saying exactly what to update on either side. * refactor(dispatcher): extract tenant-stream event handling into its own file Move workflowEventConverters, workflowRunMatchers, msgsToWorkflowEvent and isMatchingWorkflowRunV1 out of server.go into tenant_stream_events.go, next to the drift test that asserts their keys against msgqueue's allowlist. Collapse the repeated decode/loop/append scaffolding into two small generic adapters (eventConverter, runMatcher), so each map entry is just its payload type and field mapping; the workflow-run-finished status switch stays inside its closure. Drop the unused DispatcherImpl receivers from both functions. No behavior change: map misses and empty match results return exactly what they did before.
1 parent 4f65734 commit 5bd467c

4 files changed

Lines changed: 267 additions & 166 deletions

File tree

internal/msgqueue/pubsub.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package msgqueue
33
import (
44
"context"
55
"fmt"
6+
"maps"
7+
"slices"
68

79
"github.com/google/uuid"
810
"github.com/rs/zerolog"
@@ -80,9 +82,12 @@ type PubSub interface {
8082
}
8183

8284
// tenantStreamMsgIDs enumerates the message IDs the dispatcher's gRPC streams
83-
// consume from tenant topics (see msgsToWorkflowEvent and
84-
// isMatchingWorkflowRunV1 in the dispatcher). Publishing any other ID to a
85+
// consume from tenant topics (see workflowEventConverters and
86+
// workflowRunMatchers in the dispatcher). Publishing any other ID to a
8587
// tenant topic is pure waste: the streams are the only consumers.
88+
//
89+
// Guarded against drift by TestTenantStreamMsgIDsInSync in the dispatcher
90+
// package: an ID consumed there but missing here is silently never published.
8691
var tenantStreamMsgIDs = map[string]struct{}{
8792
MsgIDCreatedTask: {},
8893
MsgIDTaskCompleted: {},
@@ -93,6 +98,13 @@ var tenantStreamMsgIDs = map[string]struct{}{
9398
MsgIDWorkflowRunFinishedCandidate: {},
9499
}
95100

101+
// TenantStreamMsgIDs returns the message IDs PubTenantMessage publishes to
102+
// tenant-stream topics, sorted. It exists so the dispatcher can assert its
103+
// stream consumers stay in sync with this allowlist.
104+
func TenantStreamMsgIDs() []string {
105+
return slices.Sorted(maps.Keys(tenantStreamMsgIDs))
106+
}
107+
96108
// PubTenantMessage writes a tenant-scoped message to its destinations: a
97109
// durable send to queue via mq (when queue is non-nil), plus a publish to the
98110
// tenant stream when the message ID is one the dispatcher's streams consume.

internal/services/dispatcher/server.go

Lines changed: 3 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,7 @@ func (s *DispatcherImpl) subscribeToWorkflowRunsV1(server contracts.Dispatcher_S
11241124
wg.Add(1)
11251125
defer wg.Done()
11261126

1127-
if matchedWorkflowRunIds, ok := s.isMatchingWorkflowRunV1(msg, acks); ok {
1127+
if matchedWorkflowRunIds, ok := isMatchingWorkflowRunV1(msg, acks); ok {
11281128
if err := iter(matchedWorkflowRunIds); err != nil {
11291129
s.l.Error().Ctx(ctx).Err(err).Msg("could not iterate over workflow runs")
11301130
}
@@ -2049,7 +2049,7 @@ func (s *DispatcherImpl) subscribeToWorkflowEventsByWorkflowRunIdV1(workflowRunI
20492049
wg.Add(1)
20502050
defer wg.Done()
20512051

2052-
events, err := s.msgsToWorkflowEvent(
2052+
events, err := msgsToWorkflowEvent(
20532053
msgId,
20542054
payloads,
20552055
func(events []*contracts.WorkflowEvent) ([]*contracts.WorkflowEvent, error) {
@@ -2175,7 +2175,7 @@ func (s *DispatcherImpl) subscribeToWorkflowEventsByAdditionalMetaV1(key string,
21752175
wg.Add(1)
21762176
defer wg.Done()
21772177

2178-
events, err := s.msgsToWorkflowEvent(
2178+
events, err := msgsToWorkflowEvent(
21792179
msgId,
21802180
payloads,
21812181
func(events []*contracts.WorkflowEvent) ([]*contracts.WorkflowEvent, error) {
@@ -2349,164 +2349,3 @@ func (s *DispatcherImpl) listWorkflowRuns(ctx context.Context, tenantId uuid.UUI
23492349

23502350
return res, nil
23512351
}
2352-
2353-
func (s *DispatcherImpl) msgsToWorkflowEvent(msgId string, payloads [][]byte, filter func(tasks []*contracts.WorkflowEvent) ([]*contracts.WorkflowEvent, error), hangupFunc func(tasks []*contracts.WorkflowEvent) ([]*contracts.WorkflowEvent, error)) ([]*contracts.WorkflowEvent, error) {
2354-
workflowEvents := []*contracts.WorkflowEvent{}
2355-
2356-
switch msgId {
2357-
case "created-task":
2358-
payloads := msgqueue.JSONConvert[tasktypes.CreatedTaskPayload](payloads)
2359-
2360-
for _, payload := range payloads {
2361-
workflowEvents = append(workflowEvents, &contracts.WorkflowEvent{
2362-
WorkflowRunId: payload.WorkflowRunID.String(),
2363-
ResourceType: contracts.ResourceType_RESOURCE_TYPE_STEP_RUN,
2364-
ResourceId: payload.ExternalID.String(),
2365-
EventType: contracts.ResourceEventType_RESOURCE_EVENT_TYPE_STARTED,
2366-
EventTimestamp: timestamppb.New(payload.InsertedAt.Time),
2367-
RetryCount: &payload.RetryCount,
2368-
})
2369-
}
2370-
case "task-completed":
2371-
payloads := msgqueue.JSONConvert[tasktypes.CompletedTaskPayload](payloads)
2372-
2373-
for _, payload := range payloads {
2374-
workflowEvents = append(workflowEvents, &contracts.WorkflowEvent{
2375-
WorkflowRunId: payload.WorkflowRunId.String(),
2376-
ResourceType: contracts.ResourceType_RESOURCE_TYPE_STEP_RUN,
2377-
ResourceId: payload.ExternalId.String(),
2378-
EventType: contracts.ResourceEventType_RESOURCE_EVENT_TYPE_COMPLETED,
2379-
EventTimestamp: timestamppb.New(time.Now()),
2380-
RetryCount: &payload.RetryCount,
2381-
EventPayload: string(payload.Output),
2382-
})
2383-
}
2384-
case "task-failed":
2385-
payloads := msgqueue.JSONConvert[tasktypes.FailedTaskPayload](payloads)
2386-
2387-
for _, payload := range payloads {
2388-
workflowEvents = append(workflowEvents, &contracts.WorkflowEvent{
2389-
WorkflowRunId: payload.WorkflowRunId.String(),
2390-
ResourceType: contracts.ResourceType_RESOURCE_TYPE_STEP_RUN,
2391-
ResourceId: payload.ExternalId.String(),
2392-
EventType: contracts.ResourceEventType_RESOURCE_EVENT_TYPE_FAILED,
2393-
EventTimestamp: timestamppb.New(time.Now()),
2394-
RetryCount: &payload.RetryCount,
2395-
EventPayload: payload.ErrorMsg,
2396-
})
2397-
}
2398-
case "task-cancelled":
2399-
payloads := msgqueue.JSONConvert[tasktypes.CancelledTaskPayload](payloads)
2400-
2401-
for _, payload := range payloads {
2402-
workflowEvents = append(workflowEvents, &contracts.WorkflowEvent{
2403-
WorkflowRunId: payload.WorkflowRunId.String(),
2404-
ResourceType: contracts.ResourceType_RESOURCE_TYPE_STEP_RUN,
2405-
ResourceId: payload.ExternalId.String(),
2406-
EventType: contracts.ResourceEventType_RESOURCE_EVENT_TYPE_CANCELLED,
2407-
EventTimestamp: timestamppb.New(time.Now()),
2408-
RetryCount: &payload.RetryCount,
2409-
})
2410-
}
2411-
case "task-stream-event":
2412-
payloads := msgqueue.JSONConvert[tasktypes.StreamEventPayload](payloads)
2413-
2414-
for _, payload := range payloads {
2415-
workflowEvents = append(workflowEvents, &contracts.WorkflowEvent{
2416-
WorkflowRunId: payload.WorkflowRunId.String(),
2417-
ResourceType: contracts.ResourceType_RESOURCE_TYPE_STEP_RUN,
2418-
ResourceId: payload.TaskRunId.String(),
2419-
EventType: contracts.ResourceEventType_RESOURCE_EVENT_TYPE_STREAM,
2420-
EventTimestamp: timestamppb.New(payload.CreatedAt),
2421-
EventPayload: string(payload.Payload),
2422-
EventIndex: payload.EventIndex,
2423-
})
2424-
}
2425-
case "workflow-run-finished":
2426-
payloads := msgqueue.JSONConvert[tasktypes.NotifyFinalizedPayload](payloads)
2427-
2428-
for _, payload := range payloads {
2429-
eventType := contracts.ResourceEventType_RESOURCE_EVENT_TYPE_COMPLETED
2430-
2431-
switch payload.Status {
2432-
case sqlcv1.V1ReadableStatusOlapCANCELLED:
2433-
eventType = contracts.ResourceEventType_RESOURCE_EVENT_TYPE_CANCELLED
2434-
case sqlcv1.V1ReadableStatusOlapFAILED:
2435-
eventType = contracts.ResourceEventType_RESOURCE_EVENT_TYPE_FAILED
2436-
case sqlcv1.V1ReadableStatusOlapCOMPLETED:
2437-
eventType = contracts.ResourceEventType_RESOURCE_EVENT_TYPE_COMPLETED
2438-
}
2439-
2440-
workflowEvents = append(workflowEvents, &contracts.WorkflowEvent{
2441-
WorkflowRunId: payload.ExternalId.String(),
2442-
ResourceType: contracts.ResourceType_RESOURCE_TYPE_WORKFLOW_RUN,
2443-
ResourceId: payload.ExternalId.String(),
2444-
EventType: eventType,
2445-
EventTimestamp: timestamppb.New(time.Now()),
2446-
})
2447-
}
2448-
}
2449-
2450-
matches, err := filter(workflowEvents)
2451-
2452-
if err != nil {
2453-
return nil, err
2454-
}
2455-
2456-
matches, err = hangupFunc(matches)
2457-
2458-
if err != nil {
2459-
return nil, err
2460-
}
2461-
2462-
// order matches
2463-
slices.SortFunc(matches, func(a, b *contracts.WorkflowEvent) int {
2464-
// anything with a hangup should be last
2465-
if a.Hangup && !b.Hangup {
2466-
return 1
2467-
} else if !a.Hangup && b.Hangup {
2468-
return -1
2469-
}
2470-
2471-
return sortByEventIndex(a, b)
2472-
})
2473-
2474-
return matches, nil
2475-
}
2476-
2477-
func (s *DispatcherImpl) isMatchingWorkflowRunV1(msg *msgqueue.Message, acks *workflowRunAcks) ([]uuid.UUID, bool) {
2478-
switch msg.ID {
2479-
case "workflow-run-finished":
2480-
payloads := msgqueue.JSONConvert[tasktypes.NotifyFinalizedPayload](msg.Payloads)
2481-
res := make([]uuid.UUID, 0)
2482-
2483-
for _, payload := range payloads {
2484-
if acks.hasWorkflowRun(payload.ExternalId) {
2485-
res = append(res, payload.ExternalId)
2486-
}
2487-
}
2488-
2489-
if len(res) == 0 {
2490-
return nil, false
2491-
}
2492-
2493-
return res, true
2494-
case "workflow-run-finished-candidate":
2495-
payloads := msgqueue.JSONConvert[tasktypes.CandidateFinalizedPayload](msg.Payloads)
2496-
res := make([]uuid.UUID, 0)
2497-
2498-
for _, payload := range payloads {
2499-
if acks.hasWorkflowRun(payload.WorkflowRunId) {
2500-
res = append(res, payload.WorkflowRunId)
2501-
}
2502-
}
2503-
2504-
if len(res) == 0 {
2505-
return nil, false
2506-
}
2507-
2508-
return res, true
2509-
default:
2510-
return nil, false
2511-
}
2512-
}

0 commit comments

Comments
 (0)