From b790847d40ba0f089de558035286e2d7eb7ac268 Mon Sep 17 00:00:00 2001 From: cschleiden Date: Sun, 20 Oct 2024 20:37:14 -0700 Subject: [PATCH 01/13] Move Canceled --- workflow/context.go | 2 ++ workflow/sync.go | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/workflow/context.go b/workflow/context.go index 91eae5a1..7ba04500 100644 --- a/workflow/context.go +++ b/workflow/context.go @@ -4,6 +4,8 @@ import "github.com/cschleiden/go-workflows/internal/sync" type CancelFunc = sync.CancelFunc +var Canceled = sync.Canceled + // WithCancel returns a copy of parent with a new Done channel. The returned // context's Done channel is closed when the returned cancel function is called // or when the parent context's Done channel is closed, whichever happens first. diff --git a/workflow/sync.go b/workflow/sync.go index 0091acfd..8e2e2ac8 100644 --- a/workflow/sync.go +++ b/workflow/sync.go @@ -9,8 +9,6 @@ type ( WaitGroup = sync.WaitGroup ) -var Canceled = sync.Canceled - // NewWaitGroup creates a new WaitGroup instance. func NewWaitGroup() WaitGroup { return sync.NewWaitGroup() From 785f990aab38ff4cb5ca3ca07bd5ef4807f807fe Mon Sep 17 00:00:00 2001 From: cschleiden Date: Sun, 20 Oct 2024 20:51:57 -0700 Subject: [PATCH 02/13] Move propagators --- backend/redis/signal.go | 5 +++-- backend/redis/workflow.go | 4 ++-- internal/{tracing => propagators}/tracing.go | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) rename internal/{tracing => propagators}/tracing.go (98%) diff --git a/backend/redis/signal.go b/backend/redis/signal.go index b2923541..003ba1dc 100644 --- a/backend/redis/signal.go +++ b/backend/redis/signal.go @@ -7,7 +7,7 @@ import ( "github.com/cschleiden/go-workflows/backend" "github.com/cschleiden/go-workflows/backend/history" "github.com/cschleiden/go-workflows/internal/log" - "github.com/cschleiden/go-workflows/internal/tracing" + "github.com/cschleiden/go-workflows/internal/propagators" "github.com/cschleiden/go-workflows/workflow" "github.com/redis/go-redis/v9" "go.opentelemetry.io/otel/attribute" @@ -30,7 +30,8 @@ func (rb *redisBackend) SignalWorkflow(ctx context.Context, instanceID string, e return err } - ctx, err = (&tracing.TracingContextPropagator{}).Extract(ctx, instanceState.Metadata) + // TODO: Can we do this in the client? + ctx, err = (&propagators.TracingContextPropagator{}).Extract(ctx, instanceState.Metadata) if err != nil { rb.Options().Logger.Error("extracting tracing context", log.ErrorKey, err) } diff --git a/backend/redis/workflow.go b/backend/redis/workflow.go index 7b7072b7..48926293 100644 --- a/backend/redis/workflow.go +++ b/backend/redis/workflow.go @@ -11,7 +11,7 @@ import ( "github.com/cschleiden/go-workflows/backend/history" "github.com/cschleiden/go-workflows/core" "github.com/cschleiden/go-workflows/internal/log" - "github.com/cschleiden/go-workflows/internal/tracing" + "github.com/cschleiden/go-workflows/internal/propagators" "github.com/cschleiden/go-workflows/internal/workflowerrors" "github.com/cschleiden/go-workflows/workflow" "github.com/redis/go-redis/v9" @@ -301,7 +301,7 @@ func (rb *redisBackend) CompleteWorkflowTask( if state == core.WorkflowInstanceStateFinished || state == core.WorkflowInstanceStateContinuedAsNew { // Trace workflow completion - ctx, err = (&tracing.TracingContextPropagator{}).Extract(ctx, task.Metadata) + ctx, err = (&propagators.TracingContextPropagator{}).Extract(ctx, task.Metadata) if err != nil { rb.options.Logger.Error("extracting tracing context", log.ErrorKey, err) } diff --git a/internal/tracing/tracing.go b/internal/propagators/tracing.go similarity index 98% rename from internal/tracing/tracing.go rename to internal/propagators/tracing.go index 92279eb6..347404d3 100644 --- a/internal/tracing/tracing.go +++ b/internal/propagators/tracing.go @@ -1,4 +1,4 @@ -package tracing +package propagators import ( "context" From c5736960d572ac9000dbafa88db9604baf2d9333 Mon Sep 17 00:00:00 2001 From: cschleiden Date: Tue, 22 Oct 2024 22:02:04 -0700 Subject: [PATCH 03/13] Logical tracing --- backend/history/history.go | 6 + backend/history/serialization.go | 3 + backend/history/span_started.go | 7 + backend/history/subworkflow_scheduled.go | 2 + backend/history/workflow_started.go | 2 + backend/options.go | 7 +- backend/redis/instance.go | 4 +- backend/redis/signal.go | 17 -- backend/redis/workflow.go | 8 - backend/test/e2e.go | 1 + backend/test/e2e_tracing.go | 161 ++++++++++++++++++ client/client.go | 18 +- client/client_test.go | 12 +- go.mod | 54 +++--- go.sum | 130 ++++++-------- internal/activity/executor_test.go | 4 +- internal/command/schedule_subworkflow.go | 25 +-- internal/command/schedule_subworkflow_test.go | 2 +- internal/command/start_trace.go | 77 +++++++++ internal/propagators/tracing.go | 6 +- internal/tracing/context.go | 22 +++ internal/tracing/names.go | 7 + internal/tracing/workflowspan.go | 73 ++++++++ internal/workflowstate/replaylogger.go | 22 ++- internal/workflowstate/replaylogger_test.go | 5 +- internal/workflowstate/workflowstate.go | 10 +- internal/workflowstate/workflowstate_test.go | 3 +- internal/workflowtracer/tracer.go | 78 --------- samples/tracing/tracing.go | 37 +++- tester/tester.go | 3 +- workflow/activity.go | 13 +- workflow/activity_test.go | 11 +- workflow/executor/cache/cache_test.go | 10 +- workflow/executor/executor.go | 151 +++++++++++----- workflow/executor/executor_test.go | 4 +- workflow/executor/workflow.go | 5 +- workflow/sideeffect.go | 3 +- workflow/signal.go | 10 +- workflow/sleep.go | 3 +- workflow/subworkflow.go | 31 +++- workflow/subworkflow_test.go | 14 +- workflow/timer.go | 12 +- workflow/timer_test.go | 7 +- workflow/tracer.go | 85 ++++++++- 44 files changed, 805 insertions(+), 360 deletions(-) create mode 100644 backend/history/span_started.go create mode 100644 backend/test/e2e_tracing.go create mode 100644 internal/command/start_trace.go create mode 100644 internal/tracing/context.go create mode 100644 internal/tracing/names.go create mode 100644 internal/tracing/workflowspan.go delete mode 100644 internal/workflowtracer/tracer.go diff --git a/backend/history/history.go b/backend/history/history.go index 535e4aea..64d5c8b4 100644 --- a/backend/history/history.go +++ b/backend/history/history.go @@ -55,6 +55,9 @@ const ( // Recorded result of a side-efect EventType_SideEffectResult + + // Distributed tracing span has been started + EventType_TraceStarted ) func (et EventType) String() string { @@ -102,6 +105,9 @@ func (et EventType) String() string { case EventType_SideEffectResult: return "SideEffectResult" + case EventType_TraceStarted: + return "TraceStarted" + default: return "Unknown" } diff --git a/backend/history/serialization.go b/backend/history/serialization.go index 0f109deb..8d804e30 100644 --- a/backend/history/serialization.go +++ b/backend/history/serialization.go @@ -59,6 +59,9 @@ func DeserializeAttributes(eventType EventType, attributes []byte) (attr interfa case EventType_SideEffectResult: attr = &SideEffectResultAttributes{} + case EventType_TraceStarted: + attr = &TraceStartedAttributes{} + case EventType_TimerScheduled: attr = &TimerScheduledAttributes{} case EventType_TimerFired: diff --git a/backend/history/span_started.go b/backend/history/span_started.go new file mode 100644 index 00000000..bb70f4ae --- /dev/null +++ b/backend/history/span_started.go @@ -0,0 +1,7 @@ +package history + +import "github.com/cschleiden/go-workflows/backend/payload" + +type TraceStartedAttributes struct { + SpanID payload.Payload `json:"spanID"` +} diff --git a/backend/history/subworkflow_scheduled.go b/backend/history/subworkflow_scheduled.go index b1165e8c..cf5425f8 100644 --- a/backend/history/subworkflow_scheduled.go +++ b/backend/history/subworkflow_scheduled.go @@ -16,4 +16,6 @@ type SubWorkflowScheduledAttributes struct { Inputs []payload.Payload `json:"inputs,omitempty"` Metadata *metadata.WorkflowMetadata `json:"metadata,omitempty"` + + WorkflowSpanID [8]byte `json:"workflow_span_id,omitempty"` } diff --git a/backend/history/workflow_started.go b/backend/history/workflow_started.go index ccdfd2d9..7a0467f5 100644 --- a/backend/history/workflow_started.go +++ b/backend/history/workflow_started.go @@ -14,4 +14,6 @@ type ExecutionStartedAttributes struct { Metadata *metadata.WorkflowMetadata `json:"metadata,omitempty"` Inputs []payload.Payload `json:"inputs,omitempty"` + + WorkflowSpanID [8]byte `json:"workflowSpanID,omitempty"` } diff --git a/backend/options.go b/backend/options.go index d8c15e23..467abefb 100644 --- a/backend/options.go +++ b/backend/options.go @@ -7,9 +7,10 @@ import ( "github.com/cschleiden/go-workflows/backend/converter" "github.com/cschleiden/go-workflows/backend/metrics" mi "github.com/cschleiden/go-workflows/internal/metrics" - "github.com/cschleiden/go-workflows/internal/tracing" + "github.com/cschleiden/go-workflows/internal/propagators" "github.com/cschleiden/go-workflows/workflow" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) type Options struct { @@ -51,10 +52,10 @@ var DefaultOptions Options = Options{ Logger: slog.Default(), Metrics: mi.NewNoopMetricsClient(), - TracerProvider: trace.NewNoopTracerProvider(), + TracerProvider: noop.NewTracerProvider(), Converter: converter.DefaultConverter, - ContextPropagators: []workflow.ContextPropagator{&tracing.TracingContextPropagator{}}, + ContextPropagators: []workflow.ContextPropagator{&propagators.TracingContextPropagator{}}, RemoveContinuedAsNewInstances: false, } diff --git a/backend/redis/instance.go b/backend/redis/instance.go index 1a7fad5d..dd64ed30 100644 --- a/backend/redis/instance.go +++ b/backend/redis/instance.go @@ -133,10 +133,10 @@ func (rb *redisBackend) CancelWorkflowInstance(ctx context.Context, instance *co } // Cancel instance - if cmds, err := rb.rdb.Pipelined(ctx, func(p redis.Pipeliner) error { + if _, err := rb.rdb.Pipelined(ctx, func(p redis.Pipeliner) error { return rb.addWorkflowInstanceEventP(ctx, p, workflow.Queue(instanceState.Queue), instance, event) }); err != nil { - fmt.Println(cmds) + // fmt.Println(cmds) return fmt.Errorf("adding cancellation event to workflow instance: %w", err) } diff --git a/backend/redis/signal.go b/backend/redis/signal.go index 003ba1dc..b21723cd 100644 --- a/backend/redis/signal.go +++ b/backend/redis/signal.go @@ -6,12 +6,8 @@ import ( "github.com/cschleiden/go-workflows/backend" "github.com/cschleiden/go-workflows/backend/history" - "github.com/cschleiden/go-workflows/internal/log" - "github.com/cschleiden/go-workflows/internal/propagators" "github.com/cschleiden/go-workflows/workflow" "github.com/redis/go-redis/v9" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" ) func (rb *redisBackend) SignalWorkflow(ctx context.Context, instanceID string, event *history.Event) error { @@ -30,19 +26,6 @@ func (rb *redisBackend) SignalWorkflow(ctx context.Context, instanceID string, e return err } - // TODO: Can we do this in the client? - ctx, err = (&propagators.TracingContextPropagator{}).Extract(ctx, instanceState.Metadata) - if err != nil { - rb.Options().Logger.Error("extracting tracing context", log.ErrorKey, err) - } - - a := event.Attributes.(*history.SignalReceivedAttributes) - ctx, span := rb.Tracer().Start(ctx, fmt.Sprintf("SignalWorkflow: %s", a.Name), trace.WithAttributes( - attribute.String(log.InstanceIDKey, instanceID), - attribute.String(log.SignalNameKey, event.Attributes.(*history.SignalReceivedAttributes).Name), - )) - defer span.End() - if _, err = rb.rdb.TxPipelined(ctx, func(p redis.Pipeliner) error { if err := rb.addWorkflowInstanceEventP(ctx, p, workflow.Queue(instanceState.Queue), instanceState.Instance, event); err != nil { return fmt.Errorf("adding event to stream: %w", err) diff --git a/backend/redis/workflow.go b/backend/redis/workflow.go index 48926293..d14ef2d5 100644 --- a/backend/redis/workflow.go +++ b/backend/redis/workflow.go @@ -15,8 +15,6 @@ import ( "github.com/cschleiden/go-workflows/internal/workflowerrors" "github.com/cschleiden/go-workflows/workflow" "github.com/redis/go-redis/v9" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" ) func (rb *redisBackend) PrepareWorkflowQueues(ctx context.Context, queues []workflow.Queue) error { @@ -306,12 +304,6 @@ func (rb *redisBackend) CompleteWorkflowTask( rb.options.Logger.Error("extracting tracing context", log.ErrorKey, err) } - _, span := rb.Tracer().Start(ctx, "WorkflowComplete", - trace.WithAttributes( - attribute.String(log.NamespaceKey+log.InstanceIDKey, task.WorkflowInstance.InstanceID), - )) - span.End() - // Auto expiration expiration := rb.options.AutoExpiration if state == core.WorkflowInstanceStateContinuedAsNew && rb.options.AutoExpirationContinueAsNew > 0 { diff --git a/backend/test/e2e.go b/backend/test/e2e.go index 13133b09..642264f5 100644 --- a/backend/test/e2e.go +++ b/backend/test/e2e.go @@ -622,6 +622,7 @@ func EndToEndBackendTest(t *testing.T, setup func(options ...backend.BackendOpti tests = append(tests, e2eQueueTests...) tests = append(tests, e2eRemovalTests...) tests = append(tests, e2eContinueAsNewTests...) + tests = append(tests, e2eTracingTests...) run := func(suffix string, workerOptions worker.Options) { for _, tt := range tests { diff --git a/backend/test/e2e_tracing.go b/backend/test/e2e_tracing.go new file mode 100644 index 00000000..51f558e6 --- /dev/null +++ b/backend/test/e2e_tracing.go @@ -0,0 +1,161 @@ +package test + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/cschleiden/go-workflows/client" + "github.com/cschleiden/go-workflows/registry" + "github.com/cschleiden/go-workflows/worker" + "github.com/cschleiden/go-workflows/workflow" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" +) + +func setupTracing(b TestBackend) *tracetest.InMemoryExporter { + exporter := tracetest.NewInMemoryExporter() + processor := trace.NewSimpleSpanProcessor(exporter) + provider := trace.NewTracerProvider(trace.WithSpanProcessor(processor)) + b.Options().TracerProvider = provider + + return exporter +} + +var e2eTracingTests = []backendTest{ + { + name: "Tracing/WorkflowsHaveSpans", + f: func(t *testing.T, ctx context.Context, c *client.Client, w *worker.Worker, b TestBackend) { + exporter := setupTracing(b) + + wf := func(ctx workflow.Context) error { + + return nil + } + register(t, ctx, w, []interface{}{wf}, nil) + + instance := runWorkflow(t, ctx, c, wf) + _, err := client.GetWorkflowResult[any](ctx, c, instance, time.Second*5) + require.NoError(t, err) + + spans := exporter.GetSpans().Snapshots() + + createWorkflowSpan := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "CreateWorkflowInstance") + }) + require.NotNil(t, createWorkflowSpan) + + workflow1Span := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "Workflow: 1") + }) + require.NotNil(t, workflow1Span) + + require.Equal(t, + createWorkflowSpan.SpanContext().SpanID().String(), + workflow1Span.Parent().SpanID().String(), + ) + }, + }, + { + name: "Tracing/SubWorkflowsHaveSpansWithCorrectParent", + f: func(t *testing.T, ctx context.Context, c *client.Client, w *worker.Worker, b TestBackend) { + exporter := setupTracing(b) + + swf := func(ctx workflow.Context) error { + workflow.Sleep(ctx, time.Microsecond*10) + + return nil + } + + wf := func(ctx workflow.Context) error { + _, err := workflow.CreateSubWorkflowInstance[any](ctx, workflow.DefaultSubWorkflowOptions, "swf").Get(ctx) + return err + } + + w.RegisterWorkflow(wf, registry.WithName("wf")) + w.RegisterWorkflow(swf, registry.WithName("swf")) + + err := w.Start(ctx) + require.NoError(t, err) + + instance := runWorkflow(t, ctx, c, "wf") + _, err = client.GetWorkflowResult[any](ctx, c, instance, time.Second*5) + require.NoError(t, err) + + spans := exporter.GetSpans().Snapshots() + + createWorkflowSpan := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "CreateWorkflowInstance") + }) + require.NotNil(t, createWorkflowSpan) + + workflow1Span := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "Workflow: wf") + }) + require.NotNil(t, workflow1Span) + + workflow2Span := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "Workflow: swf") + }) + require.NotNil(t, workflow1Span) + + require.Equal(t, + workflow1Span.SpanContext().SpanID().String(), + workflow2Span.Parent().SpanID().String(), + ) + }, + }, + { + name: "Tracing/InnerWorkflowSpansHaveCorrectParent", + f: func(t *testing.T, ctx context.Context, c *client.Client, w *worker.Worker, b TestBackend) { + exporter := setupTracing(b) + + wf := func(ctx workflow.Context) error { + ctx, span := workflow.Tracer(ctx).Start(ctx, "inner-span") + defer span.End() + + ctx, span2 := workflow.Tracer(ctx).Start(ctx, "inner-span2") + defer span2.End() + + workflow.Sleep(ctx, time.Microsecond*10) + + return nil + } + register(t, ctx, w, []interface{}{wf}, nil) + + instance := runWorkflow(t, ctx, c, wf) + _, err := client.GetWorkflowResult[any](ctx, c, instance, time.Second*5) + require.NoError(t, err) + + spans := exporter.GetSpans().Snapshots() + workflow1Span := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "Workflow: 1") + }) + require.NotNil(t, workflow1Span) + + innerSpan := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return span.Name() == "inner-span" + }) + require.NotNil(t, innerSpan) + require.Equal(t, workflow1Span.SpanContext().SpanID(), innerSpan.Parent().SpanID()) + + innerSpan2 := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return span.Name() == "inner-span2" + }) + require.NotNil(t, innerSpan2) + require.Equal(t, innerSpan.SpanContext().SpanID(), innerSpan2.Parent().SpanID()) + }, + }, +} + +func findSpan(spans []trace.ReadOnlySpan, f func(trace.ReadOnlySpan) bool) trace.ReadOnlySpan { + for _, span := range spans { + if f(span) { + return span + } + } + + return nil +} diff --git a/client/client.go b/client/client.go index 04b19b66..71bcf3bd 100644 --- a/client/client.go +++ b/client/client.go @@ -16,6 +16,7 @@ import ( "github.com/cschleiden/go-workflows/internal/fn" "github.com/cschleiden/go-workflows/internal/log" "github.com/cschleiden/go-workflows/internal/metrickeys" + "github.com/cschleiden/go-workflows/internal/tracing" "github.com/cschleiden/go-workflows/internal/workflowerrors" "github.com/cschleiden/go-workflows/workflow" "github.com/google/uuid" @@ -81,27 +82,32 @@ func (c *Client) CreateWorkflowInstance(ctx context.Context, options WorkflowIns wfi := core.NewWorkflowInstance(options.InstanceID, uuid.NewString()) metadata := &workflow.Metadata{} - // Start new span for the workflow instance - ctx, span := c.backend.Tracer().Start(ctx, fmt.Sprintf("CreateWorkflowInstance: %s", workflowName), trace.WithAttributes( + // Span for creating the workflow instance + ctx, span := c.backend.Tracer().Start(ctx, "CreateWorkflowInstance", trace.WithAttributes( attribute.String(log.InstanceIDKey, wfi.InstanceID), + attribute.String(log.ExecutionIDKey, wfi.ExecutionID), attribute.String(log.WorkflowNameKey, workflowName), )) defer span.End() + // Inject state from any propagators for _, propagator := range c.backend.Options().ContextPropagators { if err := propagator.Inject(ctx, metadata); err != nil { return nil, fmt.Errorf("injecting context to propagate: %w", err) } } + workflowSpanID := tracing.GetNewSpanID(c.backend.Tracer()) + startedEvent := history.NewPendingEvent( c.clock.Now(), history.EventType_WorkflowExecutionStarted, &history.ExecutionStartedAttributes{ - Queue: options.Queue, - Metadata: metadata, - Name: workflowName, - Inputs: inputs, + Queue: options.Queue, + Metadata: metadata, + Name: workflowName, + Inputs: inputs, + WorkflowSpanID: workflowSpanID, }) if err := c.backend.CreateWorkflowInstance(ctx, wfi, startedEvent); err != nil { diff --git a/client/client_test.go b/client/client_test.go index 3cb401df..766bb48f 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -17,7 +17,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) func Test_Client_CreateWorkflowInstance_ParamMismatch(t *testing.T) { @@ -46,7 +46,7 @@ func Test_Client_CreateWorkflowInstance_NameGiven(t *testing.T) { b := &backend.MockBackend{} b.On("Options").Return(backend.ApplyOptions(backend.WithConverter(converter.DefaultConverter), backend.WithLogger(slog.Default()))) - b.On("Tracer").Return(trace.NewNoopTracerProvider().Tracer("test")) + b.On("Tracer").Return(noop.NewTracerProvider().Tracer("test")) b.On("Metrics").Return(metrics.NewNoopMetricsClient()) b.On("CreateWorkflowInstance", mock.Anything, mock.Anything, mock.MatchedBy(func(event *history.Event) bool { if event.Type != history.EventType_WorkflowExecutionStarted { @@ -77,7 +77,7 @@ func Test_Client_GetWorkflowResultTimeout(t *testing.T) { ctx := context.Background() b := &backend.MockBackend{} - b.On("Tracer").Return(trace.NewNoopTracerProvider().Tracer("test")) + b.On("Tracer").Return(noop.NewTracerProvider().Tracer("test")) b.On("GetWorkflowInstanceState", mock.Anything, instance).Return(core.WorkflowInstanceStateActive, nil) c := &Client{ @@ -101,7 +101,7 @@ func Test_Client_GetWorkflowResultSuccess(t *testing.T) { r, _ := converter.DefaultConverter.To(42) b := &backend.MockBackend{} - b.On("Tracer").Return(trace.NewNoopTracerProvider().Tracer("test")) + b.On("Tracer").Return(noop.NewTracerProvider().Tracer("test")) b.On("GetWorkflowInstanceState", mock.Anything, instance).Return(core.WorkflowInstanceStateActive, nil).Once().Run(func(args mock.Arguments) { // After the first call, advance the clock to immediately go to the second call below mockClock.Add(time.Second) @@ -134,7 +134,7 @@ func Test_Client_SignalWorkflow(t *testing.T) { b := &backend.MockBackend{} b.On("Options").Return(backend.ApplyOptions(backend.WithConverter(converter.DefaultConverter), backend.WithLogger(slog.Default()))) - b.On("Tracer").Return(trace.NewNoopTracerProvider().Tracer("test")) + b.On("Tracer").Return(noop.NewTracerProvider().Tracer("test")) b.On("SignalWorkflow", mock.Anything, instanceID, mock.MatchedBy(func(event *history.Event) bool { return event.Type == history.EventType_SignalReceived && event.Attributes.(*history.SignalReceivedAttributes).Name == "test" @@ -162,7 +162,7 @@ func Test_Client_SignalWorkflow_WithArgs(t *testing.T) { b := &backend.MockBackend{} b.On("Options").Return(backend.ApplyOptions(backend.WithConverter(converter.DefaultConverter), backend.WithLogger(slog.Default()))) - b.On("Tracer").Return(trace.NewNoopTracerProvider().Tracer("test")) + b.On("Tracer").Return(noop.NewTracerProvider().Tracer("test")) b.On("SignalWorkflow", mock.Anything, instanceID, mock.MatchedBy(func(event *history.Event) bool { return event.Type == history.EventType_SignalReceived && event.Attributes.(*history.SignalReceivedAttributes).Name == "test" && diff --git a/go.mod b/go.mod index 340aa8a0..686fc85d 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,26 @@ module github.com/cschleiden/go-workflows -go 1.21 +go 1.22 + +toolchain go1.23.2 require ( github.com/go-errors/errors v1.4.2 github.com/go-sql-driver/mysql v1.7.0 github.com/golang-migrate/migrate/v4 v4.16.2 github.com/golangci/golangci-lint v1.54.2 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.6.0 github.com/jellydator/ttlcache/v3 v3.0.0 github.com/jstemmer/go-junit-report/v2 v2.0.0-beta1 github.com/redis/go-redis/v9 v9.0.2 - github.com/stretchr/testify v1.8.4 - go.opentelemetry.io/otel v1.16.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 - go.opentelemetry.io/otel/trace v1.16.0 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.31.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 + go.opentelemetry.io/otel/trace v1.31.0 go.uber.org/goleak v1.3.0 - golang.org/x/tools v0.12.0 + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d modernc.org/sqlite v1.27.0 ) @@ -36,7 +38,7 @@ require ( github.com/curioswitch/go-reassign v0.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/firefart/nonamedreturns v1.0.4 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kkHAIKE/contextcheck v1.1.4 // indirect github.com/lufeee/execinquery v1.2.1 // indirect @@ -53,9 +55,8 @@ require ( github.com/timonwong/loggercheck v0.9.4 // indirect github.com/xen0n/gosmopolitan v1.2.1 // indirect github.com/ykadowak/zerologlint v0.1.3 // indirect - go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect - go.opentelemetry.io/otel/metric v1.16.0 // indirect - go.opentelemetry.io/proto/otlp v0.19.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.tmz.dev/musttag v0.7.2 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect @@ -63,7 +64,7 @@ require ( golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect - google.golang.org/grpc v1.55.0 // indirect + google.golang.org/grpc v1.67.1 // indirect lukechampine.com/uint128 v1.2.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect @@ -91,8 +92,8 @@ require ( github.com/bombsimon/wsl/v3 v3.4.0 // indirect; indirec github.com/breml/errchkjson v0.3.1 // indirect github.com/butuzov/ireturn v0.2.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charithe/durationcheck v0.0.10 // indirect github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8 // indirect github.com/daixiang0/gci v0.11.0 // indirect @@ -105,7 +106,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/go-critic/go-critic v0.9.0 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect @@ -127,7 +128,7 @@ require ( github.com/golangci/misspell v0.4.1 // indirect github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // indirect github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.4.2 // indirect @@ -205,13 +206,12 @@ require ( github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.2.0 // indirect gitlab.com/bosi/decorder v0.4.0 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.14.0 // indirect - golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect honnef.co/go/tools v0.4.5 // indirect @@ -225,7 +225,7 @@ require ( github.com/benbjohnson/clock v1.3.0 github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect - go.opentelemetry.io/otel/sdk v1.16.0 + github.com/stretchr/objx v0.5.2 // indirect + go.opentelemetry.io/otel/sdk v1.31.0 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index afde4eed..173427d0 100644 --- a/go.sum +++ b/go.sum @@ -62,7 +62,6 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard/v2 v2.1.0 h1:aQl70G173h/GZYhWf36aE5H0KaujXfVMnn/f1kSDVYY= github.com/OpenPeeDeeP/depguard/v2 v2.1.0/go.mod h1:PUBgk35fX4i7JDmwzlJwJ+GMe6NfO1723wmJMgPThNQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -76,7 +75,6 @@ github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pO github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= @@ -107,14 +105,13 @@ github.com/butuzov/mirror v1.1.0 h1:ZqX54gBVMXu78QLoiqdwpl2mgmoOJTk7s4p4o+0avZI= github.com/butuzov/mirror v1.1.0/go.mod h1:8Q0BdQU6rC6WILDiBM60DBfvV78OLJmMmixe7GF45AE= github.com/ccojocar/zxcvbn-go v1.0.1 h1:+sxrANSCj6CdadkcMnvde/GWU1vZiiXRbqYSCalV4/4= github.com/ccojocar/zxcvbn-go v1.0.1/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8 h1:W9o46d2kbNL06lq7UNDPV0zYLzkrde/bjIqO02eoll0= @@ -126,11 +123,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= @@ -160,8 +152,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/esimonov/ifshort v1.0.4 h1:6SID4yGWfRae/M7hkVDVVyppy8q/v9OuxNdmjLQStBA= github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcHcfgNWTk0= @@ -179,7 +169,6 @@ github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwV github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-critic/go-critic v0.9.0 h1:Pmys9qvU3pSML/3GEQ2Xd9RZ/ip+aXHKILuxczKGV/U= github.com/go-critic/go-critic v0.9.0/go.mod h1:5P8tdXL7m/6qnyG6oRAlYLORvoXH0WDypYgAEmagT40= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -194,8 +183,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= @@ -233,9 +222,6 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -299,8 +285,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -319,8 +305,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -338,9 +324,8 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY= github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -393,7 +378,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -530,10 +514,9 @@ github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSx github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJHMLuTw= github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= @@ -566,7 +549,6 @@ github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= @@ -586,8 +568,9 @@ github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8L github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -596,8 +579,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= @@ -649,25 +632,22 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= -go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 h1:iqjq9LAB8aK++sKVcELezzn655JnBNdsDhghU4G/So8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0/go.mod h1:hGXzO5bhhSHZnKvrDaXB82Y9DRFour0Nz/KrBh7reWw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 h1:+XWJd3jf75RXJq29mxbuXhCXFDG3S3R4vBUeSI2P7tE= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0/go.mod h1:hqgzBPTf4yONMFgdZvL/bK42R/iinTyVQtiWihs3SZc= -go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= -go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= -go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= -go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= -go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= -go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 h1:UGZ1QwZWY67Z6BmckTU+9Rxn04m2bD3gD6Mk0OIOCPk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0/go.mod h1:fcwWuDuaObkkChiDlhEpSq9+X1C0omv+s5mBtToAQ64= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.tmz.dev/musttag v0.7.2 h1:1J6S9ipDbalBSODNT5jCep8dhZyMr4ttnjQagmGYR5s= go.tmz.dev/musttag v0.7.2/go.mod h1:m6q5NiiSKMnQYokefa2xGoyoXnrswCbJ0AWYzf4Zs28= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= @@ -734,8 +714,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -777,8 +757,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -789,7 +769,6 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -803,8 +782,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -863,8 +842,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -877,14 +856,13 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -956,8 +934,8 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1011,7 +989,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -1025,7 +1002,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1041,15 +1017,11 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1062,9 +1034,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1076,7 +1047,6 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/activity/executor_test.go b/internal/activity/executor_test.go index c23c1329..4d0a2ff3 100644 --- a/internal/activity/executor_test.go +++ b/internal/activity/executor_test.go @@ -18,7 +18,7 @@ import ( "github.com/cschleiden/go-workflows/registry" "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) func TestExecutor_ExecuteActivity(t *testing.T) { @@ -114,7 +114,7 @@ func TestExecutor_ExecuteActivity(t *testing.T) { logger: slog.Default(), r: r, converter: converter.DefaultConverter, - tracer: trace.NewNoopTracerProvider().Tracer(""), + tracer: noop.NewTracerProvider().Tracer(""), } got, err := e.ExecuteActivity(context.Background(), &backend.ActivityTask{ ID: uuid.NewString(), diff --git a/internal/command/schedule_subworkflow.go b/internal/command/schedule_subworkflow.go index 8c3dbd40..d0386df0 100644 --- a/internal/command/schedule_subworkflow.go +++ b/internal/command/schedule_subworkflow.go @@ -12,9 +12,10 @@ import ( type ScheduleSubWorkflowCommand struct { cancelableCommand - Queue core.Queue - Instance *core.WorkflowInstance - Metadata *metadata.WorkflowMetadata + Queue core.Queue + Instance *core.WorkflowInstance + Metadata *metadata.WorkflowMetadata + WorkflowSpanID [8]byte Name string Inputs []payload.Payload @@ -24,7 +25,7 @@ var _ CancelableCommand = (*ScheduleSubWorkflowCommand)(nil) func NewScheduleSubWorkflowCommand( id int64, parentInstance *core.WorkflowInstance, subWorkflowQueue core.Queue, subWorkflowInstanceID, - name string, inputs []payload.Payload, metadata *metadata.WorkflowMetadata, + name string, inputs []payload.Payload, metadata *metadata.WorkflowMetadata, workflowSpanID [8]byte, ) *ScheduleSubWorkflowCommand { if subWorkflowInstanceID == "" { subWorkflowInstanceID = uuid.New().String() @@ -39,9 +40,10 @@ func NewScheduleSubWorkflowCommand( }, }, - Queue: subWorkflowQueue, - Instance: core.NewSubWorkflowInstance(subWorkflowInstanceID, uuid.NewString(), parentInstance, id), - Metadata: metadata, + Queue: subWorkflowQueue, + Instance: core.NewSubWorkflowInstance(subWorkflowInstanceID, uuid.NewString(), parentInstance, id), + Metadata: metadata, + WorkflowSpanID: workflowSpanID, Name: name, Inputs: inputs, @@ -75,10 +77,11 @@ func (c *ScheduleSubWorkflowCommand) Execute(clock clock.Clock) *CommandResult { clock.Now(), history.EventType_WorkflowExecutionStarted, &history.ExecutionStartedAttributes{ - Queue: c.Queue, - Name: c.Name, - Inputs: c.Inputs, - Metadata: c.Metadata, + Queue: c.Queue, + Name: c.Name, + Inputs: c.Inputs, + Metadata: c.Metadata, + WorkflowSpanID: c.WorkflowSpanID, }, ), }, diff --git a/internal/command/schedule_subworkflow_test.go b/internal/command/schedule_subworkflow_test.go index 82cb6940..097f41d5 100644 --- a/internal/command/schedule_subworkflow_test.go +++ b/internal/command/schedule_subworkflow_test.go @@ -98,7 +98,7 @@ func TestScheduleSubWorkflowCommand_StateTransitions(t *testing.T) { parentInstance := core.NewWorkflowInstance(uuid.NewString(), "") cmd := NewScheduleSubWorkflowCommand( - 1, parentInstance, core.QueueDefault, uuid.NewString(), "SubWorkflow", []payload.Payload{}, &metadata.WorkflowMetadata{}) + 1, parentInstance, core.QueueDefault, uuid.NewString(), "SubWorkflow", []payload.Payload{}, &metadata.WorkflowMetadata{}, [8]byte{}) tt.f(t, cmd, clock) }) diff --git a/internal/command/start_trace.go b/internal/command/start_trace.go new file mode 100644 index 00000000..599d5509 --- /dev/null +++ b/internal/command/start_trace.go @@ -0,0 +1,77 @@ +package command + +import ( + "github.com/benbjohnson/clock" + "github.com/cschleiden/go-workflows/backend/history" + "github.com/cschleiden/go-workflows/backend/payload" +) + +type StartTraceCommand struct { + command + + spanID payload.Payload +} + +var _ Command = (*StartTraceCommand)(nil) + +// Command transitions are +// Pending -> Done : Trace has been started, Span has been created + +func NewStartTraceCommand(id int64) *StartTraceCommand { + return &StartTraceCommand{ + command: command{ + id: id, + name: "StartTrace", + state: CommandState_Pending, + }, + } +} + +func (c *StartTraceCommand) SetSpanID(spanID payload.Payload) { + c.spanID = spanID +} + +func (c *StartTraceCommand) Commit() { + switch c.state { + case CommandState_Pending: + c.state = CommandState_Done + + default: + c.invalidStateTransition(CommandState_Done) + } +} + +func (c *StartTraceCommand) Execute(clock clock.Clock) *CommandResult { + switch c.state { + case CommandState_Pending: + c.state = CommandState_Done + + return &CommandResult{ + Events: []*history.Event{ + history.NewPendingEvent( + clock.Now(), + history.EventType_TraceStarted, + &history.TraceStartedAttributes{ + SpanID: c.spanID, + }, + history.ScheduleEventID(c.id), + ), + }, + } + } + + return nil +} + +func (c *StartTraceCommand) Done() { + switch c.state { + case CommandState_Pending, CommandState_Committed: + c.state = CommandState_Done + if c.whenDone != nil { + c.whenDone() + } + + default: + c.invalidStateTransition(CommandState_Done) + } +} diff --git a/internal/propagators/tracing.go b/internal/propagators/tracing.go index 347404d3..7dba148c 100644 --- a/internal/propagators/tracing.go +++ b/internal/propagators/tracing.go @@ -3,7 +3,7 @@ package propagators import ( "context" - "github.com/cschleiden/go-workflows/internal/workflowtracer" + "github.com/cschleiden/go-workflows/internal/tracing" "github.com/cschleiden/go-workflows/workflow" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" @@ -37,7 +37,7 @@ func (*TracingContextPropagator) Extract(ctx context.Context, metadata *workflow } func (*TracingContextPropagator) InjectFromWorkflow(ctx workflow.Context, metadata *workflow.Metadata) error { - span := workflowtracer.SpanFromContext(ctx) + span := tracing.SpanFromContext(ctx) sctx := trace.ContextWithSpan(context.Background(), span) injectSpan(sctx, metadata) @@ -49,5 +49,5 @@ func (*TracingContextPropagator) ExtractToWorkflow(ctx workflow.Context, metadat sctx := extractSpan(context.Background(), metadata) span := trace.SpanFromContext(sctx) - return workflowtracer.ContextWithSpan(ctx, span), nil + return tracing.ContextWithSpan(ctx, span), nil } diff --git a/internal/tracing/context.go b/internal/tracing/context.go new file mode 100644 index 00000000..23862149 --- /dev/null +++ b/internal/tracing/context.go @@ -0,0 +1,22 @@ +package tracing + +import ( + "github.com/cschleiden/go-workflows/internal/sync" + "go.opentelemetry.io/otel/trace" +) + +type spanContextKeyType int + +const spanKey spanContextKeyType = iota + +func ContextWithSpan(ctx sync.Context, span trace.Span) sync.Context { + return sync.WithValue(ctx, spanKey, span) +} + +func SpanFromContext(ctx sync.Context) trace.Span { + if span, ok := ctx.Value(spanKey).(trace.Span); ok { + return span + } + + return nil +} diff --git a/internal/tracing/names.go b/internal/tracing/names.go new file mode 100644 index 00000000..d72d43f3 --- /dev/null +++ b/internal/tracing/names.go @@ -0,0 +1,7 @@ +package tracing + +import "fmt" + +func WorkflowSpanName(workflowName string) string { + return fmt.Sprintf("Workflow: %s", workflowName) +} diff --git a/internal/tracing/workflowspan.go b/internal/tracing/workflowspan.go new file mode 100644 index 00000000..67c1d206 --- /dev/null +++ b/internal/tracing/workflowspan.go @@ -0,0 +1,73 @@ +package tracing + +import ( + "context" + "reflect" + "time" + "unsafe" + + "github.com/cschleiden/go-workflows/internal/sync" + "github.com/cschleiden/go-workflows/internal/workflowstate" + "go.opentelemetry.io/otel/trace" +) + +func SpanWithStartTime(ctx context.Context, tracer trace.Tracer, name string, spanID trace.SpanID, startTime time.Time) trace.Span { + _, span := tracer.Start(ctx, + name, + trace.WithTimestamp(startTime), + ) + + SetSpanID(span, spanID) + + return span +} + +func GetNewSpanID(tracer trace.Tracer) trace.SpanID { + // Create workflow span. We pass the name here, but in the end we only use the span ID. + // The tracer doesn't expose the span ID generator that's being used, so we use this + // ugly workaround here. + _, span := tracer.Start(context.Background(), "canceled-span", trace.WithSpanKind(trace.SpanKindInternal)) + cancelSpan(span) // We don't actually want to send this span + span.End() + + return span.SpanContext().SpanID() +} + +func GetNewSpanIDWF(ctx sync.Context) trace.SpanID { + tracer := workflowstate.WorkflowState(ctx).Tracer() + return GetNewSpanID(tracer) +} + +func SetSpanID(span trace.Span, sid trace.SpanID) { + sc := span.SpanContext() + sc = sc.WithSpanID(sid) + setSpanContext(span, sc) +} + +func cancelSpan(span trace.Span) { + sc := span.SpanContext() + if sc.IsSampled() { + sc = sc.WithTraceFlags(trace.TraceFlags(0)) // Remove the sampled flag + setSpanContext(span, sc) + } +} + +func setSpanContext(span trace.Span, sc trace.SpanContext) { + spanP := reflect.ValueOf(span) + spanV := reflect.Indirect(spanP) + field := spanV.FieldByName("spanContext") + + // noop or nonrecording spans store their spanContext in `sc`, but we ignore + // those for our purposes here. + if !field.IsValid() || field.IsZero() { + return + } + + setUnexportedField(field, sc) +} + +func setUnexportedField(field reflect.Value, value interface{}) { + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())). + Elem(). + Set(reflect.ValueOf(value)) +} diff --git a/internal/workflowstate/replaylogger.go b/internal/workflowstate/replaylogger.go index 922dc2a3..9a83902e 100644 --- a/internal/workflowstate/replaylogger.go +++ b/internal/workflowstate/replaylogger.go @@ -6,8 +6,8 @@ import ( ) type replayHandler struct { - state *WfState - handler slog.Handler + replayer Replayer + handler slog.Handler } // Enabled implements slog.Handler. @@ -17,7 +17,7 @@ func (rh *replayHandler) Enabled(ctx context.Context, level slog.Level) bool { // Handle implements slog.Handler. func (rh *replayHandler) Handle(ctx context.Context, r slog.Record) error { - if rh.state.Replaying() { + if rh.replayer.Replaying() { return nil } @@ -26,23 +26,27 @@ func (rh *replayHandler) Handle(ctx context.Context, r slog.Record) error { func (rh *replayHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &replayHandler{ - state: rh.state, - handler: rh.handler.WithAttrs(attrs), + replayer: rh.replayer, + handler: rh.handler.WithAttrs(attrs), } } // WithGroup implements slog.Handler. func (rh *replayHandler) WithGroup(name string) slog.Handler { return &replayHandler{ - state: rh.state, - handler: rh.handler.WithGroup(name), + replayer: rh.replayer, + handler: rh.handler.WithGroup(name), } } var _ slog.Handler = (*replayHandler)(nil) -func NewReplayLogger(state *WfState, logger *slog.Logger) *slog.Logger { +type Replayer interface { + Replaying() bool +} + +func NewReplayLogger(replayer Replayer, logger *slog.Logger) *slog.Logger { h := logger.Handler() - return slog.New(&replayHandler{state, h}) + return slog.New(&replayHandler{replayer, h}) } diff --git a/internal/workflowstate/replaylogger_test.go b/internal/workflowstate/replaylogger_test.go index b4250c32..fc36bbe1 100644 --- a/internal/workflowstate/replaylogger_test.go +++ b/internal/workflowstate/replaylogger_test.go @@ -8,11 +8,12 @@ import ( "github.com/cschleiden/go-workflows/core" "github.com/google/uuid" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" ) func Test_ReplayLogger_With(t *testing.T) { i := core.NewWorkflowInstance(uuid.NewString(), "") - wfState := NewWorkflowState(i, slog.Default(), clock.New()) + wfState := NewWorkflowState(i, slog.Default(), noop.NewTracerProvider().Tracer("test"), clock.New()) with := wfState.Logger().With(slog.String("foo", "bar")) require.IsType(t, &replayHandler{}, with.Handler()) @@ -20,7 +21,7 @@ func Test_ReplayLogger_With(t *testing.T) { func Test_ReplayLogger_WithGroup(t *testing.T) { i := core.NewWorkflowInstance(uuid.NewString(), "") - wfState := NewWorkflowState(i, slog.Default(), clock.New()) + wfState := NewWorkflowState(i, slog.Default(), noop.NewTracerProvider().Tracer("test"), clock.New()) with := wfState.Logger().WithGroup("group_name") require.IsType(t, &replayHandler{}, with.Handler()) diff --git a/internal/workflowstate/workflowstate.go b/internal/workflowstate/workflowstate.go index 53797dc7..7deff55b 100644 --- a/internal/workflowstate/workflowstate.go +++ b/internal/workflowstate/workflowstate.go @@ -12,6 +12,7 @@ import ( "github.com/cschleiden/go-workflows/internal/command" "github.com/cschleiden/go-workflows/internal/log" "github.com/cschleiden/go-workflows/internal/sync" + "go.opentelemetry.io/otel/trace" ) type key int @@ -63,12 +64,13 @@ type WfState struct { signalChannels map[string]*signalChannel logger *slog.Logger + tracer trace.Tracer clock clock.Clock time time.Time } -func NewWorkflowState(instance *core.WorkflowInstance, logger *slog.Logger, clock clock.Clock) *WfState { +func NewWorkflowState(instance *core.WorkflowInstance, logger *slog.Logger, tracer trace.Tracer, clock clock.Clock) *WfState { state := &WfState{ instance: instance, commands: []command.Command{}, @@ -78,6 +80,8 @@ func NewWorkflowState(instance *core.WorkflowInstance, logger *slog.Logger, cloc pendingSignals: map[string][]payload.Payload{}, signalChannels: make(map[string]*signalChannel), + tracer: tracer, + clock: clock, } @@ -169,3 +173,7 @@ func (wf *WfState) Instance() *core.WorkflowInstance { func (wf *WfState) Logger() *slog.Logger { return wf.logger } + +func (wf *WfState) Tracer() trace.Tracer { + return wf.tracer +} diff --git a/internal/workflowstate/workflowstate_test.go b/internal/workflowstate/workflowstate_test.go index b26a069a..1c1b1c0a 100644 --- a/internal/workflowstate/workflowstate_test.go +++ b/internal/workflowstate/workflowstate_test.go @@ -10,12 +10,13 @@ import ( "github.com/cschleiden/go-workflows/internal/sync" "github.com/google/uuid" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" ) func Test_PendingFutures(t *testing.T) { i := core.NewWorkflowInstance(uuid.NewString(), "") - wfState := NewWorkflowState(i, slog.Default(), clock.New()) + wfState := NewWorkflowState(i, slog.Default(), noop.NewTracerProvider().Tracer("test"), clock.New()) require.False(t, wfState.HasPendingFutures()) diff --git a/internal/workflowtracer/tracer.go b/internal/workflowtracer/tracer.go deleted file mode 100644 index 3f1b959e..00000000 --- a/internal/workflowtracer/tracer.go +++ /dev/null @@ -1,78 +0,0 @@ -package workflowtracer - -import ( - "context" - - "github.com/cschleiden/go-workflows/internal/sync" - "github.com/cschleiden/go-workflows/internal/workflowstate" - "go.opentelemetry.io/otel/trace" -) - -type tracerContextKeyType int - -const tracerKey tracerContextKeyType = iota - -func WithWorkflowTracer(ctx sync.Context, tracer *WorkflowTracer) sync.Context { - return sync.WithValue(ctx, tracerKey, tracer) -} - -func Tracer(ctx sync.Context) *WorkflowTracer { - if tracer, ok := ctx.Value(tracerKey).(*WorkflowTracer); ok { - return tracer - } - - panic("no tracer in context") -} - -type spanContextKeyType int - -const spanKey spanContextKeyType = iota - -func ContextWithSpan(ctx sync.Context, span trace.Span) sync.Context { - return sync.WithValue(ctx, spanKey, span) -} - -func SpanFromContext(ctx sync.Context) trace.Span { - if span, ok := ctx.Value(spanKey).(trace.Span); ok { - return span - } - - return nil -} - -type WorkflowTracer struct { - tracer trace.Tracer -} - -func New(tracer trace.Tracer) *WorkflowTracer { - return &WorkflowTracer{ - tracer: tracer, - } -} - -func (wt *WorkflowTracer) Start(ctx sync.Context, name string, opts ...trace.SpanStartOption) (sync.Context, Span) { - state := workflowstate.WorkflowState(ctx) - - sctx := trace.ContextWithSpan(context.Background(), SpanFromContext(ctx)) - opts = append(opts, trace.WithTimestamp(state.Time())) - sctx, span := wt.tracer.Start(sctx, name, opts...) - - if state.Replaying() { - sctx = trace.ContextWithSpanContext(sctx, span.SpanContext()) - span = trace.SpanFromContext(sctx) - } - - return ContextWithSpan(ctx, span), Span{span, state} -} - -type Span struct { - span trace.Span - state *workflowstate.WfState -} - -func (s *Span) End() { - if !s.state.Replaying() { - // Only end the trace when we are not replaying - s.span.End() - } -} diff --git a/samples/tracing/tracing.go b/samples/tracing/tracing.go index bbf362f8..b2cfa4cf 100644 --- a/samples/tracing/tracing.go +++ b/samples/tracing/tracing.go @@ -7,7 +7,10 @@ import ( "github.com/cschleiden/go-workflows/backend" "github.com/cschleiden/go-workflows/client" + "github.com/cschleiden/go-workflows/core" "github.com/cschleiden/go-workflows/samples" + "github.com/cschleiden/go-workflows/workflow" + "github.com/cschleiden/go-workflows/workflow/executor" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/resource" @@ -57,7 +60,10 @@ func main() { otel.SetTracerProvider(tp) - b := samples.GetBackend("tracing", backend.WithTracerProvider(tp), backend.WithStickyTimeout(0)) + b := samples.GetBackend("tracing", + backend.WithTracerProvider(tp), + backend.WithStickyTimeout(0), + ) // Run worker w := RunWorker(ctx, b) @@ -99,8 +105,35 @@ func runWorkflow(ctx context.Context, c *client.Client) { log.Println("Workflow finished. Result:", result) } +// Ensure we aren't caching for this sample +type noopWorkflowExecutorCache struct { +} + +var _ executor.Cache = (*noopWorkflowExecutorCache)(nil) + +// Get implements workflow.ExecutorCache +func (*noopWorkflowExecutorCache) Get(ctx context.Context, instance *core.WorkflowInstance) (executor.WorkflowExecutor, bool, error) { + return nil, false, nil +} + +// Evict implements workflow.ExecutorCache +func (*noopWorkflowExecutorCache) Evict(ctx context.Context, instance *core.WorkflowInstance) error { + return nil +} + +// StartEviction implements workflow.ExecutorCache +func (*noopWorkflowExecutorCache) StartEviction(ctx context.Context) { +} + +// Store implements workflow.ExecutorCache +func (*noopWorkflowExecutorCache) Store(ctx context.Context, instance *workflow.Instance, workflow executor.WorkflowExecutor) error { + return nil +} + func RunWorker(ctx context.Context, mb backend.Backend) *worker.Worker { - w := worker.New(mb, nil) + opt := worker.DefaultOptions + opt.WorkflowExecutorCache = &noopWorkflowExecutorCache{} + w := worker.New(mb, &opt) w.RegisterWorkflow(Workflow1) w.RegisterWorkflow(Subworkflow) diff --git a/tester/tester.go b/tester/tester.go index 00de2696..f5e944b0 100644 --- a/tester/tester.go +++ b/tester/tester.go @@ -31,6 +31,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/mock" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) type testHistoryProvider struct { @@ -203,7 +204,7 @@ func NewWorkflowTester[TResult any](workflow workflow.Workflow, opts ...Workflow o(options) } - tracer := trace.NewNoopTracerProvider().Tracer("workflow-tester") + tracer := noop.NewTracerProvider().Tracer("workflow-tester") wt := &workflowTester[TResult]{ options: options, diff --git a/workflow/activity.go b/workflow/activity.go index b6700a33..c42af83e 100644 --- a/workflow/activity.go +++ b/workflow/activity.go @@ -11,7 +11,6 @@ import ( "github.com/cschleiden/go-workflows/internal/log" "github.com/cschleiden/go-workflows/internal/sync" "github.com/cschleiden/go-workflows/internal/workflowstate" - "github.com/cschleiden/go-workflows/internal/workflowtracer" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) @@ -80,14 +79,20 @@ func executeActivity[TResult any](ctx Context, options ActivityOptions, attempt wfState.AddCommand(cmd) wfState.TrackFuture(scheduleEventID, workflowstate.AsDecodingSettable(cv, fmt.Sprintf("activity: %s", name), f)) - ctx, span := workflowtracer.Tracer(ctx).Start(ctx, + ctx, span := Tracer(ctx).Start(ctx, fmt.Sprintf("ExecuteActivity: %s", name), trace.WithAttributes( attribute.String(log.ActivityNameKey, name), attribute.Int64(log.ScheduleEventIDKey, scheduleEventID), attribute.Int(log.AttemptKey, attempt), )) - defer span.End() + + tf := sync.NewFuture[TResult]() + Go(ctx, func(ctx Context) { + r, err := f.Get(ctx) + span.End() + tf.Set(r, err) + }) // Handle cancellation if d := ctx.Done(); d != nil { @@ -103,5 +108,5 @@ func executeActivity[TResult any](ctx Context, options ActivityOptions, attempt } } - return f + return tf } diff --git a/workflow/activity_test.go b/workflow/activity_test.go index 42402b65..ca9b2068 100644 --- a/workflow/activity_test.go +++ b/workflow/activity_test.go @@ -10,9 +10,8 @@ import ( "github.com/cschleiden/go-workflows/internal/contextvalue" "github.com/cschleiden/go-workflows/internal/sync" "github.com/cschleiden/go-workflows/internal/workflowstate" - "github.com/cschleiden/go-workflows/internal/workflowtracer" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) func Test_executeActivity_ResultMismatch(t *testing.T) { @@ -24,9 +23,9 @@ func Test_executeActivity_ResultMismatch(t *testing.T) { ctx = contextvalue.WithConverter(ctx, converter.DefaultConverter) ctx = workflowstate.WithWorkflowState( ctx, - workflowstate.NewWorkflowState(core.NewWorkflowInstance("a", ""), slog.Default(), clock.New()), + workflowstate.NewWorkflowState( + core.NewWorkflowInstance("a", ""), slog.Default(), noop.NewTracerProvider().Tracer("test"), clock.New()), ) - ctx = workflowtracer.WithWorkflowTracer(ctx, workflowtracer.New(trace.NewNoopTracerProvider().Tracer("test"))) c := sync.NewCoroutine(ctx, func(ctx Context) error { f := executeActivity[string](ctx, DefaultActivityOptions, 1, a) @@ -48,9 +47,9 @@ func Test_executeActivity_ParamMismatch(t *testing.T) { ctx = contextvalue.WithConverter(ctx, converter.DefaultConverter) ctx = workflowstate.WithWorkflowState( ctx, - workflowstate.NewWorkflowState(core.NewWorkflowInstance("a", ""), slog.Default(), clock.New()), + workflowstate.NewWorkflowState( + core.NewWorkflowInstance("a", ""), slog.Default(), noop.NewTracerProvider().Tracer("test"), clock.New()), ) - ctx = workflowtracer.WithWorkflowTracer(ctx, workflowtracer.New(trace.NewNoopTracerProvider().Tracer("test"))) c := sync.NewCoroutine(ctx, func(ctx Context) error { f := executeActivity[int](ctx, DefaultActivityOptions, 1, a) diff --git a/workflow/executor/cache/cache_test.go b/workflow/executor/cache/cache_test.go index 4e364056..199a3a6a 100644 --- a/workflow/executor/cache/cache_test.go +++ b/workflow/executor/cache/cache_test.go @@ -18,7 +18,7 @@ import ( "github.com/cschleiden/go-workflows/workflow" "github.com/cschleiden/go-workflows/workflow/executor" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) func Test_Cache_StoreAndGet(t *testing.T) { @@ -29,14 +29,14 @@ func Test_Cache_StoreAndGet(t *testing.T) { i := core.NewWorkflowInstance("instanceID", "executionID") e, err := executor.NewExecutor( - slog.Default(), trace.NewNoopTracerProvider().Tracer(backend.TracerName), r, converter.DefaultConverter, + slog.Default(), noop.NewTracerProvider().Tracer(backend.TracerName), r, converter.DefaultConverter, []workflow.ContextPropagator{}, &testHistoryProvider{}, i, &metadata.WorkflowMetadata{}, clock.New(), ) require.NoError(t, err) i2 := core.NewWorkflowInstance("instanceID2", "executionID2") e2, err := executor.NewExecutor( - slog.Default(), trace.NewNoopTracerProvider().Tracer(backend.TracerName), r, converter.DefaultConverter, + slog.Default(), noop.NewTracerProvider().Tracer(backend.TracerName), r, converter.DefaultConverter, []workflow.ContextPropagator{}, &testHistoryProvider{}, i, &metadata.WorkflowMetadata{}, clock.New(), ) require.NoError(t, err) @@ -69,7 +69,7 @@ func Test_Cache_AutoEviction(t *testing.T) { r := registry.New() require.NoError(t, r.RegisterWorkflow(workflowWithActivity)) e, err := executor.NewExecutor( - slog.Default(), trace.NewNoopTracerProvider().Tracer(backend.TracerName), r, + slog.Default(), noop.NewTracerProvider().Tracer(backend.TracerName), r, converter.DefaultConverter, []workflow.ContextPropagator{}, &testHistoryProvider{}, i, &metadata.WorkflowMetadata{}, clock.New(), ) @@ -99,7 +99,7 @@ func Test_Cache_Evict(t *testing.T) { r := registry.New() require.NoError(t, r.RegisterWorkflow(workflowWithActivity)) e, err := executor.NewExecutor( - slog.Default(), trace.NewNoopTracerProvider().Tracer(backend.TracerName), r, + slog.Default(), noop.NewTracerProvider().Tracer(backend.TracerName), r, converter.DefaultConverter, []workflow.ContextPropagator{}, &testHistoryProvider{}, i, &metadata.WorkflowMetadata{}, clock.New(), ) diff --git a/workflow/executor/executor.go b/workflow/executor/executor.go index 96ee7cd5..5f020dbb 100644 --- a/workflow/executor/executor.go +++ b/workflow/executor/executor.go @@ -21,9 +21,9 @@ import ( "github.com/cschleiden/go-workflows/internal/continueasnew" "github.com/cschleiden/go-workflows/internal/log" "github.com/cschleiden/go-workflows/internal/sync" + "github.com/cschleiden/go-workflows/internal/tracing" "github.com/cschleiden/go-workflows/internal/workflowerrors" "github.com/cschleiden/go-workflows/internal/workflowstate" - "github.com/cschleiden/go-workflows/internal/workflowtracer" "github.com/cschleiden/go-workflows/registry" wf "github.com/cschleiden/go-workflows/workflow" "go.opentelemetry.io/otel/trace" @@ -61,7 +61,6 @@ type executor struct { historyProvider WorkflowHistoryProvider workflow *workflow workflowName string - workflowTracer *workflowtracer.WorkflowTracer workflowState *workflowstate.WfState workflowCtx sync.Context workflowCtxCancel sync.CancelFunc @@ -70,7 +69,9 @@ type executor struct { logger *slog.Logger tracer trace.Tracer lastSequenceID int64 - parentSpan trace.Span + + parentSpan trace.Span + workflowSpan trace.Span } func NewExecutor( @@ -84,17 +85,16 @@ func NewExecutor( metadata *metadata.WorkflowMetadata, clock clock.Clock, ) (WorkflowExecutor, error) { - s := workflowstate.NewWorkflowState(instance, logger, clock) - - wfTracer := workflowtracer.New(tracer) + s := workflowstate.NewWorkflowState(instance, logger, tracer, clock) wfCtx := sync.Background() wfCtx = contextvalue.WithConverter(wfCtx, cv) - wfCtx = workflowtracer.WithWorkflowTracer(wfCtx, wfTracer) wfCtx = workflowstate.WithWorkflowState(wfCtx, s) wfCtx = sync.WithValue(wfCtx, contextvalue.PropagatorsCtxKey, propagators) wfCtx, cancel := sync.WithCancel(wfCtx) + // As part of this, the default tracing propagator will run, and set the parent span + // in the context, which will be picked up by our workflow span later. for _, propagator := range propagators { var err error wfCtx, err = propagator.ExtractToWorkflow(wfCtx, metadata) @@ -108,13 +108,9 @@ func NewExecutor( slog.String(log.ExecutionIDKey, instance.ExecutionID), ) - // Get span from the workflow context, set by the default context propagator - parentSpan := workflowtracer.SpanFromContext(wfCtx) - return &executor{ registry: registry, historyProvider: historyProvider, - workflowTracer: wfTracer, workflowState: s, workflowCtx: wfCtx, workflowCtxCancel: cancel, @@ -122,7 +118,6 @@ func NewExecutor( clock: clock, logger: logger, tracer: tracer, - parentSpan: parentSpan, }, nil } @@ -131,7 +126,7 @@ func (e *executor) ExecuteTask(ctx context.Context, t *backend.WorkflowTask) (*E log.TaskIDKey, t.ID, ) - logger.Debug("Executing workflow task", log.TaskLastSequenceIDKey, t.LastSequenceID) + logger.Debug("Executing workflow task", slog.Int64(log.TaskLastSequenceIDKey, t.LastSequenceID)) if t.WorkflowInstanceState == core.WorkflowInstanceStateFinished { // This could happen if signals are delivered after the workflow is finished @@ -150,36 +145,9 @@ func (e *executor) ExecuteTask(ctx context.Context, t *backend.WorkflowTask) (*E }, nil } - skipNewEvents := false - - if t.LastSequenceID > e.lastSequenceID { - logger.Debug("Task has newer history than current state, fetching and replaying history", - log.TaskSequenceIDKey, t.LastSequenceID, - log.LocalSequenceIDKey, e.lastSequenceID) - - h, err := e.historyProvider.GetWorkflowInstanceHistory(ctx, t.WorkflowInstance, &e.lastSequenceID) - if err != nil { - return nil, fmt.Errorf("getting workflow history: %w", err) - } - - if err := e.replayHistory(h); err != nil { - logger.Error("Error while replaying history", "error", err) - - // Fail workflow with an error. Skip executing new events, but still go through the commands - e.workflowCompleted(nil, err) - skipNewEvents = true - - // With an error occurred during replay, we need to ensure new events don't get duplicate sequence ids - e.lastSequenceID = t.LastSequenceID - } else if t.LastSequenceID != e.lastSequenceID { - logger.Error("After replaying history, task still has newer history than current state", - log.TaskSequenceIDKey, t.LastSequenceID, - log.LocalSequenceIDKey, e.lastSequenceID) - - return nil, errors.New("even after fetching history and replaying history executor state does not match task") - } - } else if t.LastSequenceID < e.lastSequenceID { - return nil, fmt.Errorf("task has older history than current state, cannot execute") + skipNewEvents, err := e.catchupOnHistory(ctx, t, logger) + if err != nil { + return nil, err } // Always add a WorkflowTaskStarted event before executing new tasks @@ -249,6 +217,43 @@ func (e *executor) ExecuteTask(ctx context.Context, t *backend.WorkflowTask) (*E }, nil } +func (e *executor) catchupOnHistory(ctx context.Context, t *backend.WorkflowTask, logger *slog.Logger) (bool, error) { + if t.LastSequenceID < e.lastSequenceID { + return false, fmt.Errorf("task has older history than current state, cannot execute") + } + + if t.LastSequenceID > e.lastSequenceID { + logger.Debug("Task has newer history than current state, fetching and replaying history", + log.TaskSequenceIDKey, t.LastSequenceID, + log.LocalSequenceIDKey, e.lastSequenceID) + + h, err := e.historyProvider.GetWorkflowInstanceHistory(ctx, t.WorkflowInstance, &e.lastSequenceID) + if err != nil { + return false, fmt.Errorf("getting workflow history: %w", err) + } + + if err := e.replayHistory(h); err != nil { + logger.Error("Error while replaying history", "error", err) + + // Fail workflow with an error. Skip executing new events, but still go through the commands + e.workflowCompleted(nil, err) + + // With an error occurred during replay, we need to ensure new events don't get duplicate sequence ids + e.lastSequenceID = t.LastSequenceID + + return true, nil + } else if t.LastSequenceID != e.lastSequenceID { + logger.Error("After replaying history, task still has newer history than current state", + log.TaskSequenceIDKey, t.LastSequenceID, + log.LocalSequenceIDKey, e.lastSequenceID) + + return false, errors.New("even after fetching history and replaying history executor state does not match task") + } + } + + return false, nil +} + func (e *executor) replayHistory(h []*history.Event) error { e.workflowState.SetReplaying(true) for _, event := range h { @@ -278,6 +283,7 @@ func (e *executor) executeNewEvents(newEvents []*history.Event) ([]*history.Even if e.workflow.Completed() { if e.workflowState.HasPendingFutures() { + // This should not happen, provide debug information to the developer var pending []string pf := e.workflowState.PendingFutureNames() for id, name := range pf { @@ -332,7 +338,7 @@ func (e *executor) executeEvent(event *history.Event) error { switch event.Type { case history.EventType_WorkflowExecutionStarted: - err = e.handleWorkflowExecutionStarted(event.Attributes.(*history.ExecutionStartedAttributes)) + err = e.handleWorkflowExecutionStarted(event, event.Attributes.(*history.ExecutionStartedAttributes)) case history.EventType_WorkflowExecutionFinished: // Ignore @@ -376,6 +382,9 @@ func (e *executor) executeEvent(event *history.Event) error { case history.EventType_SubWorkflowCompleted: err = e.handleSubWorkflowCompleted(event, event.Attributes.(*history.SubWorkflowCompletedAttributes)) + case history.EventType_TraceStarted: + err = e.handleTraceStarted(event, event.Attributes.(*history.TraceStartedAttributes)) + default: return fmt.Errorf("unknown event type: %v", event.Type) } @@ -383,7 +392,7 @@ func (e *executor) executeEvent(event *history.Event) error { return err } -func (e *executor) handleWorkflowExecutionStarted(a *history.ExecutionStartedAttributes) error { +func (e *executor) handleWorkflowExecutionStarted(event *history.Event, a *history.ExecutionStartedAttributes) error { e.workflowName = a.Name wfFn, err := e.registry.GetWorkflow(a.Name) @@ -391,8 +400,22 @@ func (e *executor) handleWorkflowExecutionStarted(a *history.ExecutionStartedAtt return fmt.Errorf("workflow %s not found", a.Name) } - e.workflow = newWorkflow(reflect.ValueOf(wfFn)) + // Set the parent span here, so we can associate the workflow span with its parent + parentSpan := tracing.SpanFromContext(e.workflowCtx) + ctx := trace.ContextWithSpan(context.Background(), parentSpan) + + span := tracing.SpanWithStartTime( + ctx, + e.tracer, + tracing.WorkflowSpanName(e.workflowName), + a.WorkflowSpanID, + event.Timestamp) + // Set in context for workflow execution + e.workflowCtx = tracing.ContextWithSpan(e.workflowCtx, span) + e.workflowSpan = span + + e.workflow = newWorkflow(reflect.ValueOf(wfFn)) return e.workflow.Execute(e.workflowCtx, a.Inputs) } @@ -676,7 +699,34 @@ func (e *executor) handleSideEffectResult(event *history.Event, a *history.SideE } if err := f.Set(a.Result, nil); err != nil { - return fmt.Errorf("setting side effect result result: %w", err) + return fmt.Errorf("setting side effect result: %w", err) + } + + e.workflowState.RemoveFuture(event.ScheduleEventID) + + return e.workflow.Continue() +} + +func (e *executor) handleTraceStarted(event *history.Event, a *history.TraceStartedAttributes) error { + c := e.workflowState.CommandByScheduleEventID(event.ScheduleEventID) + if c == nil { + return fmt.Errorf("previous workflow execution started a trace") + } + + stc, ok := c.(*command.StartTraceCommand) + if !ok { + return fmt.Errorf("previous workflow execution started a trace, not: %v", c.Type()) + } + + stc.Done() + + f, ok := e.workflowState.FutureByScheduleEventID(event.ScheduleEventID) + if !ok { + return errors.New("no pending future found for start trace event") + } + + if err := f.Set(a.SpanID, nil); err != nil { + return fmt.Errorf("setting start trace spanID: %w", err) } e.workflowState.RemoveFuture(event.ScheduleEventID) @@ -689,6 +739,13 @@ func (e *executor) workflowCompleted(result payload.Payload, wfErr error) { cmd := command.NewCompleteWorkflowCommand(eventId, e.workflowState.Instance(), result, workflowerrors.FromError(wfErr)) e.workflowState.AddCommand(cmd) + + if e.workflowSpan != nil { + // End span that was created when starting the workflow + e.workflowSpan.End() + } else { + // TODO: this should not happen? We should always have a workflow span? + } } func (e *executor) workflowRestarted(result payload.Payload, continueAsNew *continueasnew.Error) { diff --git a/workflow/executor/executor_test.go b/workflow/executor/executor_test.go index bedf8f3f..946a70ec 100644 --- a/workflow/executor/executor_test.go +++ b/workflow/executor/executor_test.go @@ -23,7 +23,7 @@ import ( wf "github.com/cschleiden/go-workflows/workflow" "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" "go.uber.org/goleak" ) @@ -37,7 +37,7 @@ func (t *testHistoryProvider) GetWorkflowInstanceHistory(ctx context.Context, in func newExecutor(r *registry.Registry, i *core.WorkflowInstance, historyProvider WorkflowHistoryProvider) (*executor, error) { logger := slog.Default() - tracer := trace.NewNoopTracerProvider().Tracer("test") + tracer := noop.NewTracerProvider().Tracer("test") e, err := NewExecutor(logger, tracer, r, converter.DefaultConverter, []wf.ContextPropagator{}, historyProvider, i, &metadata.WorkflowMetadata{}, clock.New()) diff --git a/workflow/executor/workflow.go b/workflow/executor/workflow.go index e2f61bb7..ba54346c 100644 --- a/workflow/executor/workflow.go +++ b/workflow/executor/workflow.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "reflect" + "runtime/debug" "github.com/cschleiden/go-workflows/backend/payload" "github.com/cschleiden/go-workflows/internal/args" @@ -45,7 +46,9 @@ func (w *workflow) Execute(ctx sync.Context, inputs []payload.Payload) error { // Handle panics in workflows defer func() { if r := recover(); r != nil { - w.err = workflowerrors.NewPanicError(fmt.Sprintf("panic in workflow: %v", r)) + stack := string(debug.Stack()) + + w.err = workflowerrors.NewPanicError(fmt.Sprintf("panic in workflow: %v\n%v", r, stack)) } }() diff --git a/workflow/sideeffect.go b/workflow/sideeffect.go index 206cf98a..3e7974e7 100644 --- a/workflow/sideeffect.go +++ b/workflow/sideeffect.go @@ -5,7 +5,6 @@ import ( "github.com/cschleiden/go-workflows/internal/contextvalue" "github.com/cschleiden/go-workflows/internal/sync" "github.com/cschleiden/go-workflows/internal/workflowstate" - "github.com/cschleiden/go-workflows/internal/workflowtracer" ) // SideEffect executes the given function and returns a future that will be resolved with the result of @@ -14,7 +13,7 @@ import ( // In contrast to Activities, SideEffects are executed inline with the workflow code. They should only // be used for short and inexpensive operations. For longer operations, consider using an Activity. func SideEffect[TResult any](ctx Context, f func(ctx Context) TResult) Future[TResult] { - ctx, span := workflowtracer.Tracer(ctx).Start(ctx, "SideEffect") + ctx, span := Tracer(ctx).Start(ctx, "SideEffect") defer span.End() future := sync.NewFuture[TResult]() diff --git a/workflow/signal.go b/workflow/signal.go index 267ebac7..fe06dcd5 100644 --- a/workflow/signal.go +++ b/workflow/signal.go @@ -2,9 +2,11 @@ package workflow import ( "github.com/cschleiden/go-workflows/core" + "github.com/cschleiden/go-workflows/internal/log" "github.com/cschleiden/go-workflows/internal/signals" "github.com/cschleiden/go-workflows/internal/workflowstate" - "github.com/cschleiden/go-workflows/internal/workflowtracer" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) // NewSignalChannel returns a new signal channel. @@ -15,7 +17,11 @@ func NewSignalChannel[T any](ctx Context, name string) Channel[T] { // SignalWorkflow sends a signal to another running workflow instance. func SignalWorkflow[T any](ctx Context, instanceID string, name string, arg T) Future[any] { - ctx, span := workflowtracer.Tracer(ctx).Start(ctx, "SignalWorkflow") + ctx, span := Tracer(ctx).Start(ctx, "SignalWorkflow", + trace.WithAttributes( + attribute.String(log.SignalNameKey, name), + ), + ) defer span.End() var a *signals.Activities diff --git a/workflow/sleep.go b/workflow/sleep.go index 3dea88ee..66001757 100644 --- a/workflow/sleep.go +++ b/workflow/sleep.go @@ -4,14 +4,13 @@ import ( "time" "github.com/cschleiden/go-workflows/internal/log" - "github.com/cschleiden/go-workflows/internal/workflowtracer" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) // Sleep sleeps for the given duration. func Sleep(ctx Context, d time.Duration) error { - ctx, span := workflowtracer.Tracer(ctx).Start(ctx, "Sleep", + ctx, span := Tracer(ctx).Start(ctx, "Sleep", trace.WithAttributes(attribute.Int64(log.DurationKey, int64(d/time.Millisecond)))) defer span.End() diff --git a/workflow/subworkflow.go b/workflow/subworkflow.go index c4a9f6d4..241c27eb 100644 --- a/workflow/subworkflow.go +++ b/workflow/subworkflow.go @@ -10,10 +10,12 @@ import ( "github.com/cschleiden/go-workflows/internal/fn" "github.com/cschleiden/go-workflows/internal/log" "github.com/cschleiden/go-workflows/internal/sync" - "github.com/cschleiden/go-workflows/internal/workflowstate" - "github.com/cschleiden/go-workflows/internal/workflowtracer" + "github.com/cschleiden/go-workflows/internal/tracing" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + + // "github.com/cschleiden/go-workflows/internal/tracing" + "github.com/cschleiden/go-workflows/internal/workflowstate" ) type SubWorkflowOptions struct { @@ -81,14 +83,20 @@ func createSubWorkflowInstance[TResult any](ctx Context, options SubWorkflowOpti wfState := workflowstate.WorkflowState(ctx) scheduleEventID := wfState.GetNextScheduleEventID() - ctx, span := workflowtracer.Tracer(ctx).Start(ctx, + ctx, span := Tracer(ctx).Start(ctx, fmt.Sprintf("CreateSubworkflowInstance: %s", workflowName), trace.WithAttributes( attribute.String(log.WorkflowNameKey, workflowName), attribute.Int64(log.ScheduleEventIDKey, scheduleEventID), attribute.Int(log.AttemptKey, attempt), )) - defer span.End() + + tf := sync.NewFuture[TResult]() + Go(ctx, func(ctx Context) { + r, err := f.Get(ctx) + span.End() + tf.Set(r, err) + }) // Capture context propagators := propagators(ctx) @@ -98,7 +106,18 @@ func createSubWorkflowInstance[TResult any](ctx Context, options SubWorkflowOpti return f } - cmd := command.NewScheduleSubWorkflowCommand(scheduleEventID, wfState.Instance(), options.Queue, options.InstanceID, workflowName, inputs, metadata) + workflowSpanID := tracing.GetNewSpanIDWF(ctx) + + cmd := command.NewScheduleSubWorkflowCommand( + scheduleEventID, + wfState.Instance(), + options.Queue, + options.InstanceID, + workflowName, + inputs, + metadata, + workflowSpanID, + ) wfState.AddCommand(cmd) wfState.TrackFuture(scheduleEventID, workflowstate.AsDecodingSettable(cv, fmt.Sprintf("subworkflow:%s", workflowName), f)) @@ -127,5 +146,5 @@ func createSubWorkflowInstance[TResult any](ctx Context, options SubWorkflowOpti }) } - return f + return tf } diff --git a/workflow/subworkflow_test.go b/workflow/subworkflow_test.go index 01bcf8e6..fc73b2a0 100644 --- a/workflow/subworkflow_test.go +++ b/workflow/subworkflow_test.go @@ -10,9 +10,8 @@ import ( "github.com/cschleiden/go-workflows/internal/contextvalue" "github.com/cschleiden/go-workflows/internal/sync" "github.com/cschleiden/go-workflows/internal/workflowstate" - "github.com/cschleiden/go-workflows/internal/workflowtracer" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) func Test_createSubWorkflowInstance_NameAsString(t *testing.T) { @@ -20,9 +19,9 @@ func Test_createSubWorkflowInstance_NameAsString(t *testing.T) { ctx = contextvalue.WithConverter(ctx, converter.DefaultConverter) ctx = workflowstate.WithWorkflowState( ctx, - workflowstate.NewWorkflowState(core.NewWorkflowInstance("a", ""), slog.Default(), clock.New()), + workflowstate.NewWorkflowState( + core.NewWorkflowInstance("a", ""), slog.Default(), noop.NewTracerProvider().Tracer("test"), clock.New()), ) - ctx = workflowtracer.WithWorkflowTracer(ctx, workflowtracer.New(trace.NewNoopTracerProvider().Tracer("test"))) c := sync.NewCoroutine(ctx, func(ctx Context) error { createSubWorkflowInstance[int](ctx, DefaultSubWorkflowOptions, 1, "workflowName", "foo") @@ -43,9 +42,9 @@ func Test_createSubWorkflowInstance_ParamMismatch(t *testing.T) { ctx = contextvalue.WithConverter(ctx, converter.DefaultConverter) ctx = workflowstate.WithWorkflowState( ctx, - workflowstate.NewWorkflowState(core.NewWorkflowInstance("a", ""), slog.Default(), clock.New()), + workflowstate.NewWorkflowState( + core.NewWorkflowInstance("a", ""), slog.Default(), noop.NewTracerProvider().Tracer("test"), clock.New()), ) - ctx = workflowtracer.WithWorkflowTracer(ctx, workflowtracer.New(trace.NewNoopTracerProvider().Tracer("test"))) c := sync.NewCoroutine(ctx, func(ctx Context) error { f := createSubWorkflowInstance[int](ctx, DefaultSubWorkflowOptions, 1, wf, "foo") @@ -68,9 +67,8 @@ func Test_createSubWorkflowInstance_ReturnMismatch(t *testing.T) { ctx = contextvalue.WithConverter(ctx, converter.DefaultConverter) ctx = workflowstate.WithWorkflowState( ctx, - workflowstate.NewWorkflowState(core.NewWorkflowInstance("a", ""), slog.Default(), clock.New()), + workflowstate.NewWorkflowState(core.NewWorkflowInstance("a", ""), slog.Default(), noop.NewTracerProvider().Tracer("test"), clock.New()), ) - ctx = workflowtracer.WithWorkflowTracer(ctx, workflowtracer.New(trace.NewNoopTracerProvider().Tracer("test"))) c := sync.NewCoroutine(ctx, func(ctx Context) error { f := createSubWorkflowInstance[string](ctx, DefaultSubWorkflowOptions, 1, wf) diff --git a/workflow/timer.go b/workflow/timer.go index 51b83ec4..086bfe54 100644 --- a/workflow/timer.go +++ b/workflow/timer.go @@ -9,7 +9,6 @@ import ( "github.com/cschleiden/go-workflows/internal/log" "github.com/cschleiden/go-workflows/internal/sync" "github.com/cschleiden/go-workflows/internal/workflowstate" - "github.com/cschleiden/go-workflows/internal/workflowtracer" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) @@ -49,13 +48,18 @@ func ScheduleTimer(ctx Context, delay time.Duration) Future[any] { }, } - ctx, span := workflowtracer.Tracer(ctx).Start(ctx, "ScheduleTimer", + ctx, span := Tracer(ctx).Start(ctx, "ScheduleTimer", trace.WithAttributes( attribute.Int64(log.DurationKey, int64(delay/time.Millisecond)), attribute.String(log.NowKey, Now(ctx).String()), attribute.String(log.AtKey, at.String()), )) - defer span.End() + tf := sync.NewFuture[any]() + Go(ctx, func(ctx Context) { + r, err := f.Get(ctx) + span.End() + tf.Set(r, err) + }) // Check if the context is cancelable if c, cancelable := ctx.Done().(sync.CancelChannel); cancelable { @@ -68,5 +72,5 @@ func ScheduleTimer(ctx Context, delay time.Duration) Future[any] { }) } - return f + return tf } diff --git a/workflow/timer_test.go b/workflow/timer_test.go index 1843509c..a02df149 100644 --- a/workflow/timer_test.go +++ b/workflow/timer_test.go @@ -11,18 +11,17 @@ import ( "github.com/cschleiden/go-workflows/internal/contextvalue" "github.com/cschleiden/go-workflows/internal/sync" "github.com/cschleiden/go-workflows/internal/workflowstate" - "github.com/cschleiden/go-workflows/internal/workflowtracer" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) func Test_Timer_Cancellation(t *testing.T) { - state := workflowstate.NewWorkflowState(core.NewWorkflowInstance("a", ""), slog.Default(), clock.New()) + state := workflowstate.NewWorkflowState( + core.NewWorkflowInstance("a", ""), slog.Default(), noop.NewTracerProvider().Tracer("test"), clock.New()) ctx, cancel := WithCancel(sync.Background()) ctx = contextvalue.WithConverter(ctx, converter.DefaultConverter) ctx = workflowstate.WithWorkflowState(ctx, state) - ctx = workflowtracer.WithWorkflowTracer(ctx, workflowtracer.New(trace.NewNoopTracerProvider().Tracer("test"))) c := sync.NewCoroutine(ctx, func(ctx Context) error { f := ScheduleTimer(ctx, time.Second*1) diff --git a/workflow/tracer.go b/workflow/tracer.go index 79f15416..315c89c5 100644 --- a/workflow/tracer.go +++ b/workflow/tracer.go @@ -1,28 +1,99 @@ package workflow import ( - "github.com/cschleiden/go-workflows/internal/workflowtracer" + "context" + "log" + + "github.com/cschleiden/go-workflows/internal/command" + "github.com/cschleiden/go-workflows/internal/contextvalue" + "github.com/cschleiden/go-workflows/internal/sync" + "github.com/cschleiden/go-workflows/internal/tracing" + "github.com/cschleiden/go-workflows/internal/workflowstate" "go.opentelemetry.io/otel/trace" ) +type wfSpan struct { + span trace.Span + state *workflowstate.WfState +} + +func (s *wfSpan) End() { + if !s.state.Replaying() { + // Only end the trace when we are not replaying + s.span.End() + } else { + log.Println("Not ending span as we are replaying") + } +} + type Span interface { + // End ends the span. End() } // Tracer creates a the workflow tracer. func Tracer(ctx Context) *WorkflowTracer { - return &WorkflowTracer{ - t: workflowtracer.Tracer(ctx), - } + return &WorkflowTracer{} } type WorkflowTracer struct { - t *workflowtracer.WorkflowTracer } // Start starts a new span. func (wt *WorkflowTracer) Start(ctx Context, name string, opts ...trace.SpanStartOption) (Context, Span) { - ctx, span := wt.t.Start(ctx, name, opts...) + wfState := workflowstate.WorkflowState(ctx) + scheduleEventID := wfState.GetNextScheduleEventID() + + var span *wfSpan + + cmd := command.NewStartTraceCommand(scheduleEventID) + wfState.AddCommand(cmd) + + future := sync.NewFuture[[8]byte]() + + cv := contextvalue.Converter(ctx) + wfState.TrackFuture(scheduleEventID, workflowstate.AsDecodingSettable(cv, "startTrace", future)) + + if wfState.Replaying() { + // We need the spanID of the original span + spanID, _ := future.Get(ctx) + + // Use original timestamp + opts := append(opts, trace.WithTimestamp(Now(ctx))) + ctx, span = start(ctx, name, opts...) + + // Update underlying span with new spanID + tracing.SetSpanID(span.span, spanID) + } else { + ctx, span = start(ctx, name, opts...) + + // Declare as [8]byte to conver to payload + var spanID [8]byte = span.span.SpanContext().SpanID() + + payload, err := cv.To(spanID) + if err != nil { + future.Set([8]byte{}, err) + } + cmd.SetSpanID(payload) + + // Update span so our tracking doesn't report it + future.Set(spanID, nil) + wfState.RemoveFuture(scheduleEventID) + } + + return ctx, span +} + +func start(ctx Context, name string, opts ...trace.SpanStartOption) (sync.Context, *wfSpan) { + state := workflowstate.WorkflowState(ctx) + tracer := state.Tracer() + + // Set correct parent span + sctx := trace.ContextWithSpan(context.Background(), tracing.SpanFromContext(ctx)) + + // Use workflow timestamp + opts = append(opts, trace.WithTimestamp(state.Time())) + sctx, span := tracer.Start(sctx, name, opts...) - return ctx, &span + return tracing.ContextWithSpan(ctx, span), &wfSpan{span, state} } From 38feda9a7dd0e240bb8c78212847070f8f8a34a4 Mon Sep 17 00:00:00 2001 From: cschleiden Date: Tue, 22 Oct 2024 22:03:08 -0700 Subject: [PATCH 04/13] Upgrade mysql image --- docker-compose.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5de21147..3255380e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,13 +2,12 @@ version: '3.1' services: db: - image: mysql - command: --default-authentication-plugin=mysql_native_password + image: mysql:8.4 restart: always environment: MYSQL_ROOT_PASSWORD: root ports: - - "3306:3306" + - '3306:3306' redis: image: redis:6.2-alpine From 1789a1ce3a402b41a1a445abe5f69ffe84b563e5 Mon Sep 17 00:00:00 2001 From: cschleiden Date: Wed, 23 Oct 2024 22:19:19 -0700 Subject: [PATCH 05/13] Correctly trace timers and adjust subworkflows --- backend/history/timer_fired.go | 3 +- backend/test/e2e_tracing.go | 44 +++++++++++++++++++++++++++++- internal/command/schedule_timer.go | 7 +++-- workflow/executor/executor.go | 14 ++++++++++ workflow/subworkflow.go | 10 ++----- workflow/timer.go | 18 +----------- workflow/tracer.go | 3 -- 7 files changed, 67 insertions(+), 32 deletions(-) diff --git a/backend/history/timer_fired.go b/backend/history/timer_fired.go index 9b7e9270..86293a83 100644 --- a/backend/history/timer_fired.go +++ b/backend/history/timer_fired.go @@ -3,5 +3,6 @@ package history import "time" type TimerFiredAttributes struct { - At time.Time `json:"at,omitempty"` + ScheduledAt time.Time `json:"scheduled_at,omitempty"` + At time.Time `json:"at,omitempty"` } diff --git a/backend/test/e2e_tracing.go b/backend/test/e2e_tracing.go index 51f558e6..e0c26bbb 100644 --- a/backend/test/e2e_tracing.go +++ b/backend/test/e2e_tracing.go @@ -58,6 +58,43 @@ var e2eTracingTests = []backendTest{ ) }, }, + { + name: "Tracing/TimersHaveSpans", + f: func(t *testing.T, ctx context.Context, c *client.Client, w *worker.Worker, b TestBackend) { + exporter := setupTracing(b) + + wf := func(ctx workflow.Context) error { + workflow.ScheduleTimer(ctx, time.Millisecond*20).Get(ctx) + + return nil + } + register(t, ctx, w, []interface{}{wf}, nil) + + instance := runWorkflow(t, ctx, c, wf) + _, err := client.GetWorkflowResult[any](ctx, c, instance, time.Second*5) + require.NoError(t, err) + + spans := exporter.GetSpans().Snapshots() + + workflow1Span := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "Workflow: 1") + }) + require.NotNil(t, workflow1Span) + + timerSpan := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "Timer") + }) + require.NotNil(t, workflow1Span) + require.InEpsilon(t, time.Duration(20*time.Millisecond), + timerSpan.EndTime().Sub(timerSpan.StartTime())/time.Millisecond, + float64(5*time.Millisecond)) + + require.Equal(t, + workflow1Span.SpanContext().SpanID().String(), + timerSpan.Parent().SpanID().String(), + ) + }, + }, { name: "Tracing/SubWorkflowsHaveSpansWithCorrectParent", f: func(t *testing.T, ctx context.Context, c *client.Client, w *worker.Worker, b TestBackend) { @@ -96,13 +133,18 @@ var e2eTracingTests = []backendTest{ }) require.NotNil(t, workflow1Span) + createSWFSpan := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "CreateSubworkflowInstance: swf") + }) + require.NotNil(t, createSWFSpan) + workflow2Span := findSpan(spans, func(span trace.ReadOnlySpan) bool { return strings.Contains(span.Name(), "Workflow: swf") }) require.NotNil(t, workflow1Span) require.Equal(t, - workflow1Span.SpanContext().SpanID().String(), + createSWFSpan.SpanContext().SpanID().String(), workflow2Span.Parent().SpanID().String(), ) }, diff --git a/internal/command/schedule_timer.go b/internal/command/schedule_timer.go index 5874995e..aba52626 100644 --- a/internal/command/schedule_timer.go +++ b/internal/command/schedule_timer.go @@ -33,10 +33,12 @@ func (c *ScheduleTimerCommand) Execute(clock clock.Clock) *CommandResult { case CommandState_Pending: c.state = CommandState_Committed + now := clock.Now() + return &CommandResult{ Events: []*history.Event{ history.NewPendingEvent( - clock.Now(), + now, history.EventType_TimerScheduled, &history.TimerScheduledAttributes{ At: c.at, @@ -50,7 +52,8 @@ func (c *ScheduleTimerCommand) Execute(clock clock.Clock) *CommandResult { clock.Now(), history.EventType_TimerFired, &history.TimerFiredAttributes{ - At: c.at, + ScheduledAt: now, + At: c.at, }, history.ScheduleEventID(c.id), history.VisibleAt(c.at), diff --git a/workflow/executor/executor.go b/workflow/executor/executor.go index 5f020dbb..cbf75eb4 100644 --- a/workflow/executor/executor.go +++ b/workflow/executor/executor.go @@ -8,6 +8,7 @@ import ( "reflect" "slices" "testing" + "time" "github.com/benbjohnson/clock" "github.com/cschleiden/go-workflows/backend" @@ -26,6 +27,7 @@ import ( "github.com/cschleiden/go-workflows/internal/workflowstate" "github.com/cschleiden/go-workflows/registry" wf "github.com/cschleiden/go-workflows/workflow" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) @@ -530,6 +532,18 @@ func (e *executor) handleTimerFired(event *history.Event, a *history.TimerFiredA return nil } + if !e.workflowState.Replaying() { + // Trace timer + parentSpan := tracing.SpanFromContext(e.workflowCtx) + ctx := trace.ContextWithSpan(context.Background(), parentSpan) + _, span := e.tracer.Start(ctx, "Timer", trace.WithAttributes( + attribute.Int64(log.DurationKey, int64(a.At.Sub(a.ScheduledAt)/time.Millisecond)), + attribute.String(log.NowKey, a.ScheduledAt.String()), + attribute.String(log.AtKey, a.At.String()), + ), trace.WithTimestamp(a.ScheduledAt)) + span.End() + } + if err := f.Set(nil, nil); err != nil { return fmt.Errorf("setting timer fired result: %w", err) } diff --git a/workflow/subworkflow.go b/workflow/subworkflow.go index 241c27eb..e44219c1 100644 --- a/workflow/subworkflow.go +++ b/workflow/subworkflow.go @@ -90,13 +90,7 @@ func createSubWorkflowInstance[TResult any](ctx Context, options SubWorkflowOpti attribute.Int64(log.ScheduleEventIDKey, scheduleEventID), attribute.Int(log.AttemptKey, attempt), )) - - tf := sync.NewFuture[TResult]() - Go(ctx, func(ctx Context) { - r, err := f.Get(ctx) - span.End() - tf.Set(r, err) - }) + defer span.End() // Capture context propagators := propagators(ctx) @@ -146,5 +140,5 @@ func createSubWorkflowInstance[TResult any](ctx Context, options SubWorkflowOpti }) } - return tf + return f } diff --git a/workflow/timer.go b/workflow/timer.go index 086bfe54..c8e12101 100644 --- a/workflow/timer.go +++ b/workflow/timer.go @@ -6,11 +6,8 @@ import ( "github.com/cschleiden/go-workflows/internal/command" "github.com/cschleiden/go-workflows/internal/contextvalue" - "github.com/cschleiden/go-workflows/internal/log" "github.com/cschleiden/go-workflows/internal/sync" "github.com/cschleiden/go-workflows/internal/workflowstate" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" ) // ScheduleTimer schedules a timer to fire after the given delay. @@ -48,19 +45,6 @@ func ScheduleTimer(ctx Context, delay time.Duration) Future[any] { }, } - ctx, span := Tracer(ctx).Start(ctx, "ScheduleTimer", - trace.WithAttributes( - attribute.Int64(log.DurationKey, int64(delay/time.Millisecond)), - attribute.String(log.NowKey, Now(ctx).String()), - attribute.String(log.AtKey, at.String()), - )) - tf := sync.NewFuture[any]() - Go(ctx, func(ctx Context) { - r, err := f.Get(ctx) - span.End() - tf.Set(r, err) - }) - // Check if the context is cancelable if c, cancelable := ctx.Done().(sync.CancelChannel); cancelable { // Register a callback for when it's canceled. The only operation on the `Done` channel @@ -72,5 +56,5 @@ func ScheduleTimer(ctx Context, delay time.Duration) Future[any] { }) } - return tf + return f } diff --git a/workflow/tracer.go b/workflow/tracer.go index 315c89c5..a0711c26 100644 --- a/workflow/tracer.go +++ b/workflow/tracer.go @@ -2,7 +2,6 @@ package workflow import ( "context" - "log" "github.com/cschleiden/go-workflows/internal/command" "github.com/cschleiden/go-workflows/internal/contextvalue" @@ -21,8 +20,6 @@ func (s *wfSpan) End() { if !s.state.Replaying() { // Only end the trace when we are not replaying s.span.End() - } else { - log.Println("Not ending span as we are replaying") } } From 1569c4b756f89cf4985b9bcf9ccded1ac544972f Mon Sep 17 00:00:00 2001 From: cschleiden Date: Fri, 25 Oct 2024 21:55:18 -0700 Subject: [PATCH 06/13] Fix issues --- backend/history/timer_fired.go | 12 +++-- backend/history/timer_scheduled.go | 3 +- backend/test/e2e_tracing.go | 64 +++++++++++++++++++++---- internal/command/schedule_timer.go | 20 +++++--- internal/command/schedule_timer_test.go | 2 +- samples/tracing/workflow.go | 4 +- workflow/activity.go | 20 +------- workflow/executor/executor.go | 16 +++++-- workflow/retries.go | 1 + workflow/sleep.go | 10 +--- workflow/subworkflow.go | 12 ----- workflow/timer.go | 49 +++++++++++++++++-- 12 files changed, 145 insertions(+), 68 deletions(-) diff --git a/backend/history/timer_fired.go b/backend/history/timer_fired.go index 86293a83..96fb53bf 100644 --- a/backend/history/timer_fired.go +++ b/backend/history/timer_fired.go @@ -1,8 +1,14 @@ package history -import "time" +import ( + "time" + + "github.com/cschleiden/go-workflows/backend/metadata" +) type TimerFiredAttributes struct { - ScheduledAt time.Time `json:"scheduled_at,omitempty"` - At time.Time `json:"at,omitempty"` + ScheduledAt time.Time `json:"scheduled_at,omitempty"` + At time.Time `json:"at,omitempty"` + Name string `json:"name,omitempty"` + SpanMetadata metadata.WorkflowMetadata `json:"span_metadata,omitempty"` } diff --git a/backend/history/timer_scheduled.go b/backend/history/timer_scheduled.go index 7a14f196..da87af96 100644 --- a/backend/history/timer_scheduled.go +++ b/backend/history/timer_scheduled.go @@ -3,5 +3,6 @@ package history import "time" type TimerScheduledAttributes struct { - At time.Time `json:"at,omitempty"` + At time.Time `json:"at,omitempty"` + Name string `json:"name,omitempty"` } diff --git a/backend/test/e2e_tracing.go b/backend/test/e2e_tracing.go index e0c26bbb..a57f8387 100644 --- a/backend/test/e2e_tracing.go +++ b/backend/test/e2e_tracing.go @@ -66,6 +66,8 @@ var e2eTracingTests = []backendTest{ wf := func(ctx workflow.Context) error { workflow.ScheduleTimer(ctx, time.Millisecond*20).Get(ctx) + workflow.Sleep(ctx, time.Millisecond*10) + return nil } register(t, ctx, w, []interface{}{wf}, nil) @@ -88,6 +90,12 @@ var e2eTracingTests = []backendTest{ require.InEpsilon(t, time.Duration(20*time.Millisecond), timerSpan.EndTime().Sub(timerSpan.StartTime())/time.Millisecond, float64(5*time.Millisecond)) + require.Equal(t, "Timer", timerSpan.Name()) + + sleepSpan := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "Timer: Sleep") + }) + require.NotNil(t, sleepSpan) require.Equal(t, workflow1Span.SpanContext().SpanID().String(), @@ -95,6 +103,51 @@ var e2eTracingTests = []backendTest{ ) }, }, + { + name: "Tracing/TimersWithinCustomSpans", + f: func(t *testing.T, ctx context.Context, c *client.Client, w *worker.Worker, b TestBackend) { + exporter := setupTracing(b) + + wf := func(ctx workflow.Context) error { + ctx, span := workflow.Tracer(ctx).Start(ctx, "custom-span") + defer span.End() + + workflow.Sleep(ctx, time.Millisecond*10) + + return nil + } + register(t, ctx, w, []interface{}{wf}, nil) + + instance := runWorkflow(t, ctx, c, wf) + _, err := client.GetWorkflowResult[any](ctx, c, instance, time.Second*5) + require.NoError(t, err) + + spans := exporter.GetSpans().Snapshots() + + workflow1Span := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "Workflow: 1") + }) + require.NotNil(t, workflow1Span) + + customSpan := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "custom-span") + }) + require.NotNil(t, workflow1Span) + require.Equal(t, + workflow1Span.SpanContext().SpanID().String(), + customSpan.Parent().SpanID().String(), + ) + + sleepSpan := findSpan(spans, func(span trace.ReadOnlySpan) bool { + return strings.Contains(span.Name(), "Timer: Sleep") + }) + require.NotNil(t, sleepSpan) + require.Equal(t, + customSpan.SpanContext().SpanID().String(), + sleepSpan.Parent().SpanID().String(), + ) + }, + }, { name: "Tracing/SubWorkflowsHaveSpansWithCorrectParent", f: func(t *testing.T, ctx context.Context, c *client.Client, w *worker.Worker, b TestBackend) { @@ -133,19 +186,14 @@ var e2eTracingTests = []backendTest{ }) require.NotNil(t, workflow1Span) - createSWFSpan := findSpan(spans, func(span trace.ReadOnlySpan) bool { - return strings.Contains(span.Name(), "CreateSubworkflowInstance: swf") - }) - require.NotNil(t, createSWFSpan) - - workflow2Span := findSpan(spans, func(span trace.ReadOnlySpan) bool { + swfSpan := findSpan(spans, func(span trace.ReadOnlySpan) bool { return strings.Contains(span.Name(), "Workflow: swf") }) require.NotNil(t, workflow1Span) require.Equal(t, - createSWFSpan.SpanContext().SpanID().String(), - workflow2Span.Parent().SpanID().String(), + workflow1Span.SpanContext().SpanID().String(), + swfSpan.Parent().SpanID().String(), ) }, }, diff --git a/internal/command/schedule_timer.go b/internal/command/schedule_timer.go index aba52626..4faffafa 100644 --- a/internal/command/schedule_timer.go +++ b/internal/command/schedule_timer.go @@ -5,17 +5,20 @@ import ( "github.com/benbjohnson/clock" "github.com/cschleiden/go-workflows/backend/history" + "github.com/cschleiden/go-workflows/backend/metadata" ) type ScheduleTimerCommand struct { cancelableCommand - at time.Time + at time.Time + name string + spanMetadata metadata.WorkflowMetadata } var _ CancelableCommand = (*ScheduleTimerCommand)(nil) -func NewScheduleTimerCommand(id int64, at time.Time) *ScheduleTimerCommand { +func NewScheduleTimerCommand(id int64, at time.Time, name string, carrier metadata.WorkflowMetadata) *ScheduleTimerCommand { return &ScheduleTimerCommand{ cancelableCommand: cancelableCommand{ command: command{ @@ -24,7 +27,9 @@ func NewScheduleTimerCommand(id int64, at time.Time) *ScheduleTimerCommand { state: CommandState_Pending, }, }, - at: at, + at: at, + name: name, + spanMetadata: carrier, } } @@ -41,7 +46,8 @@ func (c *ScheduleTimerCommand) Execute(clock clock.Clock) *CommandResult { now, history.EventType_TimerScheduled, &history.TimerScheduledAttributes{ - At: c.at, + At: c.at, + Name: c.name, }, history.ScheduleEventID(c.id), ), @@ -52,8 +58,10 @@ func (c *ScheduleTimerCommand) Execute(clock clock.Clock) *CommandResult { clock.Now(), history.EventType_TimerFired, &history.TimerFiredAttributes{ - ScheduledAt: now, - At: c.at, + ScheduledAt: now, + At: c.at, + Name: c.name, + SpanMetadata: c.spanMetadata, }, history.ScheduleEventID(c.id), history.VisibleAt(c.at), diff --git a/internal/command/schedule_timer_test.go b/internal/command/schedule_timer_test.go index f06b2ec6..d90b0c22 100644 --- a/internal/command/schedule_timer_test.go +++ b/internal/command/schedule_timer_test.go @@ -89,7 +89,7 @@ func TestScheduleTimerCommand_StateTransitions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clock := clock.NewMock() - cmd := NewScheduleTimerCommand(1, clock.Now().Add(time.Second)) + cmd := NewScheduleTimerCommand(1, clock.Now().Add(time.Second), "", nil) tt.f(t, cmd, clock) }) diff --git a/samples/tracing/workflow.go b/samples/tracing/workflow.go index 36924fd7..ef1b78c7 100644 --- a/samples/tracing/workflow.go +++ b/samples/tracing/workflow.go @@ -22,10 +22,10 @@ func Workflow1(ctx workflow.Context, msg string, times int, inputs Inputs) (int, defer logger.Debug("Leaving Workflow1") tracer := workflow.Tracer(ctx) - ctx, span := tracer.Start(ctx, "Workflow1") + ctx, span := tracer.Start(ctx, "Workflow1 custom span") defer span.End() - _, customSpan := tracer.Start(ctx, "Workflow1 span", trace.WithAttributes( + _, customSpan := tracer.Start(ctx, "Workflow1 custom inner span", trace.WithAttributes( // Add additional attribute.String("msg", "hello world"), )) diff --git a/workflow/activity.go b/workflow/activity.go index c42af83e..365288de 100644 --- a/workflow/activity.go +++ b/workflow/activity.go @@ -8,11 +8,8 @@ import ( "github.com/cschleiden/go-workflows/internal/command" "github.com/cschleiden/go-workflows/internal/contextvalue" "github.com/cschleiden/go-workflows/internal/fn" - "github.com/cschleiden/go-workflows/internal/log" "github.com/cschleiden/go-workflows/internal/sync" "github.com/cschleiden/go-workflows/internal/workflowstate" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" ) type ActivityOptions struct { @@ -79,21 +76,6 @@ func executeActivity[TResult any](ctx Context, options ActivityOptions, attempt wfState.AddCommand(cmd) wfState.TrackFuture(scheduleEventID, workflowstate.AsDecodingSettable(cv, fmt.Sprintf("activity: %s", name), f)) - ctx, span := Tracer(ctx).Start(ctx, - fmt.Sprintf("ExecuteActivity: %s", name), - trace.WithAttributes( - attribute.String(log.ActivityNameKey, name), - attribute.Int64(log.ScheduleEventIDKey, scheduleEventID), - attribute.Int(log.AttemptKey, attempt), - )) - - tf := sync.NewFuture[TResult]() - Go(ctx, func(ctx Context) { - r, err := f.Get(ctx) - span.End() - tf.Set(r, err) - }) - // Handle cancellation if d := ctx.Done(); d != nil { if c, ok := d.(sync.ChannelInternal[struct{}]); ok { @@ -108,5 +90,5 @@ func executeActivity[TResult any](ctx Context, options ActivityOptions, attempt } } - return tf + return f } diff --git a/workflow/executor/executor.go b/workflow/executor/executor.go index cbf75eb4..af606609 100644 --- a/workflow/executor/executor.go +++ b/workflow/executor/executor.go @@ -28,6 +28,7 @@ import ( "github.com/cschleiden/go-workflows/registry" wf "github.com/cschleiden/go-workflows/workflow" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" ) @@ -449,7 +450,7 @@ func (e *executor) handleActivityScheduled(event *history.Event, a *history.Acti return fmt.Errorf("previous workflow execution scheduled different type of activity: %s, %s", a.Name, sac.Name) } - c.Commit() + sac.Commit() return nil } @@ -534,9 +535,16 @@ func (e *executor) handleTimerFired(event *history.Event, a *history.TimerFiredA if !e.workflowState.Replaying() { // Trace timer - parentSpan := tracing.SpanFromContext(e.workflowCtx) - ctx := trace.ContextWithSpan(context.Background(), parentSpan) - _, span := e.tracer.Start(ctx, "Timer", trace.WithAttributes( + spanName := "Timer" + if a.Name != "" { + spanName = spanName + ": " + a.Name + } + + sctx := context.Background() + if a.SpanMetadata != nil { + sctx = propagation.TraceContext{}.Extract(sctx, a.SpanMetadata) + } + _, span := e.tracer.Start(sctx, spanName, trace.WithAttributes( attribute.Int64(log.DurationKey, int64(a.At.Sub(a.ScheduledAt)/time.Millisecond)), attribute.String(log.NowKey, a.ScheduledAt.String()), attribute.String(log.AtKey, a.At.String()), diff --git a/workflow/retries.go b/workflow/retries.go index 0671cf02..f58062c9 100644 --- a/workflow/retries.go +++ b/workflow/retries.go @@ -87,6 +87,7 @@ func WithRetries[T any](ctx Context, retryOptions RetryOptions, fn func(ctx Cont break } + // TODO: Not trace this? if err := Sleep(ctx, backoffDuration); err != nil { r.Set(*new(T), err) return diff --git a/workflow/sleep.go b/workflow/sleep.go index 66001757..9b2a9186 100644 --- a/workflow/sleep.go +++ b/workflow/sleep.go @@ -2,19 +2,11 @@ package workflow import ( "time" - - "github.com/cschleiden/go-workflows/internal/log" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" ) // Sleep sleeps for the given duration. func Sleep(ctx Context, d time.Duration) error { - ctx, span := Tracer(ctx).Start(ctx, "Sleep", - trace.WithAttributes(attribute.Int64(log.DurationKey, int64(d/time.Millisecond)))) - defer span.End() - - _, err := ScheduleTimer(ctx, d).Get(ctx) + _, err := ScheduleTimer(ctx, d, WithTimerName("Sleep")).Get(ctx) return err } diff --git a/workflow/subworkflow.go b/workflow/subworkflow.go index e44219c1..b9cffad4 100644 --- a/workflow/subworkflow.go +++ b/workflow/subworkflow.go @@ -8,11 +8,8 @@ import ( "github.com/cschleiden/go-workflows/internal/command" "github.com/cschleiden/go-workflows/internal/contextvalue" "github.com/cschleiden/go-workflows/internal/fn" - "github.com/cschleiden/go-workflows/internal/log" "github.com/cschleiden/go-workflows/internal/sync" "github.com/cschleiden/go-workflows/internal/tracing" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" // "github.com/cschleiden/go-workflows/internal/tracing" "github.com/cschleiden/go-workflows/internal/workflowstate" @@ -83,15 +80,6 @@ func createSubWorkflowInstance[TResult any](ctx Context, options SubWorkflowOpti wfState := workflowstate.WorkflowState(ctx) scheduleEventID := wfState.GetNextScheduleEventID() - ctx, span := Tracer(ctx).Start(ctx, - fmt.Sprintf("CreateSubworkflowInstance: %s", workflowName), - trace.WithAttributes( - attribute.String(log.WorkflowNameKey, workflowName), - attribute.Int64(log.ScheduleEventIDKey, scheduleEventID), - attribute.Int(log.AttemptKey, attempt), - )) - defer span.End() - // Capture context propagators := propagators(ctx) metadata := &metadata.WorkflowMetadata{} diff --git a/workflow/timer.go b/workflow/timer.go index c8e12101..d783e301 100644 --- a/workflow/timer.go +++ b/workflow/timer.go @@ -1,17 +1,53 @@ package workflow import ( + "context" "fmt" "time" + "github.com/cschleiden/go-workflows/backend/metadata" "github.com/cschleiden/go-workflows/internal/command" "github.com/cschleiden/go-workflows/internal/contextvalue" "github.com/cschleiden/go-workflows/internal/sync" + "github.com/cschleiden/go-workflows/internal/tracing" "github.com/cschleiden/go-workflows/internal/workflowstate" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" ) +type timerConfig struct { + Name string +} + +type withNameOption struct { + Name string +} + +// applySpanEnd implements TimerOption. +func (w withNameOption) apply(tc timerConfig) timerConfig { + tc.Name = w.Name + return tc +} + +var _ timerOption = withNameOption{} + +type timerOption interface { + apply(timerConfig) timerConfig +} + +func WithTimerName(name string) timerOption { + return withNameOption{ + Name: name, + } +} + // ScheduleTimer schedules a timer to fire after the given delay. -func ScheduleTimer(ctx Context, delay time.Duration) Future[any] { +func ScheduleTimer(ctx Context, delay time.Duration, opts ...timerOption) Future[any] { + var timerConfig timerConfig + for _, opt := range opts { + timerConfig = opt.apply(timerConfig) + } + f := sync.NewFuture[any]() // If the context is already canceled, return immediately. @@ -21,11 +57,18 @@ func ScheduleTimer(ctx Context, delay time.Duration) Future[any] { } wfState := workflowstate.WorkflowState(ctx) - scheduleEventID := wfState.GetNextScheduleEventID() + at := Now(ctx).Add(delay) - timerCmd := command.NewScheduleTimerCommand(scheduleEventID, at) + // Capture parent span + span := tracing.SpanFromContext(ctx) + spanCtx := trace.ContextWithSpan(context.Background(), span) + + carrier := make(metadata.WorkflowMetadata) + propagation.TraceContext{}.Inject(spanCtx, carrier) + + timerCmd := command.NewScheduleTimerCommand(scheduleEventID, at, timerConfig.Name, carrier) wfState.AddCommand(timerCmd) wfState.TrackFuture(scheduleEventID, workflowstate.AsDecodingSettable(contextvalue.Converter(ctx), fmt.Sprintf("timer:%v", delay), f)) From 010f65efe030f3bb5c94f4bc8c9b97cd91c973bc Mon Sep 17 00:00:00 2001 From: cschleiden Date: Sat, 26 Oct 2024 21:35:53 -0700 Subject: [PATCH 07/13] - Trace continued as new - Trace timers with name --- backend/history/timer_fired.go | 10 ++--- client/client.go | 2 +- internal/activity/executor.go | 45 ++++++++++--------- internal/command/continueasnew.go | 21 ++++----- internal/command/schedule_timer.go | 10 ++--- internal/log/fields.go | 2 + internal/tracing/spanerror.go | 14 ++++++ internal/tracing/tracecontext.go | 43 ++++++++++++++++++ .../tracing/{workflowspan.go => tracing.go} | 14 +++--- samples/tracing/tracing.go | 1 + samples/tracing/workflow.go | 16 ++++++- workflow/executor/executor.go | 26 +++++------ workflow/retries.go | 4 +- workflow/subworkflow.go | 3 +- workflow/timer.go | 13 +----- workflow/tracer.go | 3 +- 16 files changed, 146 insertions(+), 81 deletions(-) create mode 100644 internal/tracing/spanerror.go create mode 100644 internal/tracing/tracecontext.go rename internal/tracing/{workflowspan.go => tracing.go} (78%) diff --git a/backend/history/timer_fired.go b/backend/history/timer_fired.go index 96fb53bf..445dde75 100644 --- a/backend/history/timer_fired.go +++ b/backend/history/timer_fired.go @@ -3,12 +3,12 @@ package history import ( "time" - "github.com/cschleiden/go-workflows/backend/metadata" + "github.com/cschleiden/go-workflows/internal/tracing" ) type TimerFiredAttributes struct { - ScheduledAt time.Time `json:"scheduled_at,omitempty"` - At time.Time `json:"at,omitempty"` - Name string `json:"name,omitempty"` - SpanMetadata metadata.WorkflowMetadata `json:"span_metadata,omitempty"` + ScheduledAt time.Time `json:"scheduled_at,omitempty"` + At time.Time `json:"at,omitempty"` + Name string `json:"name,omitempty"` + TraceContext tracing.Context `json:"span_metadata,omitempty"` } diff --git a/client/client.go b/client/client.go index 71bcf3bd..d3c9ba75 100644 --- a/client/client.go +++ b/client/client.go @@ -87,7 +87,7 @@ func (c *Client) CreateWorkflowInstance(ctx context.Context, options WorkflowIns attribute.String(log.InstanceIDKey, wfi.InstanceID), attribute.String(log.ExecutionIDKey, wfi.ExecutionID), attribute.String(log.WorkflowNameKey, workflowName), - )) + ), trace.WithSpanKind(trace.SpanKindProducer)) defer span.End() // Inject state from any propagators diff --git a/internal/activity/executor.go b/internal/activity/executor.go index 1942d9ef..e4330921 100644 --- a/internal/activity/executor.go +++ b/internal/activity/executor.go @@ -13,6 +13,7 @@ import ( "github.com/cschleiden/go-workflows/backend/payload" "github.com/cschleiden/go-workflows/internal/args" "github.com/cschleiden/go-workflows/internal/log" + "github.com/cschleiden/go-workflows/internal/tracing" "github.com/cschleiden/go-workflows/internal/workflowerrors" "github.com/cschleiden/go-workflows/registry" wf "github.com/cschleiden/go-workflows/workflow" @@ -47,21 +48,6 @@ func NewExecutor( func (e *Executor) ExecuteActivity(ctx context.Context, task *backend.ActivityTask) (payload.Payload, error) { a := task.Event.Attributes.(*history.ActivityScheduledAttributes) - activity, err := e.r.GetActivity(a.Name) - if err != nil { - return nil, workflowerrors.NewPermanentError(fmt.Errorf("activity not found: %w", err)) - } - - activityFn := reflect.ValueOf(activity) - if activityFn.Type().Kind() != reflect.Func { - return nil, workflowerrors.NewPermanentError(errors.New("activity not a function")) - } - - args, addContext, err := args.InputsToArgs(e.converter, activityFn, a.Inputs) - if err != nil { - return nil, workflowerrors.NewPermanentError(fmt.Errorf("converting activity inputs: %w", err)) - } - // Add activity state to context as := NewActivityState( task.Event.ID, @@ -71,6 +57,7 @@ func (e *Executor) ExecuteActivity(ctx context.Context, task *backend.ActivityTa activityCtx := WithActivityState(ctx, as) for _, propagator := range e.propagators { + var err error activityCtx, err = propagator.Extract(activityCtx, a.Metadata) if err != nil { return nil, workflowerrors.NewPermanentError(fmt.Errorf("extracting context from propagator: %w", err)) @@ -83,6 +70,22 @@ func (e *Executor) ExecuteActivity(ctx context.Context, task *backend.ActivityTa attribute.String(log.ActivityIDKey, task.ID), attribute.Int(log.AttemptKey, a.Attempt), )) + + activity, err := e.r.GetActivity(a.Name) + if err != nil { + return nil, workflowerrors.NewPermanentError(tracing.WithSpanError(span, fmt.Errorf("activity not found: %w", err))) + } + + activityFn := reflect.ValueOf(activity) + if activityFn.Type().Kind() != reflect.Func { + return nil, workflowerrors.NewPermanentError(tracing.WithSpanError(span, errors.New("activity not a function"))) + } + + args, addContext, err := args.InputsToArgs(e.converter, activityFn, a.Inputs) + if err != nil { + return nil, workflowerrors.NewPermanentError(tracing.WithSpanError(span, fmt.Errorf("converting activity inputs: %w", err))) + } + defer span.End() // Execute activity @@ -97,7 +100,7 @@ func (e *Executor) ExecuteActivity(ctx context.Context, task *backend.ActivityTa // Recover any panic encountered during activity execution defer func() { if r := recover(); r != nil { - err = workflowerrors.NewPanicError(fmt.Sprintf("panic: %v", r)) + err := workflowerrors.NewPanicError(fmt.Sprintf("panic: %v", r)) rv = []reflect.Value{reflect.ValueOf(err)} } @@ -110,7 +113,8 @@ func (e *Executor) ExecuteActivity(ctx context.Context, task *backend.ActivityTa <-done if len(rv) < 1 || len(rv) > 2 { - return nil, workflowerrors.NewPermanentError(errors.New("activity has to return either (error) or (, error)")) + return nil, workflowerrors.NewPermanentError( + tracing.WithSpanError(span, errors.New("activity has to return either (error) or (, error)"))) } var result payload.Payload @@ -120,7 +124,7 @@ func (e *Executor) ExecuteActivity(ctx context.Context, task *backend.ActivityTa var err error result, err = e.converter.To(rv[0].Interface()) if err != nil { - return nil, workflowerrors.NewPermanentError(fmt.Errorf("converting activity result: %w", err)) + return nil, workflowerrors.NewPermanentError(tracing.WithSpanError(span, fmt.Errorf("converting activity result: %w", err))) } } @@ -133,8 +137,9 @@ func (e *Executor) ExecuteActivity(ctx context.Context, task *backend.ActivityTa err, ok := errResult.Interface().(error) if !ok { - return nil, workflowerrors.NewPermanentError(fmt.Errorf("activity error result does not satisfy error interface (%T): %v", errResult, errResult)) + return nil, workflowerrors.NewPermanentError( + tracing.WithSpanError(span, fmt.Errorf("activity error result does not satisfy error interface (%T): %v", errResult, errResult))) } - return result, workflowerrors.FromError(err) + return result, workflowerrors.FromError(tracing.WithSpanError(span, err)) } diff --git a/internal/command/continueasnew.go b/internal/command/continueasnew.go index a11cce78..71f310e9 100644 --- a/internal/command/continueasnew.go +++ b/internal/command/continueasnew.go @@ -17,6 +17,8 @@ type ContinueAsNewCommand struct { Metadata *metadata.WorkflowMetadata Inputs []payload.Payload Result payload.Payload + + ContinuedExecutionID string } var _ Command = (*ContinueAsNewCommand)(nil) @@ -28,27 +30,26 @@ func NewContinueAsNewCommand(id int64, instance *core.WorkflowInstance, result p name: "ContinueAsNew", state: CommandState_Pending, }, - Instance: instance, - Name: name, - Metadata: metadata, - Inputs: inputs, - Result: result, + Instance: instance, + Name: name, + Metadata: metadata, + Inputs: inputs, + Result: result, + ContinuedExecutionID: uuid.NewString(), } } func (c *ContinueAsNewCommand) Execute(clock clock.Clock) *CommandResult { switch c.state { case CommandState_Pending: - continuedExecutionID := uuid.NewString() - var continuedInstance *core.WorkflowInstance if c.Instance.SubWorkflow() { // If the current workflow execution was a sub-workflow, ensure the new workflow execution is also a sub-workflow. // This will guarantee that the finished event for the new execution will be delivered to the right parent instance continuedInstance = core.NewSubWorkflowInstance( - c.Instance.InstanceID, continuedExecutionID, c.Instance.Parent, c.Instance.ParentEventID) + c.Instance.InstanceID, c.ContinuedExecutionID, c.Instance.Parent, c.Instance.ParentEventID) } else { - continuedInstance = core.NewWorkflowInstance(c.Instance.InstanceID, continuedExecutionID) + continuedInstance = core.NewWorkflowInstance(c.Instance.InstanceID, c.ContinuedExecutionID) } c.state = CommandState_Committed @@ -61,7 +62,7 @@ func (c *ContinueAsNewCommand) Execute(clock clock.Clock) *CommandResult { history.EventType_WorkflowExecutionContinuedAsNew, &history.ExecutionContinuedAsNewAttributes{ Result: c.Result, - ContinuedExecutionID: continuedExecutionID, + ContinuedExecutionID: c.ContinuedExecutionID, }, ), }, diff --git a/internal/command/schedule_timer.go b/internal/command/schedule_timer.go index 4faffafa..f516f3f3 100644 --- a/internal/command/schedule_timer.go +++ b/internal/command/schedule_timer.go @@ -5,7 +5,7 @@ import ( "github.com/benbjohnson/clock" "github.com/cschleiden/go-workflows/backend/history" - "github.com/cschleiden/go-workflows/backend/metadata" + "github.com/cschleiden/go-workflows/internal/tracing" ) type ScheduleTimerCommand struct { @@ -13,12 +13,12 @@ type ScheduleTimerCommand struct { at time.Time name string - spanMetadata metadata.WorkflowMetadata + traceContext tracing.Context } var _ CancelableCommand = (*ScheduleTimerCommand)(nil) -func NewScheduleTimerCommand(id int64, at time.Time, name string, carrier metadata.WorkflowMetadata) *ScheduleTimerCommand { +func NewScheduleTimerCommand(id int64, at time.Time, name string, traceContext tracing.Context) *ScheduleTimerCommand { return &ScheduleTimerCommand{ cancelableCommand: cancelableCommand{ command: command{ @@ -29,7 +29,7 @@ func NewScheduleTimerCommand(id int64, at time.Time, name string, carrier metada }, at: at, name: name, - spanMetadata: carrier, + traceContext: traceContext, } } @@ -61,7 +61,7 @@ func (c *ScheduleTimerCommand) Execute(clock clock.Clock) *CommandResult { ScheduledAt: now, At: c.at, Name: c.name, - SpanMetadata: c.spanMetadata, + TraceContext: c.traceContext, }, history.ScheduleEventID(c.id), history.VisibleAt(c.at), diff --git a/internal/log/fields.go b/internal/log/fields.go index f945e10b..275df3af 100644 --- a/internal/log/fields.go +++ b/internal/log/fields.go @@ -10,6 +10,8 @@ const ( InstanceIDKey = NamespaceKey + ".instance.id" ExecutionIDKey = NamespaceKey + ".execution.id" + ContinuedExecutionIDKey = NamespaceKey + ".continued_execution.id" + WorkflowNameKey = NamespaceKey + ".workflow.name" SignalNameKey = NamespaceKey + ".signal.name" diff --git a/internal/tracing/spanerror.go b/internal/tracing/spanerror.go new file mode 100644 index 00000000..ec302ef1 --- /dev/null +++ b/internal/tracing/spanerror.go @@ -0,0 +1,14 @@ +package tracing + +import ( + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +func WithSpanError(span trace.Span, err error) error { + if err != nil { + span.SetStatus(codes.Error, err.Error()) + } + + return err +} diff --git a/internal/tracing/tracecontext.go b/internal/tracing/tracecontext.go new file mode 100644 index 00000000..daf8809b --- /dev/null +++ b/internal/tracing/tracecontext.go @@ -0,0 +1,43 @@ +package tracing + +import ( + "context" + + "github.com/cschleiden/go-workflows/internal/sync" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +type Context map[string]string + +func (wim Context) Get(key string) string { + return wim[key] +} + +func (wim Context) Set(key string, value string) { + wim[key] = value +} + +func (wim Context) Keys() []string { + r := make([]string, 0, len(wim)) + + for k := range wim { + r = append(r, k) + } + + return r +} + +var propagator propagation.TraceContext + +func ContextFromWfCtx(ctx sync.Context) Context { + span := SpanFromContext(ctx) + spanCtx := trace.ContextWithSpan(context.Background(), span) + carrier := make(Context) + propagator.Inject(spanCtx, carrier) + return carrier +} + +func SpanContextFromContext(ctx context.Context, tctx Context) context.Context { + return propagator.Extract(ctx, tctx) +} diff --git a/internal/tracing/workflowspan.go b/internal/tracing/tracing.go similarity index 78% rename from internal/tracing/workflowspan.go rename to internal/tracing/tracing.go index 67c1d206..cb21992b 100644 --- a/internal/tracing/workflowspan.go +++ b/internal/tracing/tracing.go @@ -6,15 +6,16 @@ import ( "time" "unsafe" - "github.com/cschleiden/go-workflows/internal/sync" - "github.com/cschleiden/go-workflows/internal/workflowstate" "go.opentelemetry.io/otel/trace" ) -func SpanWithStartTime(ctx context.Context, tracer trace.Tracer, name string, spanID trace.SpanID, startTime time.Time) trace.Span { +func SpanWithStartTime( + ctx context.Context, tracer trace.Tracer, name string, spanID trace.SpanID, startTime time.Time, opts ...trace.SpanStartOption) trace.Span { + + opts = append(opts, trace.WithTimestamp(startTime), trace.WithSpanKind(trace.SpanKindConsumer)) _, span := tracer.Start(ctx, name, - trace.WithTimestamp(startTime), + opts..., ) SetSpanID(span, spanID) @@ -33,11 +34,6 @@ func GetNewSpanID(tracer trace.Tracer) trace.SpanID { return span.SpanContext().SpanID() } -func GetNewSpanIDWF(ctx sync.Context) trace.SpanID { - tracer := workflowstate.WorkflowState(ctx).Tracer() - return GetNewSpanID(tracer) -} - func SetSpanID(span trace.Span, sid trace.SpanID) { sc := span.SpanContext() sc = sc.WithSpanID(sid) diff --git a/samples/tracing/tracing.go b/samples/tracing/tracing.go index b2cfa4cf..c2d94b76 100644 --- a/samples/tracing/tracing.go +++ b/samples/tracing/tracing.go @@ -139,6 +139,7 @@ func RunWorker(ctx context.Context, mb backend.Backend) *worker.Worker { w.RegisterWorkflow(Subworkflow) w.RegisterActivity(Activity1) + w.RegisterActivity(RetriedActivity) if err := w.Start(ctx); err != nil { panic("could not start worker") diff --git a/samples/tracing/workflow.go b/samples/tracing/workflow.go index ef1b78c7..3544649d 100644 --- a/samples/tracing/workflow.go +++ b/samples/tracing/workflow.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "time" "github.com/cschleiden/go-workflows/activity" @@ -46,9 +47,9 @@ func Workflow1(ctx workflow.Context, msg string, times int, inputs Inputs) (int, workflow.NewSignalChannel[string](ctx, "test-signal").Receive(ctx) span.End() - r1, _ := workflow.ExecuteActivity[int](ctx, workflow.DefaultActivityOptions, Activity1, 35, 12).Get(ctx) + r1, err := workflow.ExecuteActivity[int](ctx, workflow.DefaultActivityOptions, RetriedActivity, 35, 12).Get(ctx) - return r1, nil + return r1, err } func Subworkflow(ctx workflow.Context) error { @@ -74,3 +75,14 @@ func Activity1(ctx context.Context, a, b int) (int, error) { return a + b, nil } + +func RetriedActivity(ctx context.Context, a, b int) (int, error) { + logger := activity.Logger(ctx) + + if activity.Attempt(ctx) < 1 { + logger.Info("Simulating failure") + return 0, errors.New("simulated failure") + } + + return a + b, nil +} diff --git a/workflow/executor/executor.go b/workflow/executor/executor.go index af606609..e2e72455 100644 --- a/workflow/executor/executor.go +++ b/workflow/executor/executor.go @@ -28,7 +28,6 @@ import ( "github.com/cschleiden/go-workflows/registry" wf "github.com/cschleiden/go-workflows/workflow" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" ) @@ -285,6 +284,8 @@ func (e *executor) executeNewEvents(newEvents []*history.Event) ([]*history.Even } if e.workflow.Completed() { + defer e.workflowSpan.End() + if e.workflowState.HasPendingFutures() { // This should not happen, provide debug information to the developer var pending []string @@ -298,7 +299,8 @@ func (e *executor) executeNewEvents(newEvents []*history.Event) ([]*history.Even panic(fmt.Sprintf("workflow completed, but there are still pending futures: %s", pending)) } - return newEvents, fmt.Errorf("workflow completed, but there are still pending futures: %s", pending) + return newEvents, tracing.WithSpanError( + e.workflowSpan, fmt.Errorf("workflow completed, but there are still pending futures: %s", pending)) } if canErr, ok := e.workflow.Error().(*continueasnew.Error); ok { @@ -534,15 +536,15 @@ func (e *executor) handleTimerFired(event *history.Event, a *history.TimerFiredA } if !e.workflowState.Replaying() { - // Trace timer + // Trace timer fired spanName := "Timer" if a.Name != "" { spanName = spanName + ": " + a.Name } sctx := context.Background() - if a.SpanMetadata != nil { - sctx = propagation.TraceContext{}.Extract(sctx, a.SpanMetadata) + if a.TraceContext != nil { + sctx = tracing.SpanContextFromContext(sctx, a.TraceContext) } _, span := e.tracer.Start(sctx, spanName, trace.WithAttributes( attribute.Int64(log.DurationKey, int64(a.At.Sub(a.ScheduledAt)/time.Millisecond)), @@ -761,20 +763,18 @@ func (e *executor) workflowCompleted(result payload.Payload, wfErr error) { cmd := command.NewCompleteWorkflowCommand(eventId, e.workflowState.Instance(), result, workflowerrors.FromError(wfErr)) e.workflowState.AddCommand(cmd) - - if e.workflowSpan != nil { - // End span that was created when starting the workflow - e.workflowSpan.End() - } else { - // TODO: this should not happen? We should always have a workflow span? - } } func (e *executor) workflowRestarted(result payload.Payload, continueAsNew *continueasnew.Error) { eventId := e.workflowState.GetNextScheduleEventID() - cmd := command.NewContinueAsNewCommand(eventId, e.workflowState.Instance(), result, e.workflowName, continueAsNew.Metadata, continueAsNew.Inputs) + cmd := command.NewContinueAsNewCommand( + eventId, e.workflowState.Instance(), result, e.workflowName, continueAsNew.Metadata, continueAsNew.Inputs) e.workflowState.AddCommand(cmd) + + e.workflowSpan.SetAttributes( + attribute.String(log.ContinuedExecutionIDKey, cmd.ContinuedExecutionID), + ) } func (e *executor) nextSequenceID() int64 { diff --git a/workflow/retries.go b/workflow/retries.go index f58062c9..fc028c7b 100644 --- a/workflow/retries.go +++ b/workflow/retries.go @@ -87,8 +87,8 @@ func WithRetries[T any](ctx Context, retryOptions RetryOptions, fn func(ctx Cont break } - // TODO: Not trace this? - if err := Sleep(ctx, backoffDuration); err != nil { + // Wait before next attempt + if _, err := ScheduleTimer(ctx, backoffDuration, WithTimerName("Retry-Backoff")).Get(ctx); err != nil { r.Set(*new(T), err) return } diff --git a/workflow/subworkflow.go b/workflow/subworkflow.go index b9cffad4..8a5454a8 100644 --- a/workflow/subworkflow.go +++ b/workflow/subworkflow.go @@ -88,7 +88,8 @@ func createSubWorkflowInstance[TResult any](ctx Context, options SubWorkflowOpti return f } - workflowSpanID := tracing.GetNewSpanIDWF(ctx) + tracer := workflowstate.WorkflowState(ctx).Tracer() + workflowSpanID := tracing.GetNewSpanID(tracer) cmd := command.NewScheduleSubWorkflowCommand( scheduleEventID, diff --git a/workflow/timer.go b/workflow/timer.go index d783e301..76241f1b 100644 --- a/workflow/timer.go +++ b/workflow/timer.go @@ -1,18 +1,14 @@ package workflow import ( - "context" "fmt" "time" - "github.com/cschleiden/go-workflows/backend/metadata" "github.com/cschleiden/go-workflows/internal/command" "github.com/cschleiden/go-workflows/internal/contextvalue" "github.com/cschleiden/go-workflows/internal/sync" "github.com/cschleiden/go-workflows/internal/tracing" "github.com/cschleiden/go-workflows/internal/workflowstate" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" ) type timerConfig struct { @@ -61,14 +57,9 @@ func ScheduleTimer(ctx Context, delay time.Duration, opts ...timerOption) Future at := Now(ctx).Add(delay) - // Capture parent span - span := tracing.SpanFromContext(ctx) - spanCtx := trace.ContextWithSpan(context.Background(), span) + traceContext := tracing.ContextFromWfCtx(ctx) - carrier := make(metadata.WorkflowMetadata) - propagation.TraceContext{}.Inject(spanCtx, carrier) - - timerCmd := command.NewScheduleTimerCommand(scheduleEventID, at, timerConfig.Name, carrier) + timerCmd := command.NewScheduleTimerCommand(scheduleEventID, at, timerConfig.Name, traceContext) wfState.AddCommand(timerCmd) wfState.TrackFuture(scheduleEventID, workflowstate.AsDecodingSettable(contextvalue.Converter(ctx), fmt.Sprintf("timer:%v", delay), f)) diff --git a/workflow/tracer.go b/workflow/tracer.go index a0711c26..00042bec 100644 --- a/workflow/tracer.go +++ b/workflow/tracer.go @@ -64,9 +64,8 @@ func (wt *WorkflowTracer) Start(ctx Context, name string, opts ...trace.SpanStar } else { ctx, span = start(ctx, name, opts...) - // Declare as [8]byte to conver to payload + // Declare as [8]byte to convert to payload var spanID [8]byte = span.span.SpanContext().SpanID() - payload, err := cv.To(spanID) if err != nil { future.Set([8]byte{}, err) From 9031064840d0f249b5f0b1ce97d9c5be942c0d63 Mon Sep 17 00:00:00 2001 From: cschleiden Date: Sat, 26 Oct 2024 21:57:22 -0700 Subject: [PATCH 08/13] Update docs --- docs/source/includes/_guide.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/source/includes/_guide.md b/docs/source/includes/_guide.md index 19c8531d..b90e5be1 100644 --- a/docs/source/includes/_guide.md +++ b/docs/source/includes/_guide.md @@ -666,10 +666,6 @@ For logging in activities, you can get a logger using `activity.Logger`. The ret The library supports tracing via [OpenTelemetry](https://opentelemetry.io/). When you pass a `TracerProvider` when creating a backend instance, workflow execution will be traced. You can also add additional spans for both activities and workflows. - - ### Activities ```go From 43e6640cda9cf466097208f8979161e38990326b Mon Sep 17 00:00:00 2001 From: cschleiden Date: Sat, 26 Oct 2024 22:15:06 -0700 Subject: [PATCH 09/13] Include timer name in future name --- workflow/timer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/timer.go b/workflow/timer.go index 76241f1b..6b4e1c6d 100644 --- a/workflow/timer.go +++ b/workflow/timer.go @@ -61,7 +61,7 @@ func ScheduleTimer(ctx Context, delay time.Duration, opts ...timerOption) Future timerCmd := command.NewScheduleTimerCommand(scheduleEventID, at, timerConfig.Name, traceContext) wfState.AddCommand(timerCmd) - wfState.TrackFuture(scheduleEventID, workflowstate.AsDecodingSettable(contextvalue.Converter(ctx), fmt.Sprintf("timer:%v", delay), f)) + wfState.TrackFuture(scheduleEventID, workflowstate.AsDecodingSettable(contextvalue.Converter(ctx), fmt.Sprintf("timer:%s:%v", timerConfig.Name, delay), f)) cancelReceiver := &sync.Receiver[struct{}]{ Receive: func(v struct{}, ok bool) { From a2f5b0383b3ce445f54d97337eff8d683c1f6e66 Mon Sep 17 00:00:00 2001 From: cschleiden Date: Sat, 26 Oct 2024 22:15:28 -0700 Subject: [PATCH 10/13] Update docs --- docs/source/includes/_guide.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/includes/_guide.md b/docs/source/includes/_guide.md index b90e5be1..bd942e9a 100644 --- a/docs/source/includes/_guide.md +++ b/docs/source/includes/_guide.md @@ -248,6 +248,12 @@ You can schedule timers to fire at any point in the future by calling `workflow. All timers must have either fired or been canceled before a workflow can complete. If the workflow function exits with pending timer futures an error will be returned. +```go +t := workflow.ScheduleTimer(ctx, 2*time.Second, workflow.WithTimerName("my-timer")) +``` + +You can optionally name timers for tracing and debugging purposes. + ### Canceling timers ```go From a73d5f6acb50b9b32f614616dc4a2a06496e180d Mon Sep 17 00:00:00 2001 From: cschleiden Date: Sun, 27 Oct 2024 14:13:59 -0700 Subject: [PATCH 11/13] Test for named timer --- workflow/executor/executor_test.go | 5 ++++- workflow/timer.go | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/workflow/executor/executor_test.go b/workflow/executor/executor_test.go index 946a70ec..994d338b 100644 --- a/workflow/executor/executor_test.go +++ b/workflow/executor/executor_test.go @@ -598,6 +598,9 @@ func Test_Executor(t *testing.T) { // Schedule but not wait for timer wf.ScheduleTimer(ctx, time.Second*2) + // Schedule but not wait for named timer + wf.ScheduleTimer(ctx, time.Second*2, wf.WithTimerName("delay")) + return nil } @@ -606,7 +609,7 @@ func Test_Executor(t *testing.T) { task := startWorkflowTask("instanceID", workflow) - require.PanicsWithValue(t, "workflow completed, but there are still pending futures: [1-subworkflow:1 2-timer:2s]", func() { + require.PanicsWithValue(t, "workflow completed, but there are still pending futures: [1-subworkflow:1 2-timer:2s 3-timer-delay:2s]", func() { e.ExecuteTask(context.Background(), task) }) }, diff --git a/workflow/timer.go b/workflow/timer.go index 6b4e1c6d..3da3dbda 100644 --- a/workflow/timer.go +++ b/workflow/timer.go @@ -61,7 +61,15 @@ func ScheduleTimer(ctx Context, delay time.Duration, opts ...timerOption) Future timerCmd := command.NewScheduleTimerCommand(scheduleEventID, at, timerConfig.Name, traceContext) wfState.AddCommand(timerCmd) - wfState.TrackFuture(scheduleEventID, workflowstate.AsDecodingSettable(contextvalue.Converter(ctx), fmt.Sprintf("timer:%s:%v", timerConfig.Name, delay), f)) + + timerSuffix := "" + if timerConfig.Name != "" { + timerSuffix = "-" + timerConfig.Name + } + + wfState.TrackFuture( + scheduleEventID, + workflowstate.AsDecodingSettable(contextvalue.Converter(ctx), fmt.Sprintf("timer%s:%v", timerSuffix, delay), f)) cancelReceiver := &sync.Receiver[struct{}]{ Receive: func(v struct{}, ok bool) { From 8a70d1200e3d38b64def21fee9dd0b07035785ba Mon Sep 17 00:00:00 2001 From: cschleiden Date: Sun, 27 Oct 2024 14:20:02 -0700 Subject: [PATCH 12/13] Update build workflow --- .github/workflows/go.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 44723eaa..6b8ebd2a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,12 +16,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: 1.21 + go-version: 1.22 check-latest: true cache: true From 8bfa7a65f54d07d03b2706e81e527e599e8783c0 Mon Sep 17 00:00:00 2001 From: cschleiden Date: Sun, 27 Oct 2024 14:30:38 -0700 Subject: [PATCH 13/13] Improve removal retries --- backend/test/e2e_removal.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/test/e2e_removal.go b/backend/test/e2e_removal.go index 649f9a28..30442df3 100644 --- a/backend/test/e2e_removal.go +++ b/backend/test/e2e_removal.go @@ -27,12 +27,10 @@ var e2eRemovalTests = []backendTest{ _, err := client.GetWorkflowResult[bool](ctx, c, workflowA, time.Second*10) require.NoError(t, err) - now := time.Now() - for i := 0; i < 10; i++ { time.Sleep(300 * time.Millisecond) - err = b.RemoveWorkflowInstances(ctx, backend.RemoveFinishedBefore(now)) + err = b.RemoveWorkflowInstances(ctx, backend.RemoveFinishedBefore(time.Now())) if errors.As(err, &backend.ErrNotSupported{}) { t.Skip() return