diff --git a/go.mod b/go.mod index 6745077c9..ff72dc80f 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/Pallinder/sillyname-go v0.0.0-20130730142914-97aeae9e6ba1 github.com/briandowns/spinner v1.19.0 github.com/dapr/dapr v1.17.0-rc.8 - github.com/dapr/durabletask-go v0.11.0 + github.com/dapr/durabletask-go v0.11.4-0.20260408193502-7e0554e3883e github.com/dapr/go-sdk v1.13.0 - github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d + github.com/dapr/kit v0.17.1-0.20260402173438-be272d92042b github.com/diagridio/go-etcd-cron v0.12.3 github.com/docker/docker v28.5.2+incompatible github.com/evanphx/json-patch/v5 v5.9.0 @@ -104,6 +104,7 @@ require ( github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-events v0.0.0-20250808211157-605354379745 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect @@ -189,6 +190,7 @@ require ( github.com/montanaflynn/stats v0.7.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect @@ -204,6 +206,7 @@ require ( github.com/prometheus/common v0.64.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/statsd_exporter v0.22.7 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rubenv/sql-migrate v1.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect @@ -269,6 +272,10 @@ require ( k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/kubectl v0.33.3 // indirect k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect + modernc.org/libc v1.61.9 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect + modernc.org/sqlite v1.34.5 // indirect oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/controller-runtime v0.19.0 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect diff --git a/go.sum b/go.sum index 86d37d1fb..1b64d2881 100644 --- a/go.sum +++ b/go.sum @@ -166,10 +166,14 @@ github.com/dapr/dapr v1.17.0-rc.8 h1:CZTPQRwX7sO9H4wKrVnnV6w1foKWLP89wtAJY/bL5KQ github.com/dapr/dapr v1.17.0-rc.8/go.mod h1:GZq8BjQaX3BbMmZET4HDe6Rrv58yC1pHNVQod3bIAVk= github.com/dapr/durabletask-go v0.11.0 h1:e9Ns/3a2b6JDKGuvksvx6gCHn7rd+nwZZyAXbg5Ley4= github.com/dapr/durabletask-go v0.11.0/go.mod h1:0Ts4rXp74JyG19gDWPcwNo5V6NBZzhARzHF5XynmA7Q= +github.com/dapr/durabletask-go v0.11.4-0.20260408193502-7e0554e3883e h1:/ZE9swGMPaaaoo2wJNjC4xzDaQRaAk0m9yddSo7YRlo= +github.com/dapr/durabletask-go v0.11.4-0.20260408193502-7e0554e3883e/go.mod h1:nElJPTX9FmveqErAmvTXZrAmt2nnyTQ7Glj0ahPphSY= github.com/dapr/go-sdk v1.13.0 h1:Qw2BmUonClQ9yK/rrEEaFL1PyDgq616RrvYj0CT67Lk= github.com/dapr/go-sdk v1.13.0/go.mod h1:RsffVNZitDApmQqoS68tNKGMXDZUjTviAbKZupJSzts= github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d h1:csljij9d1IO6u9nqbg+TuSRmTZ+OXT8G49yh6zie1yI= github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= +github.com/dapr/kit v0.17.1-0.20260402173438-be272d92042b h1:hXbRlNKvmMGbiMSRy3MX04kH5OiMgH82PRuLN7adtwE= +github.com/dapr/kit v0.17.1-0.20260402173438-be272d92042b/go.mod h1:2v02LZdXzPmOadxoT6EMEt0bsEYe6h1fn2ndYWmylCg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -202,6 +206,8 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -540,6 +546,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -617,6 +625,8 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +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/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= @@ -1167,6 +1177,14 @@ k8s.io/kubectl v0.33.3 h1:r/phHvH1iU7gO/l7tTjQk2K01ER7/OAJi8uFHHyWSac= k8s.io/kubectl v0.33.3/go.mod h1:euj2bG56L6kUGOE/ckZbCoudPwuj4Kud7BR0GzyNiT0= k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM= +modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= +modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/pkg/workflow/history.go b/pkg/workflow/history.go index a19ec7323..68293da84 100644 --- a/pkg/workflow/history.go +++ b/pkg/workflow/history.go @@ -230,6 +230,12 @@ func HistoryWide(ctx context.Context, opts HistoryOptions) ([]*HistoryOutputWide row.addAttr("timerName", *t.TimerCreated.Name) } row.addAttr("fireAt", t.TimerCreated.FireAt.AsTime().Format(time.RFC3339)) + if o := timerOriginInfo(t.TimerCreated); o != nil { + row.addAttr("origin", o.kind) + if o.attrKey != "" { + row.addAttr(o.attrKey, o.attrVal) + } + } case *protos.HistoryEvent_TimerFired: row.addAttr("timerId", fmt.Sprintf("%d", t.TimerFired.TimerId)) row.addAttr("fireAt", t.TimerFired.FireAt.AsTime().Format(time.RFC3339)) @@ -382,6 +388,9 @@ func deriveDetails(first *protos.HistoryEvent, h *protos.HistoryEvent) *string { if in := t.TimerCreated.RerunParentInstanceInfo; in != nil { det += fmt.Sprintf(",rerunParent=%s", in.InstanceID) } + if o := timerOriginString(t.TimerCreated); o != "" { + det += ",origin=" + o + } return ptr.Of(det) case *protos.HistoryEvent_EventRaised: return ptr.Of(fmt.Sprintf("event=%s", t.EventRaised.Name)) @@ -450,6 +459,38 @@ func flatTags(tags map[string]string, max int) string { return s } +type timerOrigin struct { + kind string // e.g. "createTimer", "externalEvent" + attrKey string // extra attr key, empty if none + attrVal string // extra attr value +} + +func timerOriginInfo(tc *protos.TimerCreatedEvent) *timerOrigin { + switch x := tc.GetOrigin().(type) { + case *protos.TimerCreatedEvent_CreateTimer: + return &timerOrigin{kind: "createTimer"} + case *protos.TimerCreatedEvent_ExternalEvent: + return &timerOrigin{kind: "externalEvent", attrKey: "eventName", attrVal: x.ExternalEvent.GetName()} + case *protos.TimerCreatedEvent_ActivityRetry: + return &timerOrigin{kind: "activityRetry", attrKey: "taskExecId", attrVal: x.ActivityRetry.GetTaskExecutionId()} + case *protos.TimerCreatedEvent_ChildWorkflowRetry: + return &timerOrigin{kind: "childWorkflowRetry", attrKey: "instanceId", attrVal: x.ChildWorkflowRetry.GetInstanceId()} + default: + return nil + } +} + +func timerOriginString(tc *protos.TimerCreatedEvent) string { + o := timerOriginInfo(tc) + if o == nil { + return "" + } + if o.attrVal != "" { + return o.kind + "(" + o.attrVal + ")" + } + return o.kind +} + func trim(ww *wrapperspb.StringValue, limit int) string { if ww == nil { return "" diff --git a/pkg/workflow/history_test.go b/pkg/workflow/history_test.go new file mode 100644 index 000000000..5d93e9ae0 --- /dev/null +++ b/pkg/workflow/history_test.go @@ -0,0 +1,180 @@ +/* +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package workflow + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/dapr/durabletask-go/api/protos" +) + +func TestTimerOriginString(t *testing.T) { + tests := []struct { + name string + event *protos.TimerCreatedEvent + expect string + }{ + { + name: "nil origin", + event: &protos.TimerCreatedEvent{ + FireAt: timestamppb.Now(), + }, + expect: "", + }, + { + name: "createTimer", + event: &protos.TimerCreatedEvent{ + FireAt: timestamppb.Now(), + Origin: &protos.TimerCreatedEvent_CreateTimer{ + CreateTimer: &protos.TimerOriginCreateTimer{}, + }, + }, + expect: "createTimer", + }, + { + name: "externalEvent", + event: &protos.TimerCreatedEvent{ + FireAt: timestamppb.Now(), + Origin: &protos.TimerCreatedEvent_ExternalEvent{ + ExternalEvent: &protos.TimerOriginExternalEvent{ + Name: "myEvent", + }, + }, + }, + expect: "externalEvent(myEvent)", + }, + { + name: "activityRetry", + event: &protos.TimerCreatedEvent{ + FireAt: timestamppb.Now(), + Origin: &protos.TimerCreatedEvent_ActivityRetry{ + ActivityRetry: &protos.TimerOriginActivityRetry{ + TaskExecutionId: "exec-123", + }, + }, + }, + expect: "activityRetry(exec-123)", + }, + { + name: "childWorkflowRetry", + event: &protos.TimerCreatedEvent{ + FireAt: timestamppb.Now(), + Origin: &protos.TimerCreatedEvent_ChildWorkflowRetry{ + ChildWorkflowRetry: &protos.TimerOriginChildWorkflowRetry{ + InstanceId: "wf-456", + }, + }, + }, + expect: "childWorkflowRetry(wf-456)", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := timerOriginString(tc.event) + assert.Equal(t, tc.expect, got) + }) + } +} + +func TestDeriveDetails_TimerCreated(t *testing.T) { + first := &protos.HistoryEvent{ + Timestamp: timestamppb.Now(), + } + + tests := []struct { + name string + event *protos.HistoryEvent + contains []string + excludes []string + }{ + { + name: "timer with no origin", + event: &protos.HistoryEvent{ + EventType: &protos.HistoryEvent_TimerCreated{ + TimerCreated: &protos.TimerCreatedEvent{ + FireAt: timestamppb.Now(), + }, + }, + }, + contains: []string{"fireAt="}, + excludes: []string{"origin="}, + }, + { + name: "timer with createTimer origin", + event: &protos.HistoryEvent{ + EventType: &protos.HistoryEvent_TimerCreated{ + TimerCreated: &protos.TimerCreatedEvent{ + FireAt: timestamppb.Now(), + Origin: &protos.TimerCreatedEvent_CreateTimer{ + CreateTimer: &protos.TimerOriginCreateTimer{}, + }, + }, + }, + }, + contains: []string{"fireAt=", "origin=createTimer"}, + }, + { + name: "timer with activityRetry origin", + event: &protos.HistoryEvent{ + EventType: &protos.HistoryEvent_TimerCreated{ + TimerCreated: &protos.TimerCreatedEvent{ + FireAt: timestamppb.Now(), + Origin: &protos.TimerCreatedEvent_ActivityRetry{ + ActivityRetry: &protos.TimerOriginActivityRetry{ + TaskExecutionId: "exec-abc", + }, + }, + }, + }, + }, + contains: []string{"fireAt=", "origin=activityRetry(exec-abc)"}, + }, + { + name: "timer with rerunParent and origin", + event: &protos.HistoryEvent{ + EventType: &protos.HistoryEvent_TimerCreated{ + TimerCreated: &protos.TimerCreatedEvent{ + FireAt: timestamppb.Now(), + RerunParentInstanceInfo: &protos.RerunParentInstanceInfo{ + InstanceID: "parent-123", + }, + Origin: &protos.TimerCreatedEvent_ExternalEvent{ + ExternalEvent: &protos.TimerOriginExternalEvent{ + Name: "approval", + }, + }, + }, + }, + }, + contains: []string{"fireAt=", "rerunParent=parent-123", "origin=externalEvent(approval)"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + details := deriveDetails(first, tc.event) + assert.NotNil(t, details) + for _, s := range tc.contains { + assert.Contains(t, *details, s) + } + for _, s := range tc.excludes { + assert.NotContains(t, *details, s) + } + }) + } +} diff --git a/tests/apps/workflow/app.go b/tests/apps/workflow/app.go index a070c3fad..02e1e1ba5 100644 --- a/tests/apps/workflow/app.go +++ b/tests/apps/workflow/app.go @@ -49,12 +49,16 @@ func register(ctx context.Context) { RecursiveChildWorkflow, FanOutWorkflow, DataWorkflow, + ActivityRetryWorkflow, + ChildWorkflowRetryWorkflow, + FailingChildWorkflow, } activities := []workflow.Activity{ ANoOP, SimpleActivity, LongRunningActivity, DataProcessingActivity, + FailingActivity, } for _, w := range workflows { @@ -368,3 +372,44 @@ func FanOutWorkflow(ctx *workflow.WorkflowContext) (any, error) { "results": results, }, nil } + +// FailingActivity always returns an error so that retry policies create +// timer events with origin=activityRetry. +func FailingActivity(ctx workflow.ActivityContext) (any, error) { + return nil, fmt.Errorf("intentional failure") +} + +// ActivityRetryWorkflow calls FailingActivity with a retry policy. +// The activity always fails, producing TimerCreated events with +// origin=activityRetry. The workflow itself will eventually fail +// after max attempts, but the history will contain the retry timers. +func ActivityRetryWorkflow(ctx *workflow.WorkflowContext) (any, error) { + var result any + err := ctx.CallActivity(FailingActivity, workflow.WithActivityRetryPolicy(&workflow.RetryPolicy{ + MaxAttempts: 3, + InitialRetryInterval: time.Second, + BackoffCoefficient: 1, + MaxRetryInterval: time.Second, + })).Await(&result) + return result, err +} + +// FailingChildWorkflow always returns an error so that retry policies create +// timer events with origin=childWorkflowRetry. +func FailingChildWorkflow(ctx *workflow.WorkflowContext) (any, error) { + return nil, fmt.Errorf("intentional child failure") +} + +// ChildWorkflowRetryWorkflow calls FailingChildWorkflow with a retry policy. +// The child always fails, producing TimerCreated events with +// origin=childWorkflowRetry. +func ChildWorkflowRetryWorkflow(ctx *workflow.WorkflowContext) (any, error) { + var result any + err := ctx.CallChildWorkflow(FailingChildWorkflow, workflow.WithChildWorkflowRetryPolicy(&workflow.RetryPolicy{ + MaxAttempts: 3, + InitialRetryInterval: time.Second, + BackoffCoefficient: 1, + MaxRetryInterval: time.Second, + })).Await(&result) + return result, err +} diff --git a/tests/e2e/standalone/workflow_test.go b/tests/e2e/standalone/workflow_test.go index 682bfe42f..a37151113 100644 --- a/tests/e2e/standalone/workflow_test.go +++ b/tests/e2e/standalone/workflow_test.go @@ -732,6 +732,122 @@ func TestWorkflowHistory(t *testing.T) { require.NoError(t, json.Unmarshal([]byte(output), &history)) assert.GreaterOrEqual(t, len(history), 1) }) + + t.Run("timer origin createTimer", func(t *testing.T) { + // WTimer calls ctx.CreateTimer which produces origin=createTimer. + _, err := cmdWorkflowRun(appID, "WTimer", "--instance-id=timer-origin-test") + require.NoError(t, err) + + require.Eventually(t, func() bool { + out, err := cmdWorkflowHistory(appID, "timer-origin-test") + return err == nil && strings.Contains(out, "origin=createTimer") + }, 10*time.Second, 200*time.Millisecond) + + output, err := cmdWorkflowHistory(appID, "timer-origin-test", "-o", "json") + require.NoError(t, err) + + var history []map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(output), &history)) + + found := false + for _, ev := range history { + if ev["type"] == "TimerCreated" { + if attrs, ok := ev["attrs"].(string); ok { + assert.Contains(t, attrs, "origin=createTimer") + found = true + } + } + } + assert.True(t, found, "expected TimerCreated event with origin=createTimer in attrs") + }) + + t.Run("timer origin externalEvent", func(t *testing.T) { + // EventWorkflow calls ctx.WaitForExternalEvent("test-event", time.Hour) + // which produces origin=externalEvent(test-event). + _, err := cmdWorkflowRun(appID, "EventWorkflow", "--instance-id=event-origin-test") + require.NoError(t, err) + + require.Eventually(t, func() bool { + out, err := cmdWorkflowHistory(appID, "event-origin-test") + return err == nil && strings.Contains(out, "origin=externalEvent(test-event)") + }, 10*time.Second, 200*time.Millisecond) + + output, err := cmdWorkflowHistory(appID, "event-origin-test", "-o", "json") + require.NoError(t, err) + + var history []map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(output), &history)) + + found := false + for _, ev := range history { + if ev["type"] == "TimerCreated" { + if attrs, ok := ev["attrs"].(string); ok { + assert.Contains(t, attrs, "origin=externalEvent") + assert.Contains(t, attrs, "eventName=test-event") + found = true + } + } + } + assert.True(t, found, "expected TimerCreated event with origin=externalEvent in attrs") + }) + + t.Run("timer origin activityRetry", func(t *testing.T) { + // ActivityRetryWorkflow calls a failing activity with retry policy, + // producing TimerCreated events with origin=activityRetry. + _, err := cmdWorkflowRun(appID, "ActivityRetryWorkflow", "--instance-id=activity-retry-origin-test") + require.NoError(t, err) + + require.Eventually(t, func() bool { + out, err := cmdWorkflowHistory(appID, "activity-retry-origin-test") + return err == nil && strings.Contains(out, "origin=activityRetry(") + }, 15*time.Second, 250*time.Millisecond) + + output, err := cmdWorkflowHistory(appID, "activity-retry-origin-test", "-o", "json") + require.NoError(t, err) + + var history []map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(output), &history)) + + found := false + for _, ev := range history { + if ev["type"] == "TimerCreated" { + if attrs, ok := ev["attrs"].(string); ok && strings.Contains(attrs, "origin=activityRetry") { + assert.Contains(t, attrs, "taskExecId=") + found = true + } + } + } + assert.True(t, found, "expected TimerCreated event with origin=activityRetry in attrs") + }) + + t.Run("timer origin childWorkflowRetry", func(t *testing.T) { + // ChildWorkflowRetryWorkflow calls a failing child workflow with retry + // policy, producing TimerCreated events with origin=childWorkflowRetry. + _, err := cmdWorkflowRun(appID, "ChildWorkflowRetryWorkflow", "--instance-id=child-retry-origin-test") + require.NoError(t, err) + + require.Eventually(t, func() bool { + out, err := cmdWorkflowHistory(appID, "child-retry-origin-test") + return err == nil && strings.Contains(out, "origin=childWorkflowRetry(") + }, 30*time.Second, 500*time.Millisecond) + + output, err := cmdWorkflowHistory(appID, "child-retry-origin-test", "-o", "json") + require.NoError(t, err) + + var history []map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(output), &history)) + + found := false + for _, ev := range history { + if ev["type"] == "TimerCreated" { + if attrs, ok := ev["attrs"].(string); ok && strings.Contains(attrs, "origin=childWorkflowRetry") { + assert.Contains(t, attrs, "instanceId=") + found = true + } + } + } + assert.True(t, found, "expected TimerCreated event with origin=childWorkflowRetry in attrs") + }) } func TestWorkflowSuspendResume(t *testing.T) {