From 03bace3a20a5429d04ea001bf4137a1687ecea08 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 17 Oct 2025 09:27:44 -0400 Subject: [PATCH 01/24] add test displaying NDE from 1.11.3 replay --- packages/core-bridge/Cargo.lock | 152 ++++++++----- packages/core-bridge/Cargo.toml | 22 +- packages/core-bridge/sdk-core | 2 +- packages/test/history_files/otel_1_11_3.json | 215 ++++++++++++++++++ packages/test/src/helpers-integration.ts | 27 +++ .../test/src/test-integration-workflows.ts | 6 +- packages/test/src/test-otel.ts | 72 +++++- packages/test/src/workflows/inbound-signal.ts | 0 packages/test/src/workflows/index.ts | 1 + .../test/src/workflows/signal-start-otel.ts | 29 +++ 10 files changed, 454 insertions(+), 72 deletions(-) create mode 100644 packages/test/history_files/otel_1_11_3.json create mode 100644 packages/test/src/workflows/inbound-signal.ts create mode 100644 packages/test/src/workflows/signal-start-otel.ts diff --git a/packages/core-bridge/Cargo.lock b/packages/core-bridge/Cargo.lock index a3f0f9339..255c47502 100644 --- a/packages/core-bridge/Cargo.lock +++ b/packages/core-bridge/Cargo.lock @@ -1375,23 +1375,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "opentelemetry" -version = "0.29.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e87237e2775f74896f9ad219d26a2081751187eb7c9f5c58dde20a23b95d16c" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.14", - "tracing", -] - -[[package]] -name = "opentelemetry" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" dependencies = [ "futures-core", "futures-sink", @@ -1403,25 +1389,25 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", "http", - "opentelemetry 0.30.0", + "opentelemetry", "reqwest", ] [[package]] name = "opentelemetry-otlp" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" dependencies = [ "http", - "opentelemetry 0.30.0", + "opentelemetry", "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", @@ -1435,29 +1421,29 @@ dependencies = [ [[package]] name = "opentelemetry-proto" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ - "opentelemetry 0.30.0", + "opentelemetry", "opentelemetry_sdk", "prost", "tonic", + "tonic-prost", ] [[package]] name = "opentelemetry_sdk" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry 0.30.0", + "opentelemetry", "percent-encoding", "rand 0.9.2", - "serde_json", "thiserror 2.0.14", "tokio", "tokio-stream", @@ -1654,9 +1640,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ "bytes", "prost-derive", @@ -1664,9 +1650,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" dependencies = [ "heck", "itertools", @@ -1677,6 +1663,8 @@ dependencies = [ "prettyplease", "prost", "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", "regex", "syn", "tempfile", @@ -1684,9 +1672,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", "itertools", @@ -1697,18 +1685,18 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" dependencies = [ "prost", ] [[package]] name = "prost-wkt" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497e1e938f0c09ef9cabe1d49437b4016e03e8f82fbbe5d1c62a9b61b9decae1" +checksum = "655944d0ce015e71b3ec21279437e6a09e58433e50c7b0677901f3d5235e74f5" dependencies = [ "chrono", "inventory", @@ -1721,9 +1709,9 @@ dependencies = [ [[package]] name = "prost-wkt-build" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b8bf115b70a7aa5af1fd5d6e9418492e9ccb6e4785e858c938e28d132a884b" +checksum = "f869f1443fee474b785e935d92e1007f57443e485f51668ed41943fc01a321a2" dependencies = [ "heck", "prost", @@ -1734,9 +1722,9 @@ dependencies = [ [[package]] name = "prost-wkt-types" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8cdde6df0a98311c839392ca2f2f0bcecd545f86a62b4e3c6a49c336e970fe5" +checksum = "eeeffd6b9becd4600dd461399f3f71aeda2ff0848802a9ed526cf12e8f42902a" dependencies = [ "chrono", "prost", @@ -1770,6 +1758,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5b6a0769a491a08b31ea5c62494a8f144ee0987d86d670a8af4df1e1b7cde75" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "quanta" version = "0.12.6" @@ -2459,7 +2467,7 @@ dependencies = [ "itertools", "lru", "mockall", - "opentelemetry 0.30.0", + "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", "parking_lot", @@ -2500,7 +2508,7 @@ dependencies = [ "async-trait", "derive_builder", "derive_more", - "opentelemetry 0.30.0", + "opentelemetry", "prost", "serde_json", "temporal-sdk-core-protos", @@ -2519,16 +2527,15 @@ dependencies = [ "base64", "derive_more", "prost", - "prost-build", "prost-wkt", - "prost-wkt-build", "prost-wkt-types", "rand 0.9.2", "serde", "serde_json", "thiserror 2.0.14", "tonic", - "tonic-build", + "tonic-prost", + "tonic-prost-build", "uuid", ] @@ -2541,7 +2548,7 @@ dependencies = [ "bridge-macros", "futures", "neon", - "opentelemetry 0.29.1", + "opentelemetry", "os_pipe", "parking_lot", "prost", @@ -2705,9 +2712,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.13.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", "axum", @@ -2722,9 +2729,9 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", "rustls-native-certs", - "socket2 0.5.10", + "socket2 0.6.0", + "sync_wrapper", "tokio", "tokio-rustls", "tokio-stream", @@ -2736,9 +2743,32 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.13.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847" +checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" dependencies = [ "prettyplease", "proc-macro2", @@ -2746,6 +2776,8 @@ dependencies = [ "prost-types", "quote", "syn", + "tempfile", + "tonic-build", ] [[package]] @@ -2882,6 +2914,12 @@ dependencies = [ "syn", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -2925,9 +2963,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", diff --git a/packages/core-bridge/Cargo.toml b/packages/core-bridge/Cargo.toml index 1831c7b59..ecc4e78e0 100644 --- a/packages/core-bridge/Cargo.toml +++ b/packages/core-bridge/Cargo.toml @@ -25,28 +25,28 @@ async-trait = "0.1.83" bridge-macros = { path = "bridge-macros" } futures = { version = "0.3", features = ["executor"] } neon = { version = "1.0.0", default-features = false, features = [ - "napi-6", - "futures", + "napi-6", + "futures", ] } -opentelemetry = "0.29" +opentelemetry = "0.31" os_pipe = "1.2.1" parking_lot = "0.12" -prost = "0.13" -prost-types = "0.13" +prost = "0.14" +prost-types = "0.14" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" temporal-sdk-core = { version = "*", path = "./sdk-core/core", features = [ - "ephemeral-server", + "ephemeral-server", ] } temporal-client = { version = "*", path = "./sdk-core/client" } thiserror = "2" tokio = "1.13" tokio-stream = "0.1" -tonic = "0.13" +tonic = "0.14" tracing = "0.1" tracing-subscriber = { version = "0.3", default-features = false, features = [ - "parking_lot", - "env-filter", - "registry", - "ansi", + "parking_lot", + "env-filter", + "registry", + "ansi", ] } diff --git a/packages/core-bridge/sdk-core b/packages/core-bridge/sdk-core index de674173c..bd02cceae 160000 --- a/packages/core-bridge/sdk-core +++ b/packages/core-bridge/sdk-core @@ -1 +1 @@ -Subproject commit de674173c664d42f85d0dee1ff3b2ac47e36d545 +Subproject commit bd02cceae2a4e0006fe86911a6ad3b7cf7dd2ad8 diff --git a/packages/test/history_files/otel_1_11_3.json b/packages/test/history_files/otel_1_11_3.json new file mode 100644 index 000000000..fb6c8a119 --- /dev/null +++ b/packages/test/history_files/otel_1_11_3.json @@ -0,0 +1,215 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2025-10-17T12:56:44.200769Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1049716", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "signalStartOtel" + }, + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": {}, + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "10b7e171-0ee7-4775-ad6d-f5df09aa4160", + "identity": "82936@mac.lan", + "firstExecutionRunId": "10b7e171-0ee7-4775-ad6d-f5df09aa4160", + "attempt": 1, + "firstWorkflowTaskBackoff": "0s", + "header": { + "fields": {} + }, + "workflowId": "f61035b7-9fa3-4120-ac03-98763ccd469a" + } + }, + { + "eventId": "2", + "eventTime": "2025-10-17T12:56:44.200815Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1049717", + "workflowExecutionSignaledEventAttributes": { + "signalName": "startSignal", + "input": {}, + "identity": "82936@mac.lan", + "header": { + "fields": {} + } + } + }, + { + "eventId": "3", + "eventTime": "2025-10-17T12:56:44.200819Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049718", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "4", + "eventTime": "2025-10-17T12:56:44.201794Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049722", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "3", + "identity": "82936@mac.lan", + "requestId": "488319d7-d425-44eb-b5ce-362f8462be1f", + "historySizeBytes": "327", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+6b92c11c4f907379345c3513a5f749c90d752cfd0a3bf888bf0be04350bb0d2e" + } + } + }, + { + "eventId": "5", + "eventTime": "2025-10-17T12:56:44.229959Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049726", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "3", + "startedEventId": "4", + "identity": "82936@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+6b92c11c4f907379345c3513a5f749c90d752cfd0a3bf888bf0be04350bb0d2e" + }, + "sdkMetadata": { + "coreUsedFlags": [ + 1, + 3, + 2 + ], + "langUsedFlags": [ + 2 + ], + "sdkName": "temporal-typescript", + "sdkVersion": "1.13.1" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "6", + "eventTime": "2025-10-17T12:56:44.229985Z", + "eventType": "EVENT_TYPE_MARKER_RECORDED", + "taskId": "1049727", + "markerRecordedEventAttributes": { + "details": { + "data": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzZXEiOjEsImF0dGVtcHQiOjEsImFjdGl2aXR5X2lkIjoiMSIsImFjdGl2aXR5X3R5cGUiOiJhIiwiY29tcGxldGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzA1ODA0LCJuYW5vcyI6MjAyMzY4NjY3fSwiYmFja29mZiI6bnVsbCwib3JpZ2luYWxfc2NoZWR1bGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzA1ODA0LCJuYW5vcyI6MjI0MTE2MDAwfX0=" + } + ] + }, + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImEi" + } + ] + } + }, + "markerName": "core_local_activity", + "workflowTaskCompletedEventId": "5" + } + }, + { + "eventId": "7", + "eventTime": "2025-10-17T12:56:44.229986Z", + "eventType": "EVENT_TYPE_MARKER_RECORDED", + "taskId": "1049728", + "markerRecordedEventAttributes": { + "details": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImIi" + } + ] + }, + "data": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzZXEiOjIsImF0dGVtcHQiOjEsImFjdGl2aXR5X2lkIjoiMiIsImFjdGl2aXR5X3R5cGUiOiJiIiwiY29tcGxldGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzA1ODA0LCJuYW5vcyI6MjAyNjc1OTE3fSwiYmFja29mZiI6bnVsbCwib3JpZ2luYWxfc2NoZWR1bGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzA1ODA0LCJuYW5vcyI6MjI3MzQ4MDAwfX0=" + } + ] + } + }, + "markerName": "core_local_activity", + "workflowTaskCompletedEventId": "5" + } + }, + { + "eventId": "8", + "eventTime": "2025-10-17T12:56:44.230034Z", + "eventType": "EVENT_TYPE_MARKER_RECORDED", + "taskId": "1049729", + "markerRecordedEventAttributes": { + "details": { + "data": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzZXEiOjMsImF0dGVtcHQiOjEsImFjdGl2aXR5X2lkIjoiMyIsImFjdGl2aXR5X3R5cGUiOiJjIiwiY29tcGxldGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzA1ODA0LCJuYW5vcyI6MjAyNzI0ODM0fSwiYmFja29mZiI6bnVsbCwib3JpZ2luYWxfc2NoZWR1bGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzA1ODA0LCJuYW5vcyI6MjI3MzU2MDAwfX0=" + } + ] + }, + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImMi" + } + ] + } + }, + "markerName": "core_local_activity", + "workflowTaskCompletedEventId": "5" + } + }, + { + "eventId": "9", + "eventTime": "2025-10-17T12:56:44.230036Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1049730", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImFiYyI=" + } + ] + }, + "workflowTaskCompletedEventId": "5" + } + } + ] +} \ No newline at end of file diff --git a/packages/test/src/helpers-integration.ts b/packages/test/src/helpers-integration.ts index 9d0d6fab0..556bc7c15 100644 --- a/packages/test/src/helpers-integration.ts +++ b/packages/test/src/helpers-integration.ts @@ -6,8 +6,10 @@ import { WorkflowFailedError, WorkflowHandle, WorkflowHandleWithFirstExecutionRunId, + WorkflowHandleWithSignaledRunId, WorkflowStartOptions, WorkflowUpdateFailedError, + WorkflowSignalWithStartOptionsWithArgs, } from '@temporalio/client'; import { LocalTestWorkflowEnvironmentOptions, @@ -195,6 +197,16 @@ export interface Helpers { fn: T, opts: Omit & Partial> ): Promise>; + signalWithStart( + fn: T, + signal: workflow.SignalDefinition + ): Promise>; + signalWithStart( + fn: T, + signal: workflow.SignalDefinition, + opts: Omit, 'taskQueue' | 'workflowId'> & + Partial> + ): Promise>; assertWorkflowUpdateFailed(p: Promise, causeConstructor: ErrorConstructor, message?: string): Promise; assertWorkflowFailedError(p: Promise, causeConstructor: ErrorConstructor, message?: string): Promise; updateHasBeenAdmitted(handle: WorkflowHandle, updateId: string): Promise; @@ -250,6 +262,21 @@ export function configurableHelpers( ...opts, }); }, + + async signalWithStart( + fn: workflow.Workflow, + signal: workflow.SignalDefinition, + opts?: Omit, 'taskQueue' | 'workflowId'> & + Partial> + ): Promise> { + return await testEnv.client.workflow.signalWithStart(fn, { + signal, + taskQueue, + workflowId: randomUUID(), + ...opts, + }); + }, + async assertWorkflowUpdateFailed( p: Promise, causeConstructor: ErrorConstructor, diff --git a/packages/test/src/test-integration-workflows.ts b/packages/test/src/test-integration-workflows.ts index 3b0604d17..f704e8e01 100644 --- a/packages/test/src/test-integration-workflows.ts +++ b/packages/test/src/test-integration-workflows.ts @@ -1,3 +1,6 @@ +import * as opentelemetry from '@opentelemetry/sdk-node'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'; import { setTimeout as setTimeoutPromise } from 'timers/promises'; import { randomUUID } from 'crypto'; import asyncRetry from 'async-retry'; @@ -45,7 +48,7 @@ import * as workflows from './workflows'; import { Context, createLocalTestEnvironment, helpers, makeTestFunction } from './helpers-integration'; import { overrideSdkInternalFlag } from './mock-internal-flags'; import { ActivityState, heartbeatCancellationDetailsActivity } from './activities/heartbeat-cancellation-details'; -import { loadHistory, RUN_TIME_SKIPPING_TESTS, waitUntil } from './helpers'; +import { loadHistory, RUN_TIME_SKIPPING_TESTS, saveHistory, waitUntil } from './helpers'; const test = makeTestFunction({ workflowsPath: __filename, @@ -509,6 +512,7 @@ test("Worker doesn't request Eager Activity Dispatch if no activities are regist const unblockSignal = defineSignal('unblock'); const getBuildIdQuery = defineQuery('getBuildId'); +const startSignal = defineSignal('startSignal'); export async function buildIdTester(): Promise { let blocked = true; diff --git a/packages/test/src/test-otel.ts b/packages/test/src/test-otel.ts index a45a8951a..f893f1259 100644 --- a/packages/test/src/test-otel.ts +++ b/packages/test/src/test-otel.ts @@ -8,7 +8,12 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { ExportResultCode } from '@opentelemetry/core'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; import * as opentelemetry from '@opentelemetry/sdk-node'; -import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { + BasicTracerProvider, + ConsoleSpanExporter, + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import test from 'ava'; import { v4 as uuid4 } from 'uuid'; @@ -24,8 +29,9 @@ import { import { OpenTelemetrySinks, SpanName, SPAN_DELIMITER } from '@temporalio/interceptors-opentelemetry/lib/workflow'; import { DefaultLogger, InjectedSinks, Runtime } from '@temporalio/worker'; import * as activities from './activities'; -import { RUN_INTEGRATION_TESTS, TestWorkflowEnvironment, Worker } from './helpers'; +import { loadHistory, RUN_INTEGRATION_TESTS, saveHistory, TestWorkflowEnvironment, Worker } from './helpers'; import * as workflows from './workflows'; +import { createTestWorkflowBundle } from './helpers-integration'; async function withFakeGrpcServer( fn: (port: number) => Promise, @@ -510,3 +516,65 @@ if (RUN_INTEGRATION_TESTS) { t.is(spans[2].status.code, SpanStatusCode.OK); }); } + +test('Can replay otel history from 1.11.3', async (t) => { + const staticResource = new opentelemetry.resources.Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', + }); + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows/signal-start-otel'), + activities: { + a: async () => 'a', + b: async () => 'b', + c: async () => 'c', + }, + taskQueue: 'test-otel-inbound', + sinks: { + exporter: makeWorkflowExporter(new InMemorySpanExporter(), staticResource), + }, + interceptors: { + workflowModules: [require.resolve('./workflows/signal-start-otel')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + }), + ], + }, + }); + const client = new WorkflowClient(); + + /* + const result = await worker.runUntil(async () => { + const handle = await client.signalWithStart(workflows.signalStartOtel, { + signal: workflows.startSignal, + taskQueue: 'test-otel-inbound', + workflowId: uuid4(), + }); + const result = await handle.result(); + const history = await handle.fetchHistory(); + await saveHistory('otel_current.json', history); + return result; + }); + */ + + const hist = await loadHistory('otel_1_11_3.json'); + Worker.runReplayHistory( + { + workflowBundle: await createTestWorkflowBundle({ + workflowsPath: require.resolve('./workflows/signal-start-otel'), + workflowInterceptorModules: [require.resolve('./workflows/signal-start-otel')], + }), + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + }), + ], + }, + }, + hist + ); + // t.is('abc', result); + t.pass(); +}); diff --git a/packages/test/src/workflows/inbound-signal.ts b/packages/test/src/workflows/inbound-signal.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/test/src/workflows/index.ts b/packages/test/src/workflows/index.ts index d86048b26..92c3a23a0 100644 --- a/packages/test/src/workflows/index.ts +++ b/packages/test/src/workflows/index.ts @@ -65,6 +65,7 @@ export * from './shared-cancellation-scopes'; export * from './noncancellable-awaited-in-root-scope'; export * from './noncancellable-in-noncancellable'; export * from './signal-handlers-clear'; +export * from './signal-start-otel'; export * from './signal-target'; export * from './signals-are-always-processed'; export * from './signals-ordering'; diff --git a/packages/test/src/workflows/signal-start-otel.ts b/packages/test/src/workflows/signal-start-otel.ts new file mode 100644 index 000000000..734e213cf --- /dev/null +++ b/packages/test/src/workflows/signal-start-otel.ts @@ -0,0 +1,29 @@ +import * as workflow from '@temporalio/workflow'; +import { + OpenTelemetryInboundInterceptor, + OpenTelemetryOutboundInterceptor, + OpenTelemetryInternalsInterceptor, +} from '@temporalio/interceptors-opentelemetry/lib/workflow'; + +export const startSignal = workflow.defineSignal('startSignal'); + +const { a, b, c } = workflow.proxyLocalActivities({ + scheduleToCloseTimeout: '1m', +}); + +export async function signalStartOtel(): Promise { + const order = []; + order.push(await a()); + workflow.setHandler(startSignal, async () => { + order.push(await b()); + }); + order.push(await c()); + + return order.join(''); +} + +export const interceptors = (): workflow.WorkflowInterceptors => ({ + inbound: [new OpenTelemetryInboundInterceptor()], + outbound: [new OpenTelemetryOutboundInterceptor()], + internals: [new OpenTelemetryInternalsInterceptor()], +}); From f0efdfdef2047ab6c88292d8142a1ff9e12af806 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 17 Oct 2025 12:06:24 -0400 Subject: [PATCH 02/24] add test for 1.13.1 replay --- packages/test/history_files/otel_1_11_3.json | 4 +- packages/test/history_files/otel_1_13_1.json | 215 +++++++++++++++++++ packages/test/src/test-otel.ts | 98 +++++++-- 3 files changed, 299 insertions(+), 18 deletions(-) create mode 100644 packages/test/history_files/otel_1_13_1.json diff --git a/packages/test/history_files/otel_1_11_3.json b/packages/test/history_files/otel_1_11_3.json index fb6c8a119..df2551177 100644 --- a/packages/test/history_files/otel_1_11_3.json +++ b/packages/test/history_files/otel_1_11_3.json @@ -91,7 +91,7 @@ 2 ], "sdkName": "temporal-typescript", - "sdkVersion": "1.13.1" + "sdkVersion": "1.11.3" }, "meteringMetadata": {} } @@ -212,4 +212,4 @@ } } ] -} \ No newline at end of file +} diff --git a/packages/test/history_files/otel_1_13_1.json b/packages/test/history_files/otel_1_13_1.json new file mode 100644 index 000000000..eae5bf240 --- /dev/null +++ b/packages/test/history_files/otel_1_13_1.json @@ -0,0 +1,215 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2025-10-17T15:54:19.338328Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1049155", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "signalStartOtel" + }, + "taskQueue": { + "name": "test-otel-inbound-curr", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": {}, + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "e4d113d2-d2ab-4220-8ccf-26d89f8f48b2", + "identity": "1661@mac.lan", + "firstExecutionRunId": "e4d113d2-d2ab-4220-8ccf-26d89f8f48b2", + "attempt": 1, + "firstWorkflowTaskBackoff": "0s", + "header": { + "fields": {} + }, + "workflowId": "c62cec51-d338-4d7b-a973-7663de797b3f" + } + }, + { + "eventId": "2", + "eventTime": "2025-10-17T15:54:19.338361Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1049156", + "workflowExecutionSignaledEventAttributes": { + "signalName": "startSignal", + "input": {}, + "identity": "1661@mac.lan", + "header": { + "fields": {} + } + } + }, + { + "eventId": "3", + "eventTime": "2025-10-17T15:54:19.338363Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049157", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "test-otel-inbound-curr", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "4", + "eventTime": "2025-10-17T15:54:19.339448Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049161", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "3", + "identity": "1661@mac.lan", + "requestId": "eed21ba4-1b61-48a5-a3a2-37e633ef2e35", + "historySizeBytes": "338", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+5c835f0ea50dafc7b77a54f34f9a8ec6e81e5e2f7a7cff91c2c52e956e689243" + } + } + }, + { + "eventId": "5", + "eventTime": "2025-10-17T15:54:19.375411Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049165", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "3", + "startedEventId": "4", + "identity": "1661@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+5c835f0ea50dafc7b77a54f34f9a8ec6e81e5e2f7a7cff91c2c52e956e689243" + }, + "sdkMetadata": { + "coreUsedFlags": [ + 2, + 3, + 1 + ], + "langUsedFlags": [ + 2 + ], + "sdkName": "temporal-typescript", + "sdkVersion": "1.13.1" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "6", + "eventTime": "2025-10-17T15:54:19.375433Z", + "eventType": "EVENT_TYPE_MARKER_RECORDED", + "taskId": "1049166", + "markerRecordedEventAttributes": { + "details": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImEi" + } + ] + }, + "data": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzZXEiOjEsImF0dGVtcHQiOjEsImFjdGl2aXR5X2lkIjoiMSIsImFjdGl2aXR5X3R5cGUiOiJhIiwiY29tcGxldGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzE2NDU5LCJuYW5vcyI6MzQwMzQ0MTY2fSwiYmFja29mZiI6bnVsbCwib3JpZ2luYWxfc2NoZWR1bGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzE2NDU5LCJuYW5vcyI6MzY1OTMwMDAwfX0=" + } + ] + } + }, + "markerName": "core_local_activity", + "workflowTaskCompletedEventId": "5" + } + }, + { + "eventId": "7", + "eventTime": "2025-10-17T15:54:19.375435Z", + "eventType": "EVENT_TYPE_MARKER_RECORDED", + "taskId": "1049167", + "markerRecordedEventAttributes": { + "details": { + "data": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzZXEiOjIsImF0dGVtcHQiOjEsImFjdGl2aXR5X2lkIjoiMiIsImFjdGl2aXR5X3R5cGUiOiJjIiwiY29tcGxldGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzE2NDU5LCJuYW5vcyI6MzQwODY5MTY2fSwiYmFja29mZiI6bnVsbCwib3JpZ2luYWxfc2NoZWR1bGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzE2NDU5LCJuYW5vcyI6MzcxNTI1MDAwfX0=" + } + ] + }, + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImMi" + } + ] + } + }, + "markerName": "core_local_activity", + "workflowTaskCompletedEventId": "5" + } + }, + { + "eventId": "8", + "eventTime": "2025-10-17T15:54:19.375440Z", + "eventType": "EVENT_TYPE_MARKER_RECORDED", + "taskId": "1049168", + "markerRecordedEventAttributes": { + "details": { + "data": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzZXEiOjMsImF0dGVtcHQiOjEsImFjdGl2aXR5X2lkIjoiMyIsImFjdGl2aXR5X3R5cGUiOiJiIiwiY29tcGxldGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzE2NDU5LCJuYW5vcyI6MzQxMjEzODc0fSwiYmFja29mZiI6bnVsbCwib3JpZ2luYWxfc2NoZWR1bGVfdGltZSI6eyJzZWNvbmRzIjoxNzYwNzE2NDU5LCJuYW5vcyI6MzcxNTM4MDAwfX0=" + } + ] + }, + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImIi" + } + ] + } + }, + "markerName": "core_local_activity", + "workflowTaskCompletedEventId": "5" + } + }, + { + "eventId": "9", + "eventTime": "2025-10-17T15:54:19.375444Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1049169", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImFjIg==" + } + ] + }, + "workflowTaskCompletedEventId": "5" + } + } + ] +} diff --git a/packages/test/src/test-otel.ts b/packages/test/src/test-otel.ts index f893f1259..4e254d1a2 100644 --- a/packages/test/src/test-otel.ts +++ b/packages/test/src/test-otel.ts @@ -558,23 +558,89 @@ test('Can replay otel history from 1.11.3', async (t) => { */ const hist = await loadHistory('otel_1_11_3.json'); - Worker.runReplayHistory( - { - workflowBundle: await createTestWorkflowBundle({ - workflowsPath: require.resolve('./workflows/signal-start-otel'), - workflowInterceptorModules: [require.resolve('./workflows/signal-start-otel')], - }), - interceptors: { - workflowModules: [require.resolve('./workflows/otel-interceptors')], - activity: [ - (ctx) => ({ - inbound: new OpenTelemetryActivityInboundInterceptor(ctx), - }), - ], + await t.notThrowsAsync(async () => { + await Worker.runReplayHistory( + { + workflowBundle: await createTestWorkflowBundle({ + workflowsPath: require.resolve('./workflows/signal-start-otel'), + workflowInterceptorModules: [require.resolve('./workflows/signal-start-otel')], + }), + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + }), + ], + }, }, - }, - hist - ); + hist + ); + }); // t.is('abc', result); t.pass(); }); + +test('Can replay otel history from 1.13.1', async (t) => { + const staticResource = new opentelemetry.resources.Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', + }); + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows/signal-start-otel'), + activities: { + a: async () => 'a', + b: async () => 'b', + c: async () => 'c', + }, + taskQueue: 'test-otel-inbound-curr', + sinks: { + exporter: makeWorkflowExporter(new InMemorySpanExporter(), staticResource), + }, + interceptors: { + workflowModules: [require.resolve('./workflows/signal-start-otel')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + }), + ], + }, + }); + const client = new WorkflowClient(); + + /* + const result = await worker.runUntil(async () => { + const handle = await client.signalWithStart(workflows.signalStartOtel, { + signal: workflows.startSignal, + taskQueue: 'test-otel-inbound-curr', + workflowId: uuid4(), + }); + const result = await handle.result(); + const history = await handle.fetchHistory(); + await saveHistory('otel_1_13_1.json', history); + return result; + }); + */ + + const hist = await loadHistory('otel_1_13_1.json'); + await t.notThrowsAsync(async () => { + await Worker.runReplayHistory( + { + workflowBundle: await createTestWorkflowBundle({ + workflowsPath: require.resolve('./workflows/signal-start-otel'), + workflowInterceptorModules: [require.resolve('./workflows/signal-start-otel')], + }), + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + }), + ], + }, + }, + hist + ); + }); + // t.is('ac', result); + t.pass(); +}); From 954d706f9656b9a9d75b92c7065110bb32c9c00b Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 20 Oct 2025 16:12:39 -0400 Subject: [PATCH 03/24] update core-sdk --- packages/core-bridge/Cargo.lock | 1 + packages/core-bridge/src/runtime.rs | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/core-bridge/Cargo.lock b/packages/core-bridge/Cargo.lock index 255c47502..c3e0f6f6f 100644 --- a/packages/core-bridge/Cargo.lock +++ b/packages/core-bridge/Cargo.lock @@ -2517,6 +2517,7 @@ dependencies = [ "tracing", "tracing-core", "url", + "uuid", ] [[package]] diff --git a/packages/core-bridge/src/runtime.rs b/packages/core-bridge/src/runtime.rs index 42ad067ef..95b9dfa41 100644 --- a/packages/core-bridge/src/runtime.rs +++ b/packages/core-bridge/src/runtime.rs @@ -6,7 +6,7 @@ use neon::prelude::*; use tracing::{Instrument, warn}; use temporal_sdk_core::{ - CoreRuntime, TokioRuntimeBuilder, + CoreRuntime, RuntimeOptionsBuilder, TokioRuntimeBuilder, api::telemetry::{ CoreLog, OtelCollectorOptions as CoreOtelCollectorOptions, PrometheusExporterOptions as CorePrometheusExporterOptions, metrics::CoreMeter, @@ -62,8 +62,14 @@ pub fn runtime_new( let (telemetry_options, metrics_options, logging_options) = bridge_options.try_into()?; // Create core runtime which starts tokio multi-thread runtime - let mut core_runtime = CoreRuntime::new(telemetry_options, TokioRuntimeBuilder::default()) - .context("Failed to initialize Core Runtime")?; + let mut core_runtime = CoreRuntime::new( + RuntimeOptionsBuilder::default() + .telemetry_options(telemetry_options) + .build() + .expect("RuntimeOptionsBuilder cannot fail"), + TokioRuntimeBuilder::default(), + ) + .context("Failed to initialize Core Runtime")?; enter_sync!(core_runtime); From ef425996f6d2cd1be93db8d7cf4b709df72ddf3d Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 20 Oct 2025 16:43:28 -0400 Subject: [PATCH 04/24] break up --- packages/core-bridge/Cargo.lock | 1 - packages/core-bridge/src/runtime.rs | 7 +--- .../src/instrumentation.ts | 2 +- .../src/worker/index.ts | 2 +- .../src/workflow/index.ts | 4 +- packages/test/src/test-otel.ts | 2 +- packages/worker/src/workflow/vm-shared.ts | 1 + packages/workflow/src/flags.ts | 2 + packages/workflow/src/internals.ts | 39 ++++++++++++++++++- 9 files changed, 48 insertions(+), 12 deletions(-) diff --git a/packages/core-bridge/Cargo.lock b/packages/core-bridge/Cargo.lock index c3e0f6f6f..255c47502 100644 --- a/packages/core-bridge/Cargo.lock +++ b/packages/core-bridge/Cargo.lock @@ -2517,7 +2517,6 @@ dependencies = [ "tracing", "tracing-core", "url", - "uuid", ] [[package]] diff --git a/packages/core-bridge/src/runtime.rs b/packages/core-bridge/src/runtime.rs index 95b9dfa41..af340af95 100644 --- a/packages/core-bridge/src/runtime.rs +++ b/packages/core-bridge/src/runtime.rs @@ -6,7 +6,7 @@ use neon::prelude::*; use tracing::{Instrument, warn}; use temporal_sdk_core::{ - CoreRuntime, RuntimeOptionsBuilder, TokioRuntimeBuilder, + CoreRuntime, TokioRuntimeBuilder, api::telemetry::{ CoreLog, OtelCollectorOptions as CoreOtelCollectorOptions, PrometheusExporterOptions as CorePrometheusExporterOptions, metrics::CoreMeter, @@ -63,10 +63,7 @@ pub fn runtime_new( // Create core runtime which starts tokio multi-thread runtime let mut core_runtime = CoreRuntime::new( - RuntimeOptionsBuilder::default() - .telemetry_options(telemetry_options) - .build() - .expect("RuntimeOptionsBuilder cannot fail"), + telemetry_options, TokioRuntimeBuilder::default(), ) .context("Failed to initialize Core Runtime")?; diff --git a/packages/interceptors-opentelemetry/src/instrumentation.ts b/packages/interceptors-opentelemetry/src/instrumentation.ts index 85adb1fa3..cf53a425a 100644 --- a/packages/interceptors-opentelemetry/src/instrumentation.ts +++ b/packages/interceptors-opentelemetry/src/instrumentation.ts @@ -20,7 +20,7 @@ const payloadConverter = defaultPayloadConverter; /** * If found, return an otel Context deserialized from the provided headers */ -export async function extractContextFromHeaders(headers: Headers): Promise { +export function extractContextFromHeaders(headers: Headers): otel.Context | undefined { const encodedSpanContext = headers[TRACE_HEADER]; if (encodedSpanContext === undefined) { return undefined; diff --git a/packages/interceptors-opentelemetry/src/worker/index.ts b/packages/interceptors-opentelemetry/src/worker/index.ts index 0be3916c6..506200bdb 100644 --- a/packages/interceptors-opentelemetry/src/worker/index.ts +++ b/packages/interceptors-opentelemetry/src/worker/index.ts @@ -34,7 +34,7 @@ export class OpenTelemetryActivityInboundInterceptor implements ActivityInboundC } async execute(input: ActivityExecuteInput, next: Next): Promise { - const context = await extractContextFromHeaders(input.headers); + const context = extractContextFromHeaders(input.headers); const spanName = `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}${this.ctx.info.activityType}`; return await instrument({ tracer: this.tracer, spanName, fn: () => next(input), context }); } diff --git a/packages/interceptors-opentelemetry/src/workflow/index.ts b/packages/interceptors-opentelemetry/src/workflow/index.ts index 5895c5a6a..0f54f82f2 100644 --- a/packages/interceptors-opentelemetry/src/workflow/index.ts +++ b/packages/interceptors-opentelemetry/src/workflow/index.ts @@ -62,7 +62,7 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte next: Next ): Promise { const { workflowInfo, ContinueAsNew } = getWorkflowModule(); - const context = await extractContextFromHeaders(input.headers); + const context = await Promise.resolve(extractContextFromHeaders(input.headers)); return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}${workflowInfo().workflowType}`, @@ -76,7 +76,7 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte input: SignalInput, next: Next ): Promise { - const context = await extractContextFromHeaders(input.headers); + const context = extractContextFromHeaders(input.headers); return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, diff --git a/packages/test/src/test-otel.ts b/packages/test/src/test-otel.ts index 4e254d1a2..19d37e218 100644 --- a/packages/test/src/test-otel.ts +++ b/packages/test/src/test-otel.ts @@ -630,7 +630,7 @@ test('Can replay otel history from 1.13.1', async (t) => { workflowInterceptorModules: [require.resolve('./workflows/signal-start-otel')], }), interceptors: { - workflowModules: [require.resolve('./workflows/otel-interceptors')], + workflowModules: [require.resolve('./workflows/signal-start-otel')], activity: [ (ctx) => ({ inbound: new OpenTelemetryActivityInboundInterceptor(ctx), diff --git a/packages/worker/src/workflow/vm-shared.ts b/packages/worker/src/workflow/vm-shared.ts index 6db33fbf6..5506d8a0d 100644 --- a/packages/worker/src/workflow/vm-shared.ts +++ b/packages/worker/src/workflow/vm-shared.ts @@ -357,6 +357,7 @@ export abstract class BaseVMWorkflow implements Workflow { }, })); this.activator.addKnownFlags(activation.availableInternalFlags ?? []); + if (activation.lastSdkVersion) this.activator.sdkVersion = activation.lastSdkVersion; // Initialization of the workflow must happen before anything else. Yet, keep the init job in // place in the list as we'll use it as a marker to know when to start the workflow function. diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index 1ed209b13..4d680482c 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -44,6 +44,8 @@ export const SdkFlags = { * to implicitely have this flag on. */ ProcessWorkflowActivationJobsAsSingleBatch: defineFlag(2, true, [buildIdSdkVersionMatches(/1\.11\.[01]/)]), + + OpenTelemetryInterceptorInsertYieldPoint: defineFlag(3, false, [({ info }) => false]), } as const; function defineFlag(id: number, def: boolean, alternativeConditions?: AltConditionFn[]): SdkFlag { diff --git a/packages/workflow/src/internals.ts b/packages/workflow/src/internals.ts index 25a6eb737..496275b09 100644 --- a/packages/workflow/src/internals.ts +++ b/packages/workflow/src/internals.ts @@ -434,6 +434,8 @@ export class Activator implements ActivationHandler { private readonly knownFlags = new Set(); + sdkVersion?: string; + /** * Buffered sink calls per activation */ @@ -978,7 +980,22 @@ export class Activator implements ActivationHandler { const signalExecutionNum = this.signalHandlerExecutionSeq++; this.inProgressSignals.set(signalExecutionNum, { name: signalName, unfinishedPolicy }); - const execute = composeInterceptors(interceptors, 'handleSignal', this.signalWorkflowNextHandler.bind(this)); + const injectYield = shouldInjectYield(this.sdkVersion); + const addedInterceptor: WorkflowInterceptors['inbound'] = injectYield + ? [ + { + handleSignal: async (input, next) => { + await Promise.resolve(); + return next(input); + }, + }, + ] + : []; + const execute = composeInterceptors( + [...addedInterceptor, ...interceptors], + 'handleSignal', + this.signalWorkflowNextHandler.bind(this) + ); execute({ args: arrayFromPayloads(this.payloadConverter, activation.input), signalName, @@ -1305,3 +1322,23 @@ then you can disable this warning by passing an option when setting the handler: Array.from(names.entries()).map(([name, count]) => ({ name, count })) )}`; } + +function shouldInjectYield(version?: string): boolean { + if (!version) { + return false; + } + const [major, minor, patch] = version.split('.'); + // 1.11.5 - 1.13.1: need to inject + if (major !== '1') return false; + + switch (minor) { + case '11': + return patch === '5'; + case '12': + return true; + case '13': + return patch === '1'; + default: + return false; + } +} From b205aa9cabf9513e5fea8c95118e39a7aefd8df3 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 20 Oct 2025 16:59:48 -0400 Subject: [PATCH 05/24] fix: no longer yield during OTEL handleSignal --- .../src/worker/index.ts | 2 +- .../src/workflow/index.ts | 1 + packages/test/src/helpers-integration.ts | 27 -------- .../test/src/test-integration-workflows.ts | 6 +- packages/test/src/test-otel.ts | 15 +--- packages/workflow/src/flags.ts | 2 - packages/workflow/src/internals.ts | 69 +++++++++++++++---- 7 files changed, 60 insertions(+), 62 deletions(-) diff --git a/packages/interceptors-opentelemetry/src/worker/index.ts b/packages/interceptors-opentelemetry/src/worker/index.ts index 506200bdb..4d2334a60 100644 --- a/packages/interceptors-opentelemetry/src/worker/index.ts +++ b/packages/interceptors-opentelemetry/src/worker/index.ts @@ -34,7 +34,7 @@ export class OpenTelemetryActivityInboundInterceptor implements ActivityInboundC } async execute(input: ActivityExecuteInput, next: Next): Promise { - const context = extractContextFromHeaders(input.headers); + const context = await Promise.resolve(extractContextFromHeaders(input.headers)); const spanName = `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}${this.ctx.info.activityType}`; return await instrument({ tracer: this.tracer, spanName, fn: () => next(input), context }); } diff --git a/packages/interceptors-opentelemetry/src/workflow/index.ts b/packages/interceptors-opentelemetry/src/workflow/index.ts index 0f54f82f2..c90b08125 100644 --- a/packages/interceptors-opentelemetry/src/workflow/index.ts +++ b/packages/interceptors-opentelemetry/src/workflow/index.ts @@ -52,6 +52,7 @@ function getTracer(): otel.Tracer { */ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInterceptor { protected readonly tracer = getTracer(); + protected readonly maybeInjectYield = true; public constructor() { ensureWorkflowModuleLoaded(); diff --git a/packages/test/src/helpers-integration.ts b/packages/test/src/helpers-integration.ts index 556bc7c15..9d0d6fab0 100644 --- a/packages/test/src/helpers-integration.ts +++ b/packages/test/src/helpers-integration.ts @@ -6,10 +6,8 @@ import { WorkflowFailedError, WorkflowHandle, WorkflowHandleWithFirstExecutionRunId, - WorkflowHandleWithSignaledRunId, WorkflowStartOptions, WorkflowUpdateFailedError, - WorkflowSignalWithStartOptionsWithArgs, } from '@temporalio/client'; import { LocalTestWorkflowEnvironmentOptions, @@ -197,16 +195,6 @@ export interface Helpers { fn: T, opts: Omit & Partial> ): Promise>; - signalWithStart( - fn: T, - signal: workflow.SignalDefinition - ): Promise>; - signalWithStart( - fn: T, - signal: workflow.SignalDefinition, - opts: Omit, 'taskQueue' | 'workflowId'> & - Partial> - ): Promise>; assertWorkflowUpdateFailed(p: Promise, causeConstructor: ErrorConstructor, message?: string): Promise; assertWorkflowFailedError(p: Promise, causeConstructor: ErrorConstructor, message?: string): Promise; updateHasBeenAdmitted(handle: WorkflowHandle, updateId: string): Promise; @@ -262,21 +250,6 @@ export function configurableHelpers( ...opts, }); }, - - async signalWithStart( - fn: workflow.Workflow, - signal: workflow.SignalDefinition, - opts?: Omit, 'taskQueue' | 'workflowId'> & - Partial> - ): Promise> { - return await testEnv.client.workflow.signalWithStart(fn, { - signal, - taskQueue, - workflowId: randomUUID(), - ...opts, - }); - }, - async assertWorkflowUpdateFailed( p: Promise, causeConstructor: ErrorConstructor, diff --git a/packages/test/src/test-integration-workflows.ts b/packages/test/src/test-integration-workflows.ts index f704e8e01..3b0604d17 100644 --- a/packages/test/src/test-integration-workflows.ts +++ b/packages/test/src/test-integration-workflows.ts @@ -1,6 +1,3 @@ -import * as opentelemetry from '@opentelemetry/sdk-node'; -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'; import { setTimeout as setTimeoutPromise } from 'timers/promises'; import { randomUUID } from 'crypto'; import asyncRetry from 'async-retry'; @@ -48,7 +45,7 @@ import * as workflows from './workflows'; import { Context, createLocalTestEnvironment, helpers, makeTestFunction } from './helpers-integration'; import { overrideSdkInternalFlag } from './mock-internal-flags'; import { ActivityState, heartbeatCancellationDetailsActivity } from './activities/heartbeat-cancellation-details'; -import { loadHistory, RUN_TIME_SKIPPING_TESTS, saveHistory, waitUntil } from './helpers'; +import { loadHistory, RUN_TIME_SKIPPING_TESTS, waitUntil } from './helpers'; const test = makeTestFunction({ workflowsPath: __filename, @@ -512,7 +509,6 @@ test("Worker doesn't request Eager Activity Dispatch if no activities are regist const unblockSignal = defineSignal('unblock'); const getBuildIdQuery = defineQuery('getBuildId'); -const startSignal = defineSignal('startSignal'); export async function buildIdTester(): Promise { let blocked = true; diff --git a/packages/test/src/test-otel.ts b/packages/test/src/test-otel.ts index 19d37e218..c34166c8c 100644 --- a/packages/test/src/test-otel.ts +++ b/packages/test/src/test-otel.ts @@ -8,12 +8,7 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { ExportResultCode } from '@opentelemetry/core'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; import * as opentelemetry from '@opentelemetry/sdk-node'; -import { - BasicTracerProvider, - ConsoleSpanExporter, - InMemorySpanExporter, - SimpleSpanProcessor, -} from '@opentelemetry/sdk-trace-base'; +import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import test from 'ava'; import { v4 as uuid4 } from 'uuid'; @@ -518,6 +513,7 @@ if (RUN_INTEGRATION_TESTS) { } test('Can replay otel history from 1.11.3', async (t) => { + /* const staticResource = new opentelemetry.resources.Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', }); @@ -543,7 +539,6 @@ test('Can replay otel history from 1.11.3', async (t) => { }); const client = new WorkflowClient(); - /* const result = await worker.runUntil(async () => { const handle = await client.signalWithStart(workflows.signalStartOtel, { signal: workflows.startSignal, @@ -577,11 +572,10 @@ test('Can replay otel history from 1.11.3', async (t) => { hist ); }); - // t.is('abc', result); - t.pass(); }); test('Can replay otel history from 1.13.1', async (t) => { + /* const staticResource = new opentelemetry.resources.Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', }); @@ -607,7 +601,6 @@ test('Can replay otel history from 1.13.1', async (t) => { }); const client = new WorkflowClient(); - /* const result = await worker.runUntil(async () => { const handle = await client.signalWithStart(workflows.signalStartOtel, { signal: workflows.startSignal, @@ -641,6 +634,4 @@ test('Can replay otel history from 1.13.1', async (t) => { hist ); }); - // t.is('ac', result); - t.pass(); }); diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index 4d680482c..1ed209b13 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -44,8 +44,6 @@ export const SdkFlags = { * to implicitely have this flag on. */ ProcessWorkflowActivationJobsAsSingleBatch: defineFlag(2, true, [buildIdSdkVersionMatches(/1\.11\.[01]/)]), - - OpenTelemetryInterceptorInsertYieldPoint: defineFlag(3, false, [({ info }) => false]), } as const; function defineFlag(id: number, def: boolean, alternativeConditions?: AltConditionFn[]): SdkFlag { diff --git a/packages/workflow/src/internals.ts b/packages/workflow/src/internals.ts index 496275b09..fb81b168d 100644 --- a/packages/workflow/src/internals.ts +++ b/packages/workflow/src/internals.ts @@ -980,19 +980,8 @@ export class Activator implements ActivationHandler { const signalExecutionNum = this.signalHandlerExecutionSeq++; this.inProgressSignals.set(signalExecutionNum, { name: signalName, unfinishedPolicy }); - const injectYield = shouldInjectYield(this.sdkVersion); - const addedInterceptor: WorkflowInterceptors['inbound'] = injectYield - ? [ - { - handleSignal: async (input, next) => { - await Promise.resolve(); - return next(input); - }, - }, - ] - : []; const execute = composeInterceptors( - [...addedInterceptor, ...interceptors], + this.maybeInjectYieldForOtelHandler(interceptors), 'handleSignal', this.signalWorkflowNextHandler.bind(this) ); @@ -1272,6 +1261,31 @@ export class Activator implements ActivationHandler { failureToError(failure: ProtoFailure): Error { return this.failureConverter.failureToError(failure, this.payloadConverter); } + + private maybeInjectYieldForOtelHandler( + interceptors: NonNullable + ): NonNullable { + if (!this.info.unsafe.isReplaying || !shouldInjectYield(this.sdkVersion)) { + return [...interceptors]; + } + const otelInboundInterceptorIndex = findOpenTelemetryInboundInterceptor(interceptors); + if (otelInboundInterceptorIndex === null) { + return [...interceptors]; + } + // A handler that only serves the insert a yield point in the interceptor handlers + const yieldHandleSignalInterceptor: NonNullable[number] = { + handleSignal: async (input, next) => { + await Promise.resolve(); + return next(input); + }, + }; + // Insert the yield handler before the OTEL one to synthesize the yield point added in the affected versions of the handler + return [ + ...interceptors.slice(0, otelInboundInterceptorIndex), + yieldHandleSignalInterceptor, + ...interceptors.slice(otelInboundInterceptorIndex), + ]; + } } function getSeq(activation: T): number { @@ -1323,22 +1337,47 @@ then you can disable this warning by passing an option when setting the handler: )}`; } +// Should only get run on replay function shouldInjectYield(version?: string): boolean { if (!version) { return false; } - const [major, minor, patch] = version.split('.'); + const [major, minor, patchAndTags] = version.split('.', 3); // 1.11.5 - 1.13.1: need to inject if (major !== '1') return false; + // patch might have some extra stuff that needs cleaning + // basically "takeWhile digit" + let patch; + try { + const patchDigits = /[0-9]+/.exec(patchAndTags)?.[0]; + patch = patchDigits ? Number.parseInt(patchDigits) : null; + } catch { + patch = null; + } + switch (minor) { case '11': - return patch === '5'; + // 1.11.3 was the last release that didn't inject a yield point + return Boolean(patch && patch > 3); case '12': + // Every 1.12 release requires a yield return true; case '13': - return patch === '1'; + // 1.13.2 will be the first release since 1.11.3 that doesn't have a yield point in `handleSignal` + return Boolean(patch && patch < 2); default: return false; } } + +function findOpenTelemetryInboundInterceptor( + interceptors: NonNullable +): number | null { + const index = interceptors.findIndex( + (interceptor) => + // We use a marker instead of `instanceof` to avoid taking a dependency on @temporalio/interceptors-opentelemetry + (interceptor as NonNullable & { maybeInjectYield: boolean }).maybeInjectYield + ); + return index !== -1 ? index : null; +} From 3fd63f3301bc102a6938d7ba25276a21ddc6639c Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 21 Oct 2025 14:55:45 -0400 Subject: [PATCH 06/24] chore: move conditional logic to SDK flag --- packages/test/src/test-flags.ts | 27 ++++++++++++++++ packages/workflow/src/flags.ts | 49 +++++++++++++++++++++++++++++- packages/workflow/src/internals.ts | 6 ++-- 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 packages/test/src/test-flags.ts diff --git a/packages/test/src/test-flags.ts b/packages/test/src/test-flags.ts new file mode 100644 index 000000000..89a993a0a --- /dev/null +++ b/packages/test/src/test-flags.ts @@ -0,0 +1,27 @@ +import test from 'ava'; +import { SdkFlags } from '@temporalio/workflow/lib/flags'; +import type { WorkflowInfo } from '@temporalio/workflow'; + +test('OpenTelemetryHandleSignalInterceptorInsertYield enabled by version', (t) => { + const cases = [ + { version: '1.0.0', expected: false }, + { version: '1.11.3', expected: false }, + { version: '1.11.5', expected: true }, + { version: '1.11.6', expected: true }, + { version: '1.12.0', expected: true }, + { version: '1.13.1', expected: true }, + { version: '1.13.2', expected: false }, + { version: '1.14.0', expected: false }, + ]; + for (const { version, expected } of cases) { + const actual = SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield.alternativeConditions![0]!({ + info: {} as WorkflowInfo, + sdkVersion: version, + }); + t.is( + actual, + expected, + `Expected OpenTelemetryHandleSignalInterceptorInsertYield on ${version} to evaluate as ${expected}` + ); + } +}); diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index 1ed209b13..b1efd4a63 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -44,6 +44,19 @@ export const SdkFlags = { * to implicitely have this flag on. */ ProcessWorkflowActivationJobsAsSingleBatch: defineFlag(2, true, [buildIdSdkVersionMatches(/1\.11\.[01]/)]), + /** + * In 1.11.3 and previous versions, the interceptor for `handleSignal` provided + * by @temporalio/interceptors-opentelemetry did not have a yield point in it. + * A yield point was accidentally added in later versions. This added yield point + * can cause NDE if there was a signal handler and the workflow was started with a signal. + * + * This yield point was removed in 1.13.2, but in order to prevent workflows from the + * affected versions resulting in NDE, we have to inject the yield point on replay. + * This flag should be enabled for SDK versions newer than 1.11.3 or older than 1.13.2. + * + * @since Introduced in 1.13.2. + */ + OpenTelemetryHandleSignalInterceptorInsertYield: defineFlag(3, false, [affectedOtelInterceptorVersion]), } as const; function defineFlag(id: number, def: boolean, alternativeConditions?: AltConditionFn[]): SdkFlag { @@ -68,9 +81,43 @@ export function assertValidFlag(id: number): void { * condition no longer holds. This is so to avoid incorrect behaviors in case where a Workflow * Execution has gone through a newer SDK version then again through an older one. */ -type AltConditionFn = (ctx: { info: WorkflowInfo }) => boolean; +type AltConditionFn = (ctx: { info: WorkflowInfo; sdkVersion?: string }) => boolean; function buildIdSdkVersionMatches(version: RegExp): AltConditionFn { const regex = new RegExp(`^@temporalio/worker@(${version.source})[+]`); return ({ info }) => info.currentBuildId != null && regex.test(info.currentBuildId); // eslint-disable-line deprecation/deprecation } + +function affectedOtelInterceptorVersion({ sdkVersion }: { sdkVersion?: string }): boolean { + if (!sdkVersion) { + return false; + } + const [major, minor, patchAndTags] = sdkVersion.split('.', 3); + if (major !== '1') return false; + + // Semver allows for additional tags to be appended to the version + let patch; + try { + const patchDigits = /[0-9]+/.exec(patchAndTags)?.[0]; + patch = patchDigits ? Number.parseInt(patchDigits) : null; + } catch { + // This shouldn't ever happen, but we are conservative here and avoid throwing when checking a flag. + patch = null; + } + + switch (minor) { + case '11': + // 1.11.3 was the last release that didn't inject a yield point + // If for some reason we are unable to parse the patch version, assume it isn't affected + return Boolean(patch && patch > 3); + case '12': + // Every 1.12 release requires a yield + return true; + case '13': + // 1.13.2 will be the first release since 1.11.3 that doesn't have a yield point in `handleSignal` + // If for some reason we are unable to parse the patch version, assume it isn't affected + return Boolean(patch && patch < 2); + default: + return false; + } +} diff --git a/packages/workflow/src/internals.ts b/packages/workflow/src/internals.ts index fb81b168d..017b810c8 100644 --- a/packages/workflow/src/internals.ts +++ b/packages/workflow/src/internals.ts @@ -65,7 +65,7 @@ import { import { type SinkCall } from './sinks'; import { untrackPromise } from './stack-helpers'; import pkg from './pkg'; -import { SdkFlag, assertValidFlag } from './flags'; +import { SdkFlag, SdkFlags, assertValidFlag } from './flags'; import { executeWithLifecycleLogging, log } from './logs'; const StartChildWorkflowExecutionFailedCause = { @@ -1129,7 +1129,7 @@ export class Activator implements ActivationHandler { // through an older one. if (this.info.unsafe.isReplaying && flag.alternativeConditions) { for (const cond of flag.alternativeConditions) { - if (cond({ info: this.info })) return true; + if (cond({ info: this.info, sdkVersion: this.sdkVersion })) return true; } } @@ -1265,7 +1265,7 @@ export class Activator implements ActivationHandler { private maybeInjectYieldForOtelHandler( interceptors: NonNullable ): NonNullable { - if (!this.info.unsafe.isReplaying || !shouldInjectYield(this.sdkVersion)) { + if (!this.hasFlag(SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield)) { return [...interceptors]; } const otelInboundInterceptorIndex = findOpenTelemetryInboundInterceptor(interceptors); From 9de4ee8b764a17649f3f9eee6883af1d203b0788 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 21 Oct 2025 15:03:17 -0400 Subject: [PATCH 07/24] chore: move flag checking logic to interceptor --- .../src/workflow/index.ts | 6 +- .../src/workflow/workflow-module-loader.ts | 8 ++ packages/test/src/workflows/inbound-signal.ts | 0 packages/workflow/src/internals.ts | 76 +------------------ 4 files changed, 13 insertions(+), 77 deletions(-) delete mode 100644 packages/test/src/workflows/inbound-signal.ts diff --git a/packages/interceptors-opentelemetry/src/workflow/index.ts b/packages/interceptors-opentelemetry/src/workflow/index.ts index c90b08125..f9b7bd2fa 100644 --- a/packages/interceptors-opentelemetry/src/workflow/index.ts +++ b/packages/interceptors-opentelemetry/src/workflow/index.ts @@ -22,7 +22,7 @@ import { instrument, extractContextFromHeaders, headersWithContext } from '../in import { ContextManager } from './context-manager'; import { SpanName, SPAN_DELIMITER } from './definitions'; import { SpanExporter } from './span-exporter'; -import { ensureWorkflowModuleLoaded, getWorkflowModule } from './workflow-module-loader'; +import { ensureWorkflowModuleLoaded, getSdkFlagsChecking, getWorkflowModule } from './workflow-module-loader'; export * from './definitions'; @@ -77,12 +77,14 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte input: SignalInput, next: Next ): Promise { + const { getActivator, SdkFlags } = getSdkFlagsChecking(); + const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield); const context = extractContextFromHeaders(input.headers); return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, fn: () => next(input), - context, + context: shouldInjectYield ? await Promise.resolve(context) : context, }); } } diff --git a/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts b/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts index f925bc33f..652417ca8 100644 --- a/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts +++ b/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts @@ -3,6 +3,8 @@ * @module */ import type * as WorkflowModule from '@temporalio/workflow'; +import type { SdkFlags as SdkFlagsT } from '@temporalio/workflow/lib/flags'; +import type { getActivator as getActivatorT } from '@temporalio/workflow/lib/global-attributes'; // @temporalio/workflow is an optional peer dependency. // It can be missing as long as the user isn't attempting to construct a workflow interceptor. @@ -30,6 +32,12 @@ export function getWorkflowModule(): typeof WorkflowModule { return workflowModule!; } +export function getSdkFlagsChecking(): { getActivator: typeof getActivatorT; SdkFlags: typeof SdkFlagsT } { + const { SdkFlags } = require('@temporalio/workflow/lib/flags'); + const { getActivator } = require('@temporalio/workflow/lib/global-attributes'); + return { getActivator, SdkFlags }; +} + /** * Checks if the workflow module loaded successfully and throws if not. */ diff --git a/packages/test/src/workflows/inbound-signal.ts b/packages/test/src/workflows/inbound-signal.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/workflow/src/internals.ts b/packages/workflow/src/internals.ts index 017b810c8..ae971710a 100644 --- a/packages/workflow/src/internals.ts +++ b/packages/workflow/src/internals.ts @@ -980,11 +980,7 @@ export class Activator implements ActivationHandler { const signalExecutionNum = this.signalHandlerExecutionSeq++; this.inProgressSignals.set(signalExecutionNum, { name: signalName, unfinishedPolicy }); - const execute = composeInterceptors( - this.maybeInjectYieldForOtelHandler(interceptors), - 'handleSignal', - this.signalWorkflowNextHandler.bind(this) - ); + const execute = composeInterceptors(interceptors, 'handleSignal', this.signalWorkflowNextHandler.bind(this)); execute({ args: arrayFromPayloads(this.payloadConverter, activation.input), signalName, @@ -1261,31 +1257,6 @@ export class Activator implements ActivationHandler { failureToError(failure: ProtoFailure): Error { return this.failureConverter.failureToError(failure, this.payloadConverter); } - - private maybeInjectYieldForOtelHandler( - interceptors: NonNullable - ): NonNullable { - if (!this.hasFlag(SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield)) { - return [...interceptors]; - } - const otelInboundInterceptorIndex = findOpenTelemetryInboundInterceptor(interceptors); - if (otelInboundInterceptorIndex === null) { - return [...interceptors]; - } - // A handler that only serves the insert a yield point in the interceptor handlers - const yieldHandleSignalInterceptor: NonNullable[number] = { - handleSignal: async (input, next) => { - await Promise.resolve(); - return next(input); - }, - }; - // Insert the yield handler before the OTEL one to synthesize the yield point added in the affected versions of the handler - return [ - ...interceptors.slice(0, otelInboundInterceptorIndex), - yieldHandleSignalInterceptor, - ...interceptors.slice(otelInboundInterceptorIndex), - ]; - } } function getSeq(activation: T): number { @@ -1336,48 +1307,3 @@ then you can disable this warning by passing an option when setting the handler: Array.from(names.entries()).map(([name, count]) => ({ name, count })) )}`; } - -// Should only get run on replay -function shouldInjectYield(version?: string): boolean { - if (!version) { - return false; - } - const [major, minor, patchAndTags] = version.split('.', 3); - // 1.11.5 - 1.13.1: need to inject - if (major !== '1') return false; - - // patch might have some extra stuff that needs cleaning - // basically "takeWhile digit" - let patch; - try { - const patchDigits = /[0-9]+/.exec(patchAndTags)?.[0]; - patch = patchDigits ? Number.parseInt(patchDigits) : null; - } catch { - patch = null; - } - - switch (minor) { - case '11': - // 1.11.3 was the last release that didn't inject a yield point - return Boolean(patch && patch > 3); - case '12': - // Every 1.12 release requires a yield - return true; - case '13': - // 1.13.2 will be the first release since 1.11.3 that doesn't have a yield point in `handleSignal` - return Boolean(patch && patch < 2); - default: - return false; - } -} - -function findOpenTelemetryInboundInterceptor( - interceptors: NonNullable -): number | null { - const index = interceptors.findIndex( - (interceptor) => - // We use a marker instead of `instanceof` to avoid taking a dependency on @temporalio/interceptors-opentelemetry - (interceptor as NonNullable & { maybeInjectYield: boolean }).maybeInjectYield - ); - return index !== -1 ? index : null; -} From cb95455d84188158da7ed41c777aa0e7a7f55f2f Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 21 Oct 2025 15:36:28 -0400 Subject: [PATCH 08/24] history creation code --- packages/test/src/test-otel.ts | 80 +--------------------------------- 1 file changed, 1 insertion(+), 79 deletions(-) diff --git a/packages/test/src/test-otel.ts b/packages/test/src/test-otel.ts index c34166c8c..e4771d0b3 100644 --- a/packages/test/src/test-otel.ts +++ b/packages/test/src/test-otel.ts @@ -24,7 +24,7 @@ import { import { OpenTelemetrySinks, SpanName, SPAN_DELIMITER } from '@temporalio/interceptors-opentelemetry/lib/workflow'; import { DefaultLogger, InjectedSinks, Runtime } from '@temporalio/worker'; import * as activities from './activities'; -import { loadHistory, RUN_INTEGRATION_TESTS, saveHistory, TestWorkflowEnvironment, Worker } from './helpers'; +import { loadHistory, RUN_INTEGRATION_TESTS, TestWorkflowEnvironment, Worker } from './helpers'; import * as workflows from './workflows'; import { createTestWorkflowBundle } from './helpers-integration'; @@ -513,45 +513,6 @@ if (RUN_INTEGRATION_TESTS) { } test('Can replay otel history from 1.11.3', async (t) => { - /* - const staticResource = new opentelemetry.resources.Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', - }); - const worker = await Worker.create({ - workflowsPath: require.resolve('./workflows/signal-start-otel'), - activities: { - a: async () => 'a', - b: async () => 'b', - c: async () => 'c', - }, - taskQueue: 'test-otel-inbound', - sinks: { - exporter: makeWorkflowExporter(new InMemorySpanExporter(), staticResource), - }, - interceptors: { - workflowModules: [require.resolve('./workflows/signal-start-otel')], - activity: [ - (ctx) => ({ - inbound: new OpenTelemetryActivityInboundInterceptor(ctx), - }), - ], - }, - }); - const client = new WorkflowClient(); - - const result = await worker.runUntil(async () => { - const handle = await client.signalWithStart(workflows.signalStartOtel, { - signal: workflows.startSignal, - taskQueue: 'test-otel-inbound', - workflowId: uuid4(), - }); - const result = await handle.result(); - const history = await handle.fetchHistory(); - await saveHistory('otel_current.json', history); - return result; - }); - */ - const hist = await loadHistory('otel_1_11_3.json'); await t.notThrowsAsync(async () => { await Worker.runReplayHistory( @@ -575,45 +536,6 @@ test('Can replay otel history from 1.11.3', async (t) => { }); test('Can replay otel history from 1.13.1', async (t) => { - /* - const staticResource = new opentelemetry.resources.Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', - }); - const worker = await Worker.create({ - workflowsPath: require.resolve('./workflows/signal-start-otel'), - activities: { - a: async () => 'a', - b: async () => 'b', - c: async () => 'c', - }, - taskQueue: 'test-otel-inbound-curr', - sinks: { - exporter: makeWorkflowExporter(new InMemorySpanExporter(), staticResource), - }, - interceptors: { - workflowModules: [require.resolve('./workflows/signal-start-otel')], - activity: [ - (ctx) => ({ - inbound: new OpenTelemetryActivityInboundInterceptor(ctx), - }), - ], - }, - }); - const client = new WorkflowClient(); - - const result = await worker.runUntil(async () => { - const handle = await client.signalWithStart(workflows.signalStartOtel, { - signal: workflows.startSignal, - taskQueue: 'test-otel-inbound-curr', - workflowId: uuid4(), - }); - const result = await handle.result(); - const history = await handle.fetchHistory(); - await saveHistory('otel_1_13_1.json', history); - return result; - }); - */ - const hist = await loadHistory('otel_1_13_1.json'); await t.notThrowsAsync(async () => { await Worker.runReplayHistory( From 6f53259084edb2e1d526f7c3bfe09c45889de29b Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 21 Oct 2025 16:28:42 -0400 Subject: [PATCH 09/24] run formatter --- packages/test/history_files/otel_1_11_3.json | 10 ++-------- packages/test/history_files/otel_1_13_1.json | 10 ++-------- packages/workflow/src/internals.ts | 2 +- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/test/history_files/otel_1_11_3.json b/packages/test/history_files/otel_1_11_3.json index df2551177..1fa2880ec 100644 --- a/packages/test/history_files/otel_1_11_3.json +++ b/packages/test/history_files/otel_1_11_3.json @@ -82,14 +82,8 @@ "buildId": "@temporalio/worker@1.13.1+6b92c11c4f907379345c3513a5f749c90d752cfd0a3bf888bf0be04350bb0d2e" }, "sdkMetadata": { - "coreUsedFlags": [ - 1, - 3, - 2 - ], - "langUsedFlags": [ - 2 - ], + "coreUsedFlags": [1, 3, 2], + "langUsedFlags": [2], "sdkName": "temporal-typescript", "sdkVersion": "1.11.3" }, diff --git a/packages/test/history_files/otel_1_13_1.json b/packages/test/history_files/otel_1_13_1.json index eae5bf240..11e4f0d96 100644 --- a/packages/test/history_files/otel_1_13_1.json +++ b/packages/test/history_files/otel_1_13_1.json @@ -82,14 +82,8 @@ "buildId": "@temporalio/worker@1.13.1+5c835f0ea50dafc7b77a54f34f9a8ec6e81e5e2f7a7cff91c2c52e956e689243" }, "sdkMetadata": { - "coreUsedFlags": [ - 2, - 3, - 1 - ], - "langUsedFlags": [ - 2 - ], + "coreUsedFlags": [2, 3, 1], + "langUsedFlags": [2], "sdkName": "temporal-typescript", "sdkVersion": "1.13.1" }, diff --git a/packages/workflow/src/internals.ts b/packages/workflow/src/internals.ts index ae971710a..033656b4f 100644 --- a/packages/workflow/src/internals.ts +++ b/packages/workflow/src/internals.ts @@ -65,7 +65,7 @@ import { import { type SinkCall } from './sinks'; import { untrackPromise } from './stack-helpers'; import pkg from './pkg'; -import { SdkFlag, SdkFlags, assertValidFlag } from './flags'; +import { SdkFlag, assertValidFlag } from './flags'; import { executeWithLifecycleLogging, log } from './logs'; const StartChildWorkflowExecutionFailedCause = { From b1c3a478b9fdb6307fb87e2f1b2cda674a30373c Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Wed, 22 Oct 2025 12:28:50 -0400 Subject: [PATCH 10/24] chore: refactor semver checking to be more robust --- packages/test/src/test-flags.ts | 10 +++- packages/workflow/src/flags.ts | 97 ++++++++++++++++++++++----------- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/packages/test/src/test-flags.ts b/packages/test/src/test-flags.ts index 89a993a0a..0a1061e5e 100644 --- a/packages/test/src/test-flags.ts +++ b/packages/test/src/test-flags.ts @@ -14,7 +14,15 @@ test('OpenTelemetryHandleSignalInterceptorInsertYield enabled by version', (t) = { version: '1.14.0', expected: false }, ]; for (const { version, expected } of cases) { - const actual = SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield.alternativeConditions![0]!({ + const alternativeCondition = (ctx: { info: WorkflowInfo; sdkVersion: string | undefined }) => { + for (const cond of SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield.alternativeConditions!) { + if (cond(ctx)) { + return true; + } + } + return false; + }; + const actual = alternativeCondition({ info: {} as WorkflowInfo, sdkVersion: version, }); diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index b1efd4a63..1103b775b 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -56,7 +56,9 @@ export const SdkFlags = { * * @since Introduced in 1.13.2. */ - OpenTelemetryHandleSignalInterceptorInsertYield: defineFlag(3, false, [affectedOtelInterceptorVersion]), + OpenTelemetryHandleSignalInterceptorInsertYield: defineFlag(3, false, [ + isBetween({ major: 1, minor: 11, patch: 3 }, { major: 1, minor: 13, patch: 2 }), + ]), } as const; function defineFlag(id: number, def: boolean, alternativeConditions?: AltConditionFn[]): SdkFlag { @@ -88,36 +90,65 @@ function buildIdSdkVersionMatches(version: RegExp): AltConditionFn { return ({ info }) => info.currentBuildId != null && regex.test(info.currentBuildId); // eslint-disable-line deprecation/deprecation } -function affectedOtelInterceptorVersion({ sdkVersion }: { sdkVersion?: string }): boolean { - if (!sdkVersion) { - return false; - } - const [major, minor, patchAndTags] = sdkVersion.split('.', 3); - if (major !== '1') return false; - - // Semver allows for additional tags to be appended to the version - let patch; - try { - const patchDigits = /[0-9]+/.exec(patchAndTags)?.[0]; - patch = patchDigits ? Number.parseInt(patchDigits) : null; - } catch { - // This shouldn't ever happen, but we are conservative here and avoid throwing when checking a flag. - patch = null; - } - - switch (minor) { - case '11': - // 1.11.3 was the last release that didn't inject a yield point - // If for some reason we are unable to parse the patch version, assume it isn't affected - return Boolean(patch && patch > 3); - case '12': - // Every 1.12 release requires a yield - return true; - case '13': - // 1.13.2 will be the first release since 1.11.3 that doesn't have a yield point in `handleSignal` - // If for some reason we are unable to parse the patch version, assume it isn't affected - return Boolean(patch && patch < 2); - default: - return false; - } +type SemVer = { + major: number; + minor: number; + patch: number; +}; + +function parseSemver(version: string): SemVer | undefined { + const matches = version.match(/(\d+)\.(\d+)\.(\d+)/); + if (!matches) return undefined; + const [full, major, minor, patch] = matches.map((digits) => { + try { + return Number.parseInt(digits); + } catch { + return undefined; + } + }); + if (major === undefined || minor === undefined || patch === undefined) + throw new Error(`full: ${full}, parts: ${major}.${minor}.${patch}`); + if (major === undefined || minor === undefined || patch === undefined) return undefined; + return { + major, + minor, + patch, + }; +} + +function compareSemver(a: SemVer, b: SemVer): -1 | 0 | 1 { + if (a.major < b.major) return -1; + if (a.major > b.major) return 1; + if (a.minor < b.minor) return -1; + if (a.minor > b.minor) return 1; + if (a.patch < b.patch) return -1; + if (a.patch > b.patch) return 1; + return 0; +} + +function isCompared(compare: SemVer, comparison: -1 | 0 | 1): AltConditionFn { + return ({ sdkVersion }) => { + if (!sdkVersion) throw new Error('no sdk version'); + if (!sdkVersion) return false; + const version = parseSemver(sdkVersion); + if (!version) throw new Error(`no version for ${sdkVersion}`); + if (!version) return false; + return compareSemver(compare, version) === comparison; + }; +} + +function isBefore(compare: SemVer): AltConditionFn { + return isCompared(compare, 1); +} + +function isEqual(compare: SemVer): AltConditionFn { + return isCompared(compare, 0); +} + +function isAfter(compare: SemVer): AltConditionFn { + return isCompared(compare, -1); +} + +function isBetween(lowEnd: SemVer, highEnd: SemVer): AltConditionFn { + return (ctx) => isAfter(lowEnd)(ctx) && isBefore(highEnd)(ctx); } From f7d8598afffa5441d272b624ee0d5db27cfc264e Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Wed, 22 Oct 2025 13:17:43 -0400 Subject: [PATCH 11/24] fix(otel): remove yield from execute interceptor --- .../src/workflow/index.ts | 6 ++- packages/test/src/test-flags.ts | 44 ++++++++++++++----- packages/workflow/src/flags.ts | 7 +++ 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/interceptors-opentelemetry/src/workflow/index.ts b/packages/interceptors-opentelemetry/src/workflow/index.ts index f9b7bd2fa..94db031a4 100644 --- a/packages/interceptors-opentelemetry/src/workflow/index.ts +++ b/packages/interceptors-opentelemetry/src/workflow/index.ts @@ -63,12 +63,14 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte next: Next ): Promise { const { workflowInfo, ContinueAsNew } = getWorkflowModule(); - const context = await Promise.resolve(extractContextFromHeaders(input.headers)); + const { getActivator, SdkFlags } = getSdkFlagsChecking(); + const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); + const context = extractContextFromHeaders(input.headers); return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}${workflowInfo().workflowType}`, fn: () => next(input), - context, + context: shouldInjectYield ? await Promise.resolve(context) : context, acceptableErrors: (err) => err instanceof ContinueAsNew, }); } diff --git a/packages/test/src/test-flags.ts b/packages/test/src/test-flags.ts index 0a1061e5e..6a0b09f98 100644 --- a/packages/test/src/test-flags.ts +++ b/packages/test/src/test-flags.ts @@ -1,7 +1,19 @@ import test from 'ava'; -import { SdkFlags } from '@temporalio/workflow/lib/flags'; +import { SdkFlags, type SdkFlag } from '@temporalio/workflow/lib/flags'; import type { WorkflowInfo } from '@temporalio/workflow'; +type Conditions = SdkFlag['alternativeConditions']; +function composeConditions(conditions: Conditions): NonNullable[number] { + return (ctx) => { + for (const cond of conditions ?? []) { + if (cond(ctx)) { + return true; + } + } + return false; + }; +} + test('OpenTelemetryHandleSignalInterceptorInsertYield enabled by version', (t) => { const cases = [ { version: '1.0.0', expected: false }, @@ -14,15 +26,7 @@ test('OpenTelemetryHandleSignalInterceptorInsertYield enabled by version', (t) = { version: '1.14.0', expected: false }, ]; for (const { version, expected } of cases) { - const alternativeCondition = (ctx: { info: WorkflowInfo; sdkVersion: string | undefined }) => { - for (const cond of SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield.alternativeConditions!) { - if (cond(ctx)) { - return true; - } - } - return false; - }; - const actual = alternativeCondition({ + const actual = composeConditions(SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield.alternativeConditions)({ info: {} as WorkflowInfo, sdkVersion: version, }); @@ -33,3 +37,23 @@ test('OpenTelemetryHandleSignalInterceptorInsertYield enabled by version', (t) = ); } }); + +test('OpenTelemetryInterceptorInsertYield enabled by version', (t) => { + const cases = [ + { version: '0.1.0', expected: true }, + { version: '1.0.0', expected: true }, + { version: '1.9.0-rc.0', expected: true }, + { version: '1.11.3', expected: true }, + { version: '1.13.1', expected: true }, + { version: '1.13.2', expected: false }, + { version: '1.14.0', expected: false }, + { version: '2.0.0', expected: false }, + ]; + for (const { version, expected } of cases) { + const actual = composeConditions(SdkFlags.OpenTelemetryInterceptorInsertYield.alternativeConditions)({ + info: {} as WorkflowInfo, + sdkVersion: version, + }); + t.is(actual, expected, `Expected OpenTelemetryInterceptorInsertYield on ${version} to evaluate as ${expected}`); + } +}); diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index 1103b775b..1f65a6d59 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -59,6 +59,13 @@ export const SdkFlags = { OpenTelemetryHandleSignalInterceptorInsertYield: defineFlag(3, false, [ isBetween({ major: 1, minor: 11, patch: 3 }, { major: 1, minor: 13, patch: 2 }), ]), + /** + * The interceptors provided by @temporalio/interceptors-opentelemetry initially had unnecessary yield points. + * If replaying a workflow created from these versions a yield point is injected to prevent any NDE. + * + * @since Introduced in 1.13.2 + */ + OpenTelemetryInterceptorInsertYield: defineFlag(3, false, [isBefore({ major: 1, minor: 13, patch: 2 })]), } as const; function defineFlag(id: number, def: boolean, alternativeConditions?: AltConditionFn[]): SdkFlag { From ec8319ebef7ad48386051e33c29f8a5f7be5a355 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Wed, 22 Oct 2025 16:20:02 -0400 Subject: [PATCH 12/24] chore: add replay test for otel smorgasbord --- .../otel_smorgasbord_1_13_1.json | 784 ++++++++++++++++++ packages/test/src/test-otel.ts | 25 +- 2 files changed, 808 insertions(+), 1 deletion(-) create mode 100644 packages/test/history_files/otel_smorgasbord_1_13_1.json diff --git a/packages/test/history_files/otel_smorgasbord_1_13_1.json b/packages/test/history_files/otel_smorgasbord_1_13_1.json new file mode 100644 index 000000000..4086711af --- /dev/null +++ b/packages/test/history_files/otel_smorgasbord_1_13_1.json @@ -0,0 +1,784 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2025-10-22T18:14:10.813307Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1069560", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "smorgasbord" + }, + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Mg==" + } + ] + }, + "workflowRunTimeout": "0s", + "workflowTaskTimeout": "10s", + "continuedExecutionRunId": "7889adaa-84a1-4f17-bece-3acb327d213f", + "initiator": "CONTINUE_AS_NEW_INITIATOR_WORKFLOW", + "originalExecutionRunId": "f91d0bf6-d243-4f15-aadd-baf2257189a1", + "firstExecutionRunId": "019a0d20-dc65-7dd3-9f0f-87cc5adf1312", + "attempt": 1, + "prevAutoResetPoints": { + "points": [ + { + "runId": "019a0d20-dc65-7dd3-9f0f-87cc5adf1312", + "firstWorkflowTaskCompletedId": "4", + "createTime": "2025-10-22T18:14:08.779133Z", + "expireTime": "2025-10-23T18:14:08.779326Z", + "resettable": true, + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + } + ] + }, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTlmNWM0NDc1YjNiZGZhY2U3OWQyYTk5MzM4ZmI4ZDVkLTg5M2JjMDA4ZjI0YjFhODMtMDEifQ==" + } + } + }, + "workflowId": "e8641ce1-c4f3-46e6-b947-3bc7850702c1" + } + }, + { + "eventId": "2", + "eventTime": "2025-10-22T18:14:10.814557Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1069561", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2025-10-22T18:14:10.817743Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1069568", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "48709@mac.lan", + "requestId": "6069cf07-5a58-464b-a24e-3f0bcda8c3f8", + "historySizeBytes": "606", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + } + } + }, + { + "eventId": "4", + "eventTime": "2025-10-22T18:14:10.831532Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1069572", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "48709@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + }, + "sdkMetadata": { + "coreUsedFlags": [ + 1, + 2, + 3 + ], + "sdkName": "temporal-typescript", + "sdkVersion": "1.13.1" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2025-10-22T18:14:10.831553Z", + "eventType": "EVENT_TYPE_TIMER_STARTED", + "taskId": "1069573", + "timerStartedEventAttributes": { + "timerId": "1", + "startToFireTimeout": "1s", + "workflowTaskCompletedEventId": "4" + } + }, + { + "eventId": "6", + "eventTime": "2025-10-22T18:14:10.831566Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1069574", + "activityTaskScheduledEventAttributes": { + "activityId": "1", + "activityType": { + "name": "fakeProgress" + }, + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTlmNWM0NDc1YjNiZGZhY2U3OWQyYTk5MzM4ZmI4ZDVkLTRjYzczZTZiOTI0YjE1YjctMDEifQ==" + } + } + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "MTAw" + }, + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "MTA=" + } + ] + }, + "scheduleToCloseTimeout": "0s", + "scheduleToStartTimeout": "0s", + "startToCloseTimeout": "60s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "4", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true + } + }, + { + "eventId": "7", + "eventTime": "2025-10-22T18:14:10.831582Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1069575", + "activityTaskScheduledEventAttributes": { + "activityId": "2", + "activityType": { + "name": "queryOwnWf" + }, + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTlmNWM0NDc1YjNiZGZhY2U3OWQyYTk5MzM4ZmI4ZDVkLTUzZGQzMzM4OTZkODg1ZDctMDEifQ==" + } + } + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0eXBlIjoicXVlcnkiLCJuYW1lIjoic3RlcCJ9" + } + ] + }, + "scheduleToCloseTimeout": "0s", + "scheduleToStartTimeout": "0s", + "startToCloseTimeout": "60s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "4", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true + } + }, + { + "eventId": "8", + "eventTime": "2025-10-22T18:14:10.831775Z", + "eventType": "EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED", + "taskId": "1069576", + "startChildWorkflowExecutionInitiatedEventAttributes": { + "namespace": "default", + "workflowId": "4951fb36-36e1-414c-a359-ae6f70541afe", + "workflowType": { + "name": "signalTarget" + }, + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "workflowRunTimeout": "0s", + "workflowTaskTimeout": "10s", + "parentClosePolicy": "PARENT_CLOSE_POLICY_TERMINATE", + "workflowTaskCompletedEventId": "4", + "workflowIdReusePolicy": "WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE", + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTlmNWM0NDc1YjNiZGZhY2U3OWQyYTk5MzM4ZmI4ZDVkLTRjNTRmMWMyZjA0Mjc0NTYtMDEifQ==" + } + } + }, + "memo": { + "fields": {} + }, + "searchAttributes": { + "indexedFields": {} + }, + "namespaceId": "0199f28c-0083-787b-b9c2-dd3b11a4db99", + "inheritBuildId": true + } + }, + { + "eventId": "9", + "eventTime": "2025-10-22T18:14:10.831787Z", + "eventType": "EVENT_TYPE_MARKER_RECORDED", + "taskId": "1069577", + "markerRecordedEventAttributes": { + "details": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImxvY2FsLWFjdGl2aXR5Ig==" + } + ] + }, + "data": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzZXEiOjMsImF0dGVtcHQiOjEsImFjdGl2aXR5X2lkIjoiMyIsImFjdGl2aXR5X3R5cGUiOiJlY2hvIiwiY29tcGxldGVfdGltZSI6eyJzZWNvbmRzIjoxNzYxMTU2ODUwLCJuYW5vcyI6ODE4NDA3MDAwfSwiYmFja29mZiI6bnVsbCwib3JpZ2luYWxfc2NoZWR1bGVfdGltZSI6eyJzZWNvbmRzIjoxNzYxMTU2ODUwLCJuYW5vcyI6ODI4OTM5MDAwfX0=" + } + ] + } + }, + "markerName": "core_local_activity", + "workflowTaskCompletedEventId": "4" + } + }, + { + "eventId": "10", + "eventTime": "2025-10-22T18:14:10.832850Z", + "eventType": "EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_STARTED", + "taskId": "1069588", + "childWorkflowExecutionStartedEventAttributes": { + "namespace": "default", + "initiatedEventId": "8", + "workflowExecution": { + "workflowId": "4951fb36-36e1-414c-a359-ae6f70541afe", + "runId": "019a0d20-e490-775a-aff9-bd0aacd4efe6" + }, + "workflowType": { + "name": "signalTarget" + }, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTlmNWM0NDc1YjNiZGZhY2U3OWQyYTk5MzM4ZmI4ZDVkLTRjNTRmMWMyZjA0Mjc0NTYtMDEifQ==" + } + } + }, + "namespaceId": "0199f28c-0083-787b-b9c2-dd3b11a4db99" + } + }, + { + "eventId": "11", + "eventTime": "2025-10-22T18:14:10.832854Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1069589", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "48709@mac.lan-1604b4aae7c542a1a28339cf59186b88", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "12", + "eventTime": "2025-10-22T18:14:10.834340Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1069600", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "11", + "identity": "48709@mac.lan", + "requestId": "1e071438-8f56-41ac-8440-aa459a8febb3", + "historySizeBytes": "2604", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + } + } + }, + { + "eventId": "13", + "eventTime": "2025-10-22T18:14:10.842034Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1069611", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "11", + "startedEventId": "12", + "identity": "48709@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "14", + "eventTime": "2025-10-22T18:14:10.842051Z", + "eventType": "EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED", + "taskId": "1069612", + "signalExternalWorkflowExecutionInitiatedEventAttributes": { + "workflowTaskCompletedEventId": "13", + "namespace": "default", + "workflowExecution": { + "workflowId": "4951fb36-36e1-414c-a359-ae6f70541afe" + }, + "signalName": "unblock", + "childWorkflowOnly": true, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTlmNWM0NDc1YjNiZGZhY2U3OWQyYTk5MzM4ZmI4ZDVkLTM4ZDFmZGNkY2RjZTRlMTMtMDEifQ==" + } + } + }, + "namespaceId": "0199f28c-0083-787b-b9c2-dd3b11a4db99" + } + }, + { + "eventId": "15", + "eventTime": "2025-10-22T18:14:10.836153Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1069613", + "workflowExecutionSignaledEventAttributes": { + "signalName": "activityStarted", + "input": {}, + "identity": "48709@mac.lan", + "header": { + "fields": {} + } + } + }, + { + "eventId": "16", + "eventTime": "2025-10-22T18:14:10.842061Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1069614", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "48709@mac.lan-1604b4aae7c542a1a28339cf59186b88", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "17", + "eventTime": "2025-10-22T18:14:10.842062Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1069615", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "16", + "identity": "48709@mac.lan", + "requestId": "request-from-RespondWorkflowTaskCompleted", + "historySizeBytes": "2784", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + } + } + }, + { + "eventId": "18", + "eventTime": "2025-10-22T18:14:10.847849Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1069628", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "16", + "startedEventId": "17", + "identity": "48709@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + }, + "sdkMetadata": { + "langUsedFlags": [ + 2 + ] + }, + "meteringMetadata": {} + } + }, + { + "eventId": "19", + "eventTime": "2025-10-22T18:14:10.843672Z", + "eventType": "EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1069629", + "externalWorkflowExecutionSignaledEventAttributes": { + "initiatedEventId": "14", + "namespace": "default", + "workflowExecution": { + "workflowId": "4951fb36-36e1-414c-a359-ae6f70541afe" + }, + "namespaceId": "0199f28c-0083-787b-b9c2-dd3b11a4db99" + } + }, + { + "eventId": "20", + "eventTime": "2025-10-22T18:14:10.847862Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1069630", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "48709@mac.lan-1604b4aae7c542a1a28339cf59186b88", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "21", + "eventTime": "2025-10-22T18:14:10.847864Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1069631", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "20", + "identity": "48709@mac.lan", + "requestId": "request-from-RespondWorkflowTaskCompleted", + "historySizeBytes": "3527", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + } + } + }, + { + "eventId": "22", + "eventTime": "2025-10-22T18:14:10.849933Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1069641", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "20", + "startedEventId": "21", + "identity": "48709@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "23", + "eventTime": "2025-10-22T18:14:10.833414Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1069642", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "7", + "identity": "48709@mac.lan", + "requestId": "5973a710-0f21-43df-90af-74ddda6793f1", + "attempt": 1, + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + } + } + }, + { + "eventId": "24", + "eventTime": "2025-10-22T18:14:10.848619Z", + "eventType": "EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1069643", + "childWorkflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "YmluYXJ5L251bGw=" + } + } + ] + }, + "namespace": "default", + "workflowExecution": { + "workflowId": "4951fb36-36e1-414c-a359-ae6f70541afe", + "runId": "019a0d20-e490-775a-aff9-bd0aacd4efe6" + }, + "workflowType": { + "name": "signalTarget" + }, + "initiatedEventId": "8", + "startedEventId": "10", + "namespaceId": "0199f28c-0083-787b-b9c2-dd3b11a4db99" + } + }, + { + "eventId": "25", + "eventTime": "2025-10-22T18:14:10.849242Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1069644", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Mg==" + } + ] + }, + "scheduledEventId": "7", + "startedEventId": "23", + "identity": "48709@mac.lan" + } + }, + { + "eventId": "26", + "eventTime": "2025-10-22T18:14:10.849944Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1069645", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "48709@mac.lan-1604b4aae7c542a1a28339cf59186b88", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "27", + "eventTime": "2025-10-22T18:14:10.849945Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1069646", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "26", + "identity": "48709@mac.lan", + "requestId": "request-from-RespondWorkflowTaskCompleted", + "historySizeBytes": "4078", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + } + } + }, + { + "eventId": "28", + "eventTime": "2025-10-22T18:14:10.851747Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1069649", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "26", + "startedEventId": "27", + "identity": "48709@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "29", + "eventTime": "2025-10-22T18:14:11.834282Z", + "eventType": "EVENT_TYPE_TIMER_FIRED", + "taskId": "1069651", + "timerFiredEventAttributes": { + "timerId": "1", + "startedEventId": "5" + } + }, + { + "eventId": "30", + "eventTime": "2025-10-22T18:14:11.834303Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1069652", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "48709@mac.lan-1604b4aae7c542a1a28339cf59186b88", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "31", + "eventTime": "2025-10-22T18:14:11.837493Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1069656", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "30", + "identity": "48709@mac.lan", + "requestId": "83eb577e-9e07-434e-8640-d28524a8ad9c", + "historySizeBytes": "5249", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + } + } + }, + { + "eventId": "32", + "eventTime": "2025-10-22T18:14:11.844592Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1069660", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "30", + "startedEventId": "31", + "identity": "48709@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "33", + "eventTime": "2025-10-22T18:14:10.834134Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1069662", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "6", + "identity": "48709@mac.lan", + "requestId": "d5573cc8-43bc-451d-b249-9a79d4dfe1b7", + "attempt": 1, + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + } + } + }, + { + "eventId": "34", + "eventTime": "2025-10-22T18:14:11.850643Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1069663", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "YmluYXJ5L251bGw=" + } + } + ] + }, + "scheduledEventId": "6", + "startedEventId": "33", + "identity": "48709@mac.lan" + } + }, + { + "eventId": "35", + "eventTime": "2025-10-22T18:14:11.850649Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1069664", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "48709@mac.lan-1604b4aae7c542a1a28339cf59186b88", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "36", + "eventTime": "2025-10-22T18:14:11.851942Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1069668", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "35", + "identity": "48709@mac.lan", + "requestId": "74369727-caa0-4d75-9190-a24c73fab4c2", + "historySizeBytes": "5931", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + } + } + }, + { + "eventId": "37", + "eventTime": "2025-10-22T18:14:11.856831Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1069672", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "35", + "startedEventId": "36", + "identity": "48709@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "38", + "eventTime": "2025-10-22T18:14:11.856856Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1069673", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "YmluYXJ5L251bGw=" + } + } + ] + }, + "workflowTaskCompletedEventId": "37" + } + } + ] +} \ No newline at end of file diff --git a/packages/test/src/test-otel.ts b/packages/test/src/test-otel.ts index e4771d0b3..a851d8c5b 100644 --- a/packages/test/src/test-otel.ts +++ b/packages/test/src/test-otel.ts @@ -24,7 +24,7 @@ import { import { OpenTelemetrySinks, SpanName, SPAN_DELIMITER } from '@temporalio/interceptors-opentelemetry/lib/workflow'; import { DefaultLogger, InjectedSinks, Runtime } from '@temporalio/worker'; import * as activities from './activities'; -import { loadHistory, RUN_INTEGRATION_TESTS, TestWorkflowEnvironment, Worker } from './helpers'; +import { loadHistory, RUN_INTEGRATION_TESTS, saveHistory, TestWorkflowEnvironment, Worker } from './helpers'; import * as workflows from './workflows'; import { createTestWorkflowBundle } from './helpers-integration'; @@ -557,3 +557,26 @@ test('Can replay otel history from 1.13.1', async (t) => { ); }); }); + +test('Can replay smorgasbord from 1.13.1', async (t) => { + const hist = await loadHistory('otel_smorgasbord_1_13_1.json'); + await t.notThrowsAsync(async () => { + await Worker.runReplayHistory( + { + workflowBundle: await createTestWorkflowBundle({ + workflowsPath: require.resolve('./workflows'), + workflowInterceptorModules: [require.resolve('./workflows/otel-interceptors')], + }), + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + }), + ], + }, + }, + hist + ); + }); +}); From b8dadeeade8ff60acd700392ad3f5d0f23bf9f15 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Wed, 22 Oct 2025 17:03:48 -0400 Subject: [PATCH 13/24] chore(otel): add failing test for signaling workflow --- .../history_files/signal_workflow_1_13_1.json | 463 ++++++++++++++++++ packages/test/src/test-otel.ts | 93 ++++ packages/test/src/workflows/index.ts | 1 + .../test/src/workflows/signal-workflow.ts | 21 + 4 files changed, 578 insertions(+) create mode 100644 packages/test/history_files/signal_workflow_1_13_1.json create mode 100644 packages/test/src/workflows/signal-workflow.ts diff --git a/packages/test/history_files/signal_workflow_1_13_1.json b/packages/test/history_files/signal_workflow_1_13_1.json new file mode 100644 index 000000000..67495ba0c --- /dev/null +++ b/packages/test/history_files/signal_workflow_1_13_1.json @@ -0,0 +1,463 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2025-10-22T21:01:27.376670Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1048587", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "topSecretGreeting" + }, + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IlRlbXBvcmFsIg==" + } + ] + }, + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "019a0dba-09d0-7a35-ba8f-b2843b67584b", + "identity": "69998@mac.lan", + "firstExecutionRunId": "019a0dba-09d0-7a35-ba8f-b2843b67584b", + "attempt": 1, + "firstWorkflowTaskBackoff": "0s", + "header": { + "fields": {} + }, + "workflowId": "bfe00f6e-5861-4b54-b4da-21433123ba75" + } + }, + { + "eventId": "2", + "eventTime": "2025-10-22T21:01:27.376737Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048588", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2025-10-22T21:01:27.378833Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048593", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "69998@mac.lan", + "requestId": "c234f432-3d91-4391-a647-d2cb1313ebab", + "historySizeBytes": "311", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + } + } + }, + { + "eventId": "4", + "eventTime": "2025-10-22T21:01:27.407936Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048597", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "69998@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + }, + "sdkMetadata": { + "coreUsedFlags": [ + 1, + 2, + 3 + ], + "sdkName": "temporal-typescript", + "sdkVersion": "1.13.1" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2025-10-22T21:01:27.408157Z", + "eventType": "EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED", + "taskId": "1048598", + "startChildWorkflowExecutionInitiatedEventAttributes": { + "namespace": "default", + "workflowId": "1663347e-aa08-4ee5-b426-c23f71678ddd", + "workflowType": { + "name": "topSecretGreetingChild" + }, + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IlRlbXBvcmFsIg==" + } + ] + }, + "workflowRunTimeout": "0s", + "workflowTaskTimeout": "10s", + "parentClosePolicy": "PARENT_CLOSE_POLICY_TERMINATE", + "workflowTaskCompletedEventId": "4", + "workflowIdReusePolicy": "WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE", + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTA4NjhlNzEyM2YxODZhOGM1MjVhNDFiYzNjZTg5YmI2LTJmODU3NjczZjA3YWFkMjEtMDEifQ==" + } + } + }, + "memo": { + "fields": {} + }, + "searchAttributes": { + "indexedFields": {} + }, + "namespaceId": "019a0db9-e40b-714c-a297-7071f9a97da5", + "inheritBuildId": true + } + }, + { + "eventId": "6", + "eventTime": "2025-10-22T21:01:27.409970Z", + "eventType": "EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_STARTED", + "taskId": "1048606", + "childWorkflowExecutionStartedEventAttributes": { + "namespace": "default", + "initiatedEventId": "5", + "workflowExecution": { + "workflowId": "1663347e-aa08-4ee5-b426-c23f71678ddd", + "runId": "019a0dba-09f1-7290-b42c-21b73590c34c" + }, + "workflowType": { + "name": "topSecretGreetingChild" + }, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTA4NjhlNzEyM2YxODZhOGM1MjVhNDFiYzNjZTg5YmI2LTJmODU3NjczZjA3YWFkMjEtMDEifQ==" + } + } + }, + "namespaceId": "019a0db9-e40b-714c-a297-7071f9a97da5" + } + }, + { + "eventId": "7", + "eventTime": "2025-10-22T21:01:27.409973Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048607", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "69998@mac.lan-2ddaef81986a47848e1757b47ae2ca8d", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "8", + "eventTime": "2025-10-22T21:01:27.410484Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048615", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "7", + "identity": "69998@mac.lan", + "requestId": "5b04c436-286f-4e6a-9f4e-0fb06a7c939c", + "historySizeBytes": "1418", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + } + } + }, + { + "eventId": "9", + "eventTime": "2025-10-22T21:01:27.418593Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048626", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "7", + "startedEventId": "8", + "identity": "69998@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "10", + "eventTime": "2025-10-22T21:01:27.418615Z", + "eventType": "EVENT_TYPE_TIMER_STARTED", + "taskId": "1048627", + "timerStartedEventAttributes": { + "timerId": "1", + "startToFireTimeout": "0.001s", + "workflowTaskCompletedEventId": "9" + } + }, + { + "eventId": "11", + "eventTime": "2025-10-22T21:01:27.418643Z", + "eventType": "EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED", + "taskId": "1048628", + "signalExternalWorkflowExecutionInitiatedEventAttributes": { + "workflowTaskCompletedEventId": "9", + "namespace": "default", + "workflowExecution": { + "workflowId": "1663347e-aa08-4ee5-b426-c23f71678ddd" + }, + "signalName": "approve", + "childWorkflowOnly": true, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTA4NjhlNzEyM2YxODZhOGM1MjVhNDFiYzNjZTg5YmI2LTZjMDAyMDQ2YTRiMDZjNzktMDEifQ==" + } + } + }, + "namespaceId": "019a0db9-e40b-714c-a297-7071f9a97da5" + } + }, + { + "eventId": "12", + "eventTime": "2025-10-22T21:01:27.420266Z", + "eventType": "EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048637", + "externalWorkflowExecutionSignaledEventAttributes": { + "initiatedEventId": "11", + "namespace": "default", + "workflowExecution": { + "workflowId": "1663347e-aa08-4ee5-b426-c23f71678ddd" + }, + "namespaceId": "019a0db9-e40b-714c-a297-7071f9a97da5" + } + }, + { + "eventId": "13", + "eventTime": "2025-10-22T21:01:27.420268Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048638", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "69998@mac.lan-2ddaef81986a47848e1757b47ae2ca8d", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "14", + "eventTime": "2025-10-22T21:01:27.420682Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048645", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "13", + "identity": "69998@mac.lan", + "requestId": "0c1f58c3-ef05-4166-9d81-61b92a0d29c9", + "historySizeBytes": "2247", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + } + } + }, + { + "eventId": "15", + "eventTime": "2025-10-22T21:01:27.425241Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048650", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "13", + "startedEventId": "14", + "identity": "69998@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "16", + "eventTime": "2025-10-22T21:01:27.425828Z", + "eventType": "EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1048658", + "childWorkflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvIFRlbXBvcmFsIg==" + } + ] + }, + "namespace": "default", + "workflowExecution": { + "workflowId": "1663347e-aa08-4ee5-b426-c23f71678ddd", + "runId": "019a0dba-09f1-7290-b42c-21b73590c34c" + }, + "workflowType": { + "name": "topSecretGreetingChild" + }, + "initiatedEventId": "5", + "startedEventId": "6", + "namespaceId": "019a0db9-e40b-714c-a297-7071f9a97da5" + } + }, + { + "eventId": "17", + "eventTime": "2025-10-22T21:01:27.425831Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048659", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "69998@mac.lan-2ddaef81986a47848e1757b47ae2ca8d", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "18", + "eventTime": "2025-10-22T21:01:27.426657Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048663", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "17", + "identity": "69998@mac.lan", + "requestId": "3c12f20e-7a18-4970-ac5f-fe5df677b9cb", + "historySizeBytes": "2904", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + } + } + }, + { + "eventId": "19", + "eventTime": "2025-10-22T21:01:27.429513Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048667", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "17", + "startedEventId": "18", + "identity": "69998@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "20", + "eventTime": "2025-10-22T21:01:28.413510Z", + "eventType": "EVENT_TYPE_TIMER_FIRED", + "taskId": "1048669", + "timerFiredEventAttributes": { + "timerId": "1", + "startedEventId": "10" + } + }, + { + "eventId": "21", + "eventTime": "2025-10-22T21:01:28.413530Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048670", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "69998@mac.lan-2ddaef81986a47848e1757b47ae2ca8d", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "22", + "eventTime": "2025-10-22T21:01:28.417428Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048674", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "21", + "identity": "69998@mac.lan", + "requestId": "9d9b15b7-ad92-4436-a25e-a30e4fa68682", + "historySizeBytes": "3363", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + } + } + }, + { + "eventId": "23", + "eventTime": "2025-10-22T21:01:28.427848Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048678", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "21", + "startedEventId": "22", + "identity": "69998@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "24", + "eventTime": "2025-10-22T21:01:28.427896Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1048679", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvIFRlbXBvcmFsIg==" + } + ] + }, + "workflowTaskCompletedEventId": "23" + } + } + ] +} \ No newline at end of file diff --git a/packages/test/src/test-otel.ts b/packages/test/src/test-otel.ts index a851d8c5b..4db8e2f63 100644 --- a/packages/test/src/test-otel.ts +++ b/packages/test/src/test-otel.ts @@ -559,6 +559,40 @@ test('Can replay otel history from 1.13.1', async (t) => { }); test('Can replay smorgasbord from 1.13.1', async (t) => { + /* + const staticResource = new opentelemetry.resources.Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', + }); + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows'), + activities, + taskQueue: 'test-otel-inbound', + sinks: { + exporter: makeWorkflowExporter(new InMemorySpanExporter(), staticResource), + }, + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + }), + ], + }, + }); + const client = new WorkflowClient(); + + const result = await worker.runUntil(async () => { + const handle = await client.start(workflows.smorgasbord, { + taskQueue: 'test-otel-inbound', + workflowId: uuid4(), + }); + const result = await handle.result(); + const history = await handle.fetchHistory(); + await saveHistory('smorg_with_otel.json', history); + return result; + }); + */ + const hist = await loadHistory('otel_smorgasbord_1_13_1.json'); await t.notThrowsAsync(async () => { await Worker.runReplayHistory( @@ -580,3 +614,62 @@ test('Can replay smorgasbord from 1.13.1', async (t) => { ); }); }); + +test('Can replay signal workflow from 1.13.1', async (t) => { + /* + const staticResource = new opentelemetry.resources.Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', + }); + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows/signal-workflow'), + activities: [], + taskQueue: 'test-otel-inbound', + sinks: { + exporter: makeWorkflowExporter(new InMemorySpanExporter(), staticResource), + }, + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + }), + ], + }, + }); + const client = new WorkflowClient(); + + const result = await worker.runUntil(async () => { + const handle = await client.start(workflows.topSecretGreeting, { + args: ['Temporal'], + taskQueue: 'test-otel-inbound', + workflowId: uuid4(), + }); + const result = await handle.result(); + const history = await handle.fetchHistory(); + await saveHistory('signal_workflow_1_13_1.json', history); + return result; + }); + */ + + const hist = await loadHistory('signal_workflow_1_13_1.json'); + await t.notThrowsAsync(async () => { + await Worker.runReplayHistory( + { + workflowBundle: await createTestWorkflowBundle({ + workflowsPath: require.resolve('./workflows/signal-workflow'), + workflowInterceptorModules: [require.resolve('./workflows/otel-interceptors')], + }), + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + }), + ], + }, + }, + hist + ); + }); + t.pass(); +}); diff --git a/packages/test/src/workflows/index.ts b/packages/test/src/workflows/index.ts index 92c3a23a0..0d8ef95ac 100644 --- a/packages/test/src/workflows/index.ts +++ b/packages/test/src/workflows/index.ts @@ -71,6 +71,7 @@ export * from './signals-are-always-processed'; export * from './signals-ordering'; export * from './signal-update-ordering'; export * from './signals-timers-activities-order'; +export * from './signal-workflow'; export * from './sinks'; export * from './sleep'; export * from './smorgasbord'; diff --git a/packages/test/src/workflows/signal-workflow.ts b/packages/test/src/workflows/signal-workflow.ts new file mode 100644 index 000000000..702fa8c56 --- /dev/null +++ b/packages/test/src/workflows/signal-workflow.ts @@ -0,0 +1,21 @@ +import { defineSignal, startChild, setHandler, sleep, condition } from '@temporalio/workflow'; + +const approveTopSecret = defineSignal('approve'); + +// A workflow that simply calls an activity +export async function topSecretGreeting(name: string): Promise { + const handle = await startChild(topSecretGreetingChild, { + args: [name], + }); + await Promise.all([handle.signal(approveTopSecret), sleep('1ms')]); + return await handle.result(); +} + +export async function topSecretGreetingChild(name: string): Promise { + let approved = false; + setHandler(approveTopSecret, () => { + approved = true; + }); + await condition(() => approved); + return `Hello ${name}`; +} From a1952bcccd1a89b4a941f7cbfc6255e4872ae83f Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Thu, 23 Oct 2025 09:30:46 -0400 Subject: [PATCH 14/24] clean up tests and flags --- .../src/instrumentation.ts | 2 +- .../src/workflow/index.ts | 31 ++++++-- packages/test/src/test-flags.ts | 30 +++++++ packages/test/src/test-otel.ts | 73 +---------------- packages/workflow/src/flags.ts | 79 ++++++++++++------- 5 files changed, 108 insertions(+), 107 deletions(-) diff --git a/packages/interceptors-opentelemetry/src/instrumentation.ts b/packages/interceptors-opentelemetry/src/instrumentation.ts index cf53a425a..610e05723 100644 --- a/packages/interceptors-opentelemetry/src/instrumentation.ts +++ b/packages/interceptors-opentelemetry/src/instrumentation.ts @@ -32,7 +32,7 @@ export function extractContextFromHeaders(headers: Headers): otel.Context | unde /** * Given headers, return new headers with the current otel context inserted */ -export async function headersWithContext(headers: Headers): Promise { +export function headersWithContext(headers: Headers): Headers { const carrier = {}; otel.propagation.inject(otel.context.active(), carrier, otel.defaultTextMapSetter); return { ...headers, [TRACE_HEADER]: payloadConverter.toPayload(carrier) }; diff --git a/packages/interceptors-opentelemetry/src/workflow/index.ts b/packages/interceptors-opentelemetry/src/workflow/index.ts index 94db031a4..26cab4c24 100644 --- a/packages/interceptors-opentelemetry/src/workflow/index.ts +++ b/packages/interceptors-opentelemetry/src/workflow/index.ts @@ -66,11 +66,12 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte const { getActivator, SdkFlags } = getSdkFlagsChecking(); const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); const context = extractContextFromHeaders(input.headers); + if (shouldInjectYield) await Promise.resolve(); return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}${workflowInfo().workflowType}`, fn: () => next(input), - context: shouldInjectYield ? await Promise.resolve(context) : context, + context, acceptableErrors: (err) => err instanceof ContinueAsNew, }); } @@ -82,11 +83,12 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte const { getActivator, SdkFlags } = getSdkFlagsChecking(); const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield); const context = extractContextFromHeaders(input.headers); + if (shouldInjectYield) await Promise.resolve(); return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, fn: () => next(input), - context: shouldInjectYield ? await Promise.resolve(context) : context, + context, }); } } @@ -109,11 +111,14 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: ActivityInput, next: Next ): Promise { + const { getActivator, SdkFlags } = getSdkFlagsChecking(); + const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); return await instrument({ tracer: this.tracer, spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`, fn: async () => { - const headers = await headersWithContext(input.headers); + const headers = headersWithContext(input.headers); + if (shouldInjectYield) await Promise.resolve(); return next({ ...input, headers, @@ -126,11 +131,14 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: LocalActivityInput, next: Next ): Promise { + const { getActivator, SdkFlags } = getSdkFlagsChecking(); + const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryScheduleLocalActivityInterceptorInsertYield); return await instrument({ tracer: this.tracer, spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`, fn: async () => { - const headers = await headersWithContext(input.headers); + const headers = headersWithContext(input.headers); + if (shouldInjectYield) await Promise.resolve(); return next({ ...input, headers, @@ -143,11 +151,14 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: StartChildWorkflowExecutionInput, next: Next ): Promise<[Promise, Promise]> { + const { getActivator, SdkFlags } = getSdkFlagsChecking(); + const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); return await instrument({ tracer: this.tracer, spanName: `${SpanName.CHILD_WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`, fn: async () => { - const headers = await headersWithContext(input.headers); + const headers = headersWithContext(input.headers); + if (shouldInjectYield) await Promise.resolve(); return next({ ...input, headers, @@ -161,11 +172,14 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn next: Next ): Promise { const { ContinueAsNew } = getWorkflowModule(); + const { getActivator, SdkFlags } = getSdkFlagsChecking(); + const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); return await instrument({ tracer: this.tracer, spanName: `${SpanName.CONTINUE_AS_NEW}${SPAN_DELIMITER}${input.options.workflowType}`, fn: async () => { - const headers = await headersWithContext(input.headers); + const headers = headersWithContext(input.headers); + if (shouldInjectYield) await Promise.resolve(); return next({ ...input, headers, @@ -179,11 +193,14 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: SignalWorkflowInput, next: Next ): Promise { + const { getActivator, SdkFlags } = getSdkFlagsChecking(); + const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, fn: async () => { - const headers = await headersWithContext(input.headers); + const headers = headersWithContext(input.headers); + if (shouldInjectYield) await Promise.resolve(); return next({ ...input, headers, diff --git a/packages/test/src/test-flags.ts b/packages/test/src/test-flags.ts index 6a0b09f98..8c86398e7 100644 --- a/packages/test/src/test-flags.ts +++ b/packages/test/src/test-flags.ts @@ -16,6 +16,7 @@ function composeConditions(conditions: Conditions): NonNullable[numb test('OpenTelemetryHandleSignalInterceptorInsertYield enabled by version', (t) => { const cases = [ + { version: undefined, expected: false }, { version: '1.0.0', expected: false }, { version: '1.11.3', expected: false }, { version: '1.11.5', expected: true }, @@ -40,6 +41,8 @@ test('OpenTelemetryHandleSignalInterceptorInsertYield enabled by version', (t) = test('OpenTelemetryInterceptorInsertYield enabled by version', (t) => { const cases = [ + // If there isn't any SDK version available we enable this flag as these yields were present since the initial version of the OTEL interceptors + { version: undefined, expected: true }, { version: '0.1.0', expected: true }, { version: '1.0.0', expected: true }, { version: '1.9.0-rc.0', expected: true }, @@ -57,3 +60,30 @@ test('OpenTelemetryInterceptorInsertYield enabled by version', (t) => { t.is(actual, expected, `Expected OpenTelemetryInterceptorInsertYield on ${version} to evaluate as ${expected}`); } }); + +test('OpenTelemetryScheduleLocalActivityInterceptorInsertYield enabled by version', (t) => { + const cases = [ + { version: undefined, expected: false }, + { version: '1.0.0', expected: false }, + { version: '1.11.3', expected: false }, + { version: '1.11.5', expected: false }, + { version: '1.11.6', expected: true }, + { version: '1.12.0', expected: true }, + { version: '1.13.1', expected: true }, + { version: '1.13.2', expected: false }, + { version: '1.14.0', expected: false }, + ]; + for (const { version, expected } of cases) { + const actual = composeConditions( + SdkFlags.OpenTelemetryScheduleLocalActivityInterceptorInsertYield.alternativeConditions + )({ + info: {} as WorkflowInfo, + sdkVersion: version, + }); + t.is( + actual, + expected, + `Expected OpenTelemetryScheduleLocalActivityInterceptorInsertYield on ${version} to evaluate as ${expected}` + ); + } +}); diff --git a/packages/test/src/test-otel.ts b/packages/test/src/test-otel.ts index 4db8e2f63..93dcb2e9d 100644 --- a/packages/test/src/test-otel.ts +++ b/packages/test/src/test-otel.ts @@ -24,7 +24,7 @@ import { import { OpenTelemetrySinks, SpanName, SPAN_DELIMITER } from '@temporalio/interceptors-opentelemetry/lib/workflow'; import { DefaultLogger, InjectedSinks, Runtime } from '@temporalio/worker'; import * as activities from './activities'; -import { loadHistory, RUN_INTEGRATION_TESTS, saveHistory, TestWorkflowEnvironment, Worker } from './helpers'; +import { loadHistory, RUN_INTEGRATION_TESTS, TestWorkflowEnvironment, Worker } from './helpers'; import * as workflows from './workflows'; import { createTestWorkflowBundle } from './helpers-integration'; @@ -559,40 +559,7 @@ test('Can replay otel history from 1.13.1', async (t) => { }); test('Can replay smorgasbord from 1.13.1', async (t) => { - /* - const staticResource = new opentelemetry.resources.Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', - }); - const worker = await Worker.create({ - workflowsPath: require.resolve('./workflows'), - activities, - taskQueue: 'test-otel-inbound', - sinks: { - exporter: makeWorkflowExporter(new InMemorySpanExporter(), staticResource), - }, - interceptors: { - workflowModules: [require.resolve('./workflows/otel-interceptors')], - activity: [ - (ctx) => ({ - inbound: new OpenTelemetryActivityInboundInterceptor(ctx), - }), - ], - }, - }); - const client = new WorkflowClient(); - - const result = await worker.runUntil(async () => { - const handle = await client.start(workflows.smorgasbord, { - taskQueue: 'test-otel-inbound', - workflowId: uuid4(), - }); - const result = await handle.result(); - const history = await handle.fetchHistory(); - await saveHistory('smorg_with_otel.json', history); - return result; - }); - */ - + // This test will trigger NDE if yield points for `scheduleActivity` and `startChildWorkflowExecution` are not inserted const hist = await loadHistory('otel_smorgasbord_1_13_1.json'); await t.notThrowsAsync(async () => { await Worker.runReplayHistory( @@ -616,41 +583,6 @@ test('Can replay smorgasbord from 1.13.1', async (t) => { }); test('Can replay signal workflow from 1.13.1', async (t) => { - /* - const staticResource = new opentelemetry.resources.Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', - }); - const worker = await Worker.create({ - workflowsPath: require.resolve('./workflows/signal-workflow'), - activities: [], - taskQueue: 'test-otel-inbound', - sinks: { - exporter: makeWorkflowExporter(new InMemorySpanExporter(), staticResource), - }, - interceptors: { - workflowModules: [require.resolve('./workflows/otel-interceptors')], - activity: [ - (ctx) => ({ - inbound: new OpenTelemetryActivityInboundInterceptor(ctx), - }), - ], - }, - }); - const client = new WorkflowClient(); - - const result = await worker.runUntil(async () => { - const handle = await client.start(workflows.topSecretGreeting, { - args: ['Temporal'], - taskQueue: 'test-otel-inbound', - workflowId: uuid4(), - }); - const result = await handle.result(); - const history = await handle.fetchHistory(); - await saveHistory('signal_workflow_1_13_1.json', history); - return result; - }); - */ - const hist = await loadHistory('signal_workflow_1_13_1.json'); await t.notThrowsAsync(async () => { await Worker.runReplayHistory( @@ -671,5 +603,4 @@ test('Can replay signal workflow from 1.13.1', async (t) => { hist ); }); - t.pass(); }); diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index 1f65a6d59..6499bc23b 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -63,9 +63,25 @@ export const SdkFlags = { * The interceptors provided by @temporalio/interceptors-opentelemetry initially had unnecessary yield points. * If replaying a workflow created from these versions a yield point is injected to prevent any NDE. * + * If the history does not include the SDK version, default to enabled since the yields were present since the OTEL + * package was created. + * + * @since Introduced in 1.13.2 + */ + OpenTelemetryInterceptorInsertYield: defineFlag(3, false, [isBefore({ major: 1, minor: 13, patch: 2 }, true)]), + /** + * In 1.11.6, the `scheduleLocalActivity` interceptor was added to + * `@temporalio/interceptors-opentelemetry` which added a yield point to the + * outbound interceptor. This yield point was removed in 1.13.2. + * + * If replaying a workflow from 1.11.6 up to 1.13.1, we insert a yield point + * in the interceptor to match the behavior. + * * @since Introduced in 1.13.2 */ - OpenTelemetryInterceptorInsertYield: defineFlag(3, false, [isBefore({ major: 1, minor: 13, patch: 2 })]), + OpenTelemetryScheduleLocalActivityInterceptorInsertYield: defineFlag(4, false, [ + isBetween({ major: 1, minor: 11, patch: 5 }, { major: 1, minor: 13, patch: 2 }), + ]), } as const; function defineFlag(id: number, def: boolean, alternativeConditions?: AltConditionFn[]): SdkFlag { @@ -103,6 +119,40 @@ type SemVer = { patch: number; }; +/** + * Creates an `AltConditionFn` that checks if the SDK version is before the provided version. + * An optional default can be provided in case the SDK version is not available. + */ +function isBefore(compare: SemVer, missingDefault?: boolean): AltConditionFn { + return isCompared(compare, 1, missingDefault); +} + +/** + * Creates an `AltConditionFn` that checks if the SDK version is after the provided version. + * An optional default can be provided in case the SDK version is not available. + */ +function isAfter(compare: SemVer, missingDefault?: boolean): AltConditionFn { + return isCompared(compare, -1, missingDefault); +} + +/** + * Creates an `AltConditionFn` that checks if the SDK version is between the provided versions. + * The range check is exclusive. + * An optional default can be provided in case the SDK version is not available. + */ +function isBetween(lowEnd: SemVer, highEnd: SemVer, missingDefault?: boolean): AltConditionFn { + return (ctx) => isAfter(lowEnd, missingDefault)(ctx) && isBefore(highEnd, missingDefault)(ctx); +} + +function isCompared(compare: SemVer, comparison: -1 | 0 | 1, missingDefault: boolean = false): AltConditionFn { + return ({ sdkVersion }) => { + if (!sdkVersion) return missingDefault; + const version = parseSemver(sdkVersion); + if (!version) return missingDefault; + return compareSemver(compare, version) === comparison; + }; +} + function parseSemver(version: string): SemVer | undefined { const matches = version.match(/(\d+)\.(\d+)\.(\d+)/); if (!matches) return undefined; @@ -132,30 +182,3 @@ function compareSemver(a: SemVer, b: SemVer): -1 | 0 | 1 { if (a.patch > b.patch) return 1; return 0; } - -function isCompared(compare: SemVer, comparison: -1 | 0 | 1): AltConditionFn { - return ({ sdkVersion }) => { - if (!sdkVersion) throw new Error('no sdk version'); - if (!sdkVersion) return false; - const version = parseSemver(sdkVersion); - if (!version) throw new Error(`no version for ${sdkVersion}`); - if (!version) return false; - return compareSemver(compare, version) === comparison; - }; -} - -function isBefore(compare: SemVer): AltConditionFn { - return isCompared(compare, 1); -} - -function isEqual(compare: SemVer): AltConditionFn { - return isCompared(compare, 0); -} - -function isAfter(compare: SemVer): AltConditionFn { - return isCompared(compare, -1); -} - -function isBetween(lowEnd: SemVer, highEnd: SemVer): AltConditionFn { - return (ctx) => isAfter(lowEnd)(ctx) && isBefore(highEnd)(ctx); -} From 56ab65928bd57cb6438108c1d3c2cb44b39d4702 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Thu, 23 Oct 2025 09:45:31 -0400 Subject: [PATCH 15/24] run formatter --- .../test/history_files/otel_smorgasbord_1_13_1.json | 12 +++--------- .../test/history_files/signal_workflow_1_13_1.json | 8 ++------ 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/test/history_files/otel_smorgasbord_1_13_1.json b/packages/test/history_files/otel_smorgasbord_1_13_1.json index 4086711af..638a82144 100644 --- a/packages/test/history_files/otel_smorgasbord_1_13_1.json +++ b/packages/test/history_files/otel_smorgasbord_1_13_1.json @@ -97,11 +97,7 @@ "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" }, "sdkMetadata": { - "coreUsedFlags": [ - 1, - 2, - 3 - ], + "coreUsedFlags": [1, 2, 3], "sdkName": "temporal-typescript", "sdkVersion": "1.13.1" }, @@ -448,9 +444,7 @@ "buildId": "@temporalio/worker@1.13.1+e5b3f6ec66e66a6c36c43315e9aa153389332256e179082b86435093007015ed" }, "sdkMetadata": { - "langUsedFlags": [ - 2 - ] + "langUsedFlags": [2] }, "meteringMetadata": {} } @@ -781,4 +775,4 @@ } } ] -} \ No newline at end of file +} diff --git a/packages/test/history_files/signal_workflow_1_13_1.json b/packages/test/history_files/signal_workflow_1_13_1.json index 67495ba0c..5cb466d4e 100644 --- a/packages/test/history_files/signal_workflow_1_13_1.json +++ b/packages/test/history_files/signal_workflow_1_13_1.json @@ -77,11 +77,7 @@ "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" }, "sdkMetadata": { - "coreUsedFlags": [ - 1, - 2, - 3 - ], + "coreUsedFlags": [1, 2, 3], "sdkName": "temporal-typescript", "sdkVersion": "1.13.1" }, @@ -460,4 +456,4 @@ } } ] -} \ No newline at end of file +} From a2f4822dc7467c8c61095cf1d975e50890e34126 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 31 Oct 2025 11:41:32 -0400 Subject: [PATCH 16/24] refactor sdk access with runtime workflow access --- .../src/workflow/index.ts | 23 +++++++------------ .../src/workflow/workflow-module-loader.ts | 14 ++++++++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/interceptors-opentelemetry/src/workflow/index.ts b/packages/interceptors-opentelemetry/src/workflow/index.ts index 26cab4c24..3d8e50bd6 100644 --- a/packages/interceptors-opentelemetry/src/workflow/index.ts +++ b/packages/interceptors-opentelemetry/src/workflow/index.ts @@ -22,7 +22,7 @@ import { instrument, extractContextFromHeaders, headersWithContext } from '../in import { ContextManager } from './context-manager'; import { SpanName, SPAN_DELIMITER } from './definitions'; import { SpanExporter } from './span-exporter'; -import { ensureWorkflowModuleLoaded, getSdkFlagsChecking, getWorkflowModule } from './workflow-module-loader'; +import { ensureWorkflowModuleLoaded, getWorkflowModule, hasSdkFlag } from './workflow-module-loader'; export * from './definitions'; @@ -63,8 +63,7 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte next: Next ): Promise { const { workflowInfo, ContinueAsNew } = getWorkflowModule(); - const { getActivator, SdkFlags } = getSdkFlagsChecking(); - const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); + const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceptorInsertYield'); const context = extractContextFromHeaders(input.headers); if (shouldInjectYield) await Promise.resolve(); return await instrument({ @@ -80,8 +79,7 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte input: SignalInput, next: Next ): Promise { - const { getActivator, SdkFlags } = getSdkFlagsChecking(); - const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield); + const shouldInjectYield = hasSdkFlag('OpenTelemetryHandleSignalInterceptorInsertYield'); const context = extractContextFromHeaders(input.headers); if (shouldInjectYield) await Promise.resolve(); return await instrument({ @@ -111,8 +109,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: ActivityInput, next: Next ): Promise { - const { getActivator, SdkFlags } = getSdkFlagsChecking(); - const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); + const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceptorInsertYield'); return await instrument({ tracer: this.tracer, spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`, @@ -131,8 +128,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: LocalActivityInput, next: Next ): Promise { - const { getActivator, SdkFlags } = getSdkFlagsChecking(); - const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryScheduleLocalActivityInterceptorInsertYield); + const shouldInjectYield = hasSdkFlag('OpenTelemetryScheduleLocalActivityInterceptorInsertYield'); return await instrument({ tracer: this.tracer, spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`, @@ -151,8 +147,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: StartChildWorkflowExecutionInput, next: Next ): Promise<[Promise, Promise]> { - const { getActivator, SdkFlags } = getSdkFlagsChecking(); - const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); + const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceptorInsertYield'); return await instrument({ tracer: this.tracer, spanName: `${SpanName.CHILD_WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`, @@ -172,8 +167,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn next: Next ): Promise { const { ContinueAsNew } = getWorkflowModule(); - const { getActivator, SdkFlags } = getSdkFlagsChecking(); - const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); + const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceptorInsertYield'); return await instrument({ tracer: this.tracer, spanName: `${SpanName.CONTINUE_AS_NEW}${SPAN_DELIMITER}${input.options.workflowType}`, @@ -193,8 +187,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: SignalWorkflowInput, next: Next ): Promise { - const { getActivator, SdkFlags } = getSdkFlagsChecking(); - const shouldInjectYield = getActivator().hasFlag(SdkFlags.OpenTelemetryInterceptorInsertYield); + const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceptorInsertYield'); return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, diff --git a/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts b/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts index 652417ca8..8fdf8342b 100644 --- a/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts +++ b/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts @@ -4,7 +4,8 @@ */ import type * as WorkflowModule from '@temporalio/workflow'; import type { SdkFlags as SdkFlagsT } from '@temporalio/workflow/lib/flags'; -import type { getActivator as getActivatorT } from '@temporalio/workflow/lib/global-attributes'; + +type SdkFlags = typeof SdkFlagsT; // @temporalio/workflow is an optional peer dependency. // It can be missing as long as the user isn't attempting to construct a workflow interceptor. @@ -32,10 +33,17 @@ export function getWorkflowModule(): typeof WorkflowModule { return workflowModule!; } -export function getSdkFlagsChecking(): { getActivator: typeof getActivatorT; SdkFlags: typeof SdkFlagsT } { +/** + * Returns if an SDK flag was set + * + * Expects to be called only after `ensureWorkflowModuleLoaded`. + * Will throw if `@temporalio/workflow` is not available + */ +export function hasSdkFlag(flag: keyof SdkFlags): boolean { const { SdkFlags } = require('@temporalio/workflow/lib/flags'); const { getActivator } = require('@temporalio/workflow/lib/global-attributes'); - return { getActivator, SdkFlags }; + + return getActivator().hasFlag(SdkFlags[flag]); } /** From 678c55dfb52449ba5a66d336d1f185caaa6d9ef0 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Thu, 23 Oct 2025 16:05:00 -0400 Subject: [PATCH 17/24] add new workflow service rpc calls --- packages/core-bridge/src/client.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core-bridge/src/client.rs b/packages/core-bridge/src/client.rs index 7b6fc6957..5aeaf9fe9 100644 --- a/packages/core-bridge/src/client.rs +++ b/packages/core-bridge/src/client.rs @@ -290,6 +290,9 @@ async fn client_invoke_workflow_service( "DescribeDeployment" => { rpc_call!(retry_client, call, describe_deployment) } + "DescribeWorker" => { + rpc_call!(retry_client, call, describe_worker) + } "DeprecateNamespace" => rpc_call!(retry_client, call, deprecate_namespace), "DescribeNamespace" => rpc_call!(retry_client, call, describe_namespace), "DescribeSchedule" => rpc_call!(retry_client, call, describe_schedule), @@ -450,6 +453,9 @@ async fn client_invoke_workflow_service( "SetWorkerDeploymentCurrentVersion" => { rpc_call!(retry_client, call, set_worker_deployment_current_version) } + "SetWorkerDeploymentManager" => { + rpc_call!(retry_client, call, set_worker_deployment_manager) + } "SetWorkerDeploymentRampingVersion" => { rpc_call!(retry_client, call, set_worker_deployment_ramping_version) } From 3308dc5ad51beec6e01d92e1c10f805fde090a08 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 31 Oct 2025 12:20:03 -0400 Subject: [PATCH 18/24] silence require warnings --- .../src/workflow/workflow-module-loader.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts b/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts index 8fdf8342b..cd24f285a 100644 --- a/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts +++ b/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts @@ -40,7 +40,9 @@ export function getWorkflowModule(): typeof WorkflowModule { * Will throw if `@temporalio/workflow` is not available */ export function hasSdkFlag(flag: keyof SdkFlags): boolean { + // eslint-disable-next-line @typescript-eslint/no-require-imports const { SdkFlags } = require('@temporalio/workflow/lib/flags'); + // eslint-disable-next-line @typescript-eslint/no-require-imports const { getActivator } = require('@temporalio/workflow/lib/global-attributes'); return getActivator().hasFlag(SdkFlags[flag]); From 5d0c85a30ed6bb228a53c018122d9529fe2be736 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 31 Oct 2025 14:02:05 -0400 Subject: [PATCH 19/24] remove error in semver parse --- packages/workflow/src/flags.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index 6499bc23b..2aae4d7b2 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -156,15 +156,13 @@ function isCompared(compare: SemVer, comparison: -1 | 0 | 1, missingDefault: boo function parseSemver(version: string): SemVer | undefined { const matches = version.match(/(\d+)\.(\d+)\.(\d+)/); if (!matches) return undefined; - const [full, major, minor, patch] = matches.map((digits) => { + const [_full, major, minor, patch] = matches.map((digits) => { try { return Number.parseInt(digits); } catch { return undefined; } }); - if (major === undefined || minor === undefined || patch === undefined) - throw new Error(`full: ${full}, parts: ${major}.${minor}.${patch}`); if (major === undefined || minor === undefined || patch === undefined) return undefined; return { major, From 74518555e6d9528abe908d49d2916a4b81acb5e5 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 31 Oct 2025 14:05:58 -0400 Subject: [PATCH 20/24] remove await in incoming activity --- packages/interceptors-opentelemetry/src/worker/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interceptors-opentelemetry/src/worker/index.ts b/packages/interceptors-opentelemetry/src/worker/index.ts index 4d2334a60..506200bdb 100644 --- a/packages/interceptors-opentelemetry/src/worker/index.ts +++ b/packages/interceptors-opentelemetry/src/worker/index.ts @@ -34,7 +34,7 @@ export class OpenTelemetryActivityInboundInterceptor implements ActivityInboundC } async execute(input: ActivityExecuteInput, next: Next): Promise { - const context = await Promise.resolve(extractContextFromHeaders(input.headers)); + const context = extractContextFromHeaders(input.headers); const spanName = `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}${this.ctx.info.activityType}`; return await instrument({ tracer: this.tracer, spanName, fn: () => next(input), context }); } From b581e8d2c23c45d7a8975905b9a423b96dc5a26b Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 3 Nov 2025 14:37:33 -0500 Subject: [PATCH 21/24] pr feedback --- packages/interceptors-opentelemetry/src/workflow/index.ts | 1 - packages/workflow/src/flags.ts | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/interceptors-opentelemetry/src/workflow/index.ts b/packages/interceptors-opentelemetry/src/workflow/index.ts index 3d8e50bd6..65d705828 100644 --- a/packages/interceptors-opentelemetry/src/workflow/index.ts +++ b/packages/interceptors-opentelemetry/src/workflow/index.ts @@ -52,7 +52,6 @@ function getTracer(): otel.Tracer { */ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInterceptor { protected readonly tracer = getTracer(); - protected readonly maybeInjectYield = true; public constructor() { ensureWorkflowModuleLoaded(); diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index 2aae4d7b2..7dd47119f 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -52,7 +52,7 @@ export const SdkFlags = { * * This yield point was removed in 1.13.2, but in order to prevent workflows from the * affected versions resulting in NDE, we have to inject the yield point on replay. - * This flag should be enabled for SDK versions newer than 1.11.3 or older than 1.13.2. + * This flag should be enabled for SDK versions newer than 1.11.3 and older than 1.13.2. * * @since Introduced in 1.13.2. */ @@ -68,7 +68,7 @@ export const SdkFlags = { * * @since Introduced in 1.13.2 */ - OpenTelemetryInterceptorInsertYield: defineFlag(3, false, [isBefore({ major: 1, minor: 13, patch: 2 }, true)]), + OpenTelemetryInterceptorInsertYield: defineFlag(4, false, [isBefore({ major: 1, minor: 13, patch: 2 }, true)]), /** * In 1.11.6, the `scheduleLocalActivity` interceptor was added to * `@temporalio/interceptors-opentelemetry` which added a yield point to the @@ -79,7 +79,7 @@ export const SdkFlags = { * * @since Introduced in 1.13.2 */ - OpenTelemetryScheduleLocalActivityInterceptorInsertYield: defineFlag(4, false, [ + OpenTelemetryScheduleLocalActivityInterceptorInsertYield: defineFlag(5, false, [ isBetween({ major: 1, minor: 11, patch: 5 }, { major: 1, minor: 13, patch: 2 }), ]), } as const; From f1658554382e47864756db4ec32a99ed233cb15c Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Fri, 7 Nov 2025 16:29:37 -0500 Subject: [PATCH 22/24] wip --- .../src/workflow/index.ts | 6 +- .../src/workflow/workflow-module-loader.ts | 7 +-- packages/workflow/src/flags.ts | 61 +++++++++---------- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/packages/interceptors-opentelemetry/src/workflow/index.ts b/packages/interceptors-opentelemetry/src/workflow/index.ts index 65d705828..252189965 100644 --- a/packages/interceptors-opentelemetry/src/workflow/index.ts +++ b/packages/interceptors-opentelemetry/src/workflow/index.ts @@ -78,9 +78,13 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte input: SignalInput, next: Next ): Promise { - const shouldInjectYield = hasSdkFlag('OpenTelemetryHandleSignalInterceptorInsertYield'); + // Tracing of inbound signals was added in v1.11.5. + if (!hasSdkFlag('OpenTelemetryInterceptorsTracesInboundSignals')) return next(input); + + const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields'); const context = extractContextFromHeaders(input.headers); if (shouldInjectYield) await Promise.resolve(); + return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, diff --git a/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts b/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts index cd24f285a..34019f781 100644 --- a/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts +++ b/packages/interceptors-opentelemetry/src/workflow/workflow-module-loader.ts @@ -40,10 +40,9 @@ export function getWorkflowModule(): typeof WorkflowModule { * Will throw if `@temporalio/workflow` is not available */ export function hasSdkFlag(flag: keyof SdkFlags): boolean { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { SdkFlags } = require('@temporalio/workflow/lib/flags'); - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { getActivator } = require('@temporalio/workflow/lib/global-attributes'); + const { SdkFlags } = require('@temporalio/workflow/lib/flags') as typeof import('@temporalio/workflow/lib/flags'); // eslint-disable-line @typescript-eslint/no-require-imports + const { getActivator } = + require('@temporalio/workflow/lib/global-attributes') as typeof import('@temporalio/workflow/lib/global-attributes'); // eslint-disable-line @typescript-eslint/no-require-imports return getActivator().hasFlag(SdkFlags[flag]); } diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index 7dd47119f..8928bf9cc 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -44,6 +44,7 @@ export const SdkFlags = { * to implicitely have this flag on. */ ProcessWorkflowActivationJobsAsSingleBatch: defineFlag(2, true, [buildIdSdkVersionMatches(/1\.11\.[01]/)]), + /** * In 1.11.3 and previous versions, the interceptor for `handleSignal` provided * by @temporalio/interceptors-opentelemetry did not have a yield point in it. @@ -56,19 +57,10 @@ export const SdkFlags = { * * @since Introduced in 1.13.2. */ - OpenTelemetryHandleSignalInterceptorInsertYield: defineFlag(3, false, [ - isBetween({ major: 1, minor: 11, patch: 3 }, { major: 1, minor: 13, patch: 2 }), + OpenTelemetryInterceptorsTracesInboundSignals: defineFlag(3, false, [ + isBetween({ major: 1, minor: 11, patch: 5 }, { major: 1, minor: 13, patch: 2 }), ]), - /** - * The interceptors provided by @temporalio/interceptors-opentelemetry initially had unnecessary yield points. - * If replaying a workflow created from these versions a yield point is injected to prevent any NDE. - * - * If the history does not include the SDK version, default to enabled since the yields were present since the OTEL - * package was created. - * - * @since Introduced in 1.13.2 - */ - OpenTelemetryInterceptorInsertYield: defineFlag(4, false, [isBefore({ major: 1, minor: 13, patch: 2 }, true)]), + /** * In 1.11.6, the `scheduleLocalActivity` interceptor was added to * `@temporalio/interceptors-opentelemetry` which added a yield point to the @@ -79,9 +71,21 @@ export const SdkFlags = { * * @since Introduced in 1.13.2 */ - OpenTelemetryScheduleLocalActivityInterceptorInsertYield: defineFlag(5, false, [ + OpenTelemetryScheduleLocalActivityInterceptorInsertYield: defineFlag(4, false, [ isBetween({ major: 1, minor: 11, patch: 5 }, { major: 1, minor: 13, patch: 2 }), ]), + + /** + * The interceptors provided by @temporalio/interceptors-opentelemetry initially had unnecessary + * yield points on calling to `extractContextFromHeaders`. + * If replaying a workflow created from these versions a yield point is injected to prevent any NDE. + * + * If the history does not include the SDK version, default to enabled since the yields were present since the OTEL + * package was created. + * + * @since Introduced in 1.13.2 + */ + OpenTelemetryInterceptorInsertYield: defineFlag(5, false, [isBefore({ major: 1, minor: 13, patch: 2 }, true)]), } as const; function defineFlag(id: number, def: boolean, alternativeConditions?: AltConditionFn[]): SdkFlag { @@ -154,29 +158,24 @@ function isCompared(compare: SemVer, comparison: -1 | 0 | 1, missingDefault: boo } function parseSemver(version: string): SemVer | undefined { - const matches = version.match(/(\d+)\.(\d+)\.(\d+)/); - if (!matches) return undefined; - const [_full, major, minor, patch] = matches.map((digits) => { - try { - return Number.parseInt(digits); - } catch { - return undefined; - } - }); - if (major === undefined || minor === undefined || patch === undefined) return undefined; - return { - major, - minor, - patch, - }; + try { + const [_, major, minor, patch] = version.match(/(\d+)\.(\d+)\.(\d+)/)!; + return { + major: Number.parseInt(major), + minor: Number.parseInt(minor), + patch: Number.parseInt(patch), + }; + } catch { + return undefined; + } } function compareSemver(a: SemVer, b: SemVer): -1 | 0 | 1 { if (a.major < b.major) return -1; - if (a.major > b.major) return 1; + if (a.major > b.major) return +1; if (a.minor < b.minor) return -1; - if (a.minor > b.minor) return 1; + if (a.minor > b.minor) return +1; if (a.patch < b.patch) return -1; - if (a.patch > b.patch) return 1; + if (a.patch > b.patch) return +1; return 0; } From c09b9864bdbb14024b998b2656f4213a3163b20b Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Fri, 7 Nov 2025 16:37:13 -0500 Subject: [PATCH 23/24] wip --- .../src/workflow/index.ts | 31 ++++++++++--------- packages/workflow/src/flags.ts | 10 ++---- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/interceptors-opentelemetry/src/workflow/index.ts b/packages/interceptors-opentelemetry/src/workflow/index.ts index 252189965..ea9abef99 100644 --- a/packages/interceptors-opentelemetry/src/workflow/index.ts +++ b/packages/interceptors-opentelemetry/src/workflow/index.ts @@ -62,9 +62,10 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte next: Next ): Promise { const { workflowInfo, ContinueAsNew } = getWorkflowModule(); - const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceptorInsertYield'); + const context = extractContextFromHeaders(input.headers); - if (shouldInjectYield) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve(); + return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}${workflowInfo().workflowType}`, @@ -81,9 +82,8 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte // Tracing of inbound signals was added in v1.11.5. if (!hasSdkFlag('OpenTelemetryInterceptorsTracesInboundSignals')) return next(input); - const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields'); const context = extractContextFromHeaders(input.headers); - if (shouldInjectYield) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve(); return await instrument({ tracer: this.tracer, @@ -112,13 +112,13 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: ActivityInput, next: Next ): Promise { - const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceptorInsertYield'); return await instrument({ tracer: this.tracer, spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`, fn: async () => { const headers = headersWithContext(input.headers); - if (shouldInjectYield) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceptorsAvoidsExtraYields')) await Promise.resolve(); + return next({ ...input, headers, @@ -131,13 +131,16 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: LocalActivityInput, next: Next ): Promise { - const shouldInjectYield = hasSdkFlag('OpenTelemetryScheduleLocalActivityInterceptorInsertYield'); + // Tracing of local activities was added in v1.11.6. + if (!hasSdkFlag('OpenTelemetryInterceptorsTracesLocalActivities')) return next(input); + return await instrument({ tracer: this.tracer, spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`, fn: async () => { const headers = headersWithContext(input.headers); - if (shouldInjectYield) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceptorsAvoidsExtraYields')) await Promise.resolve(); + return next({ ...input, headers, @@ -150,13 +153,13 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: StartChildWorkflowExecutionInput, next: Next ): Promise<[Promise, Promise]> { - const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceptorInsertYield'); return await instrument({ tracer: this.tracer, spanName: `${SpanName.CHILD_WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`, fn: async () => { const headers = headersWithContext(input.headers); - if (shouldInjectYield) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceptorsAvoidsExtraYields')) await Promise.resolve(); + return next({ ...input, headers, @@ -170,13 +173,13 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn next: Next ): Promise { const { ContinueAsNew } = getWorkflowModule(); - const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceptorInsertYield'); return await instrument({ tracer: this.tracer, spanName: `${SpanName.CONTINUE_AS_NEW}${SPAN_DELIMITER}${input.options.workflowType}`, fn: async () => { const headers = headersWithContext(input.headers); - if (shouldInjectYield) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceptorsAvoidsExtraYields')) await Promise.resolve(); + return next({ ...input, headers, @@ -190,13 +193,13 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn input: SignalWorkflowInput, next: Next ): Promise { - const shouldInjectYield = hasSdkFlag('OpenTelemetryInterceptorInsertYield'); return await instrument({ tracer: this.tracer, spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, fn: async () => { const headers = headersWithContext(input.headers); - if (shouldInjectYield) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceptorsAvoidsExtraYields')) await Promise.resolve(); + return next({ ...input, headers, diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index 8928bf9cc..dbcc3bfaf 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -57,9 +57,7 @@ export const SdkFlags = { * * @since Introduced in 1.13.2. */ - OpenTelemetryInterceptorsTracesInboundSignals: defineFlag(3, false, [ - isBetween({ major: 1, minor: 11, patch: 5 }, { major: 1, minor: 13, patch: 2 }), - ]), + OpenTelemetryInterceptorsTracesInboundSignals: defineFlag(3, false, [isAtLeast({ major: 1, minor: 11, patch: 5 })]), /** * In 1.11.6, the `scheduleLocalActivity` interceptor was added to @@ -71,9 +69,7 @@ export const SdkFlags = { * * @since Introduced in 1.13.2 */ - OpenTelemetryScheduleLocalActivityInterceptorInsertYield: defineFlag(4, false, [ - isBetween({ major: 1, minor: 11, patch: 5 }, { major: 1, minor: 13, patch: 2 }), - ]), + OpenTelemetryInterceptorsTracesLocalActivities: defineFlag(4, false, [isAtLeast({ major: 1, minor: 11, patch: 6 })]), /** * The interceptors provided by @temporalio/interceptors-opentelemetry initially had unnecessary @@ -85,7 +81,7 @@ export const SdkFlags = { * * @since Introduced in 1.13.2 */ - OpenTelemetryInterceptorInsertYield: defineFlag(5, false, [isBefore({ major: 1, minor: 13, patch: 2 }, true)]), + OpenTelemetryInterceporsAvoidsExtraYields: defineFlag(5, true, [isAtLeast({ major: 1, minor: 13, patch: 2 })]), } as const; function defineFlag(id: number, def: boolean, alternativeConditions?: AltConditionFn[]): SdkFlag { From f7d19c7f03f9c6cc01d36fd15bc67df7330197b0 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 7 Nov 2025 18:34:27 -0500 Subject: [PATCH 24/24] pr feedback: change flag definitions to better match existing flags --- .../src/workflow/index.ts | 10 ++-- packages/test/src/test-flags.ts | 50 ++++++++++--------- packages/workflow/src/flags.ts | 43 ++++------------ 3 files changed, 42 insertions(+), 61 deletions(-) diff --git a/packages/interceptors-opentelemetry/src/workflow/index.ts b/packages/interceptors-opentelemetry/src/workflow/index.ts index ea9abef99..b47f58545 100644 --- a/packages/interceptors-opentelemetry/src/workflow/index.ts +++ b/packages/interceptors-opentelemetry/src/workflow/index.ts @@ -117,7 +117,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`, fn: async () => { const headers = headersWithContext(input.headers); - if (!hasSdkFlag('OpenTelemetryInterceptorsAvoidsExtraYields')) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve(); return next({ ...input, @@ -139,7 +139,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`, fn: async () => { const headers = headersWithContext(input.headers); - if (!hasSdkFlag('OpenTelemetryInterceptorsAvoidsExtraYields')) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve(); return next({ ...input, @@ -158,7 +158,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn spanName: `${SpanName.CHILD_WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`, fn: async () => { const headers = headersWithContext(input.headers); - if (!hasSdkFlag('OpenTelemetryInterceptorsAvoidsExtraYields')) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve(); return next({ ...input, @@ -178,7 +178,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn spanName: `${SpanName.CONTINUE_AS_NEW}${SPAN_DELIMITER}${input.options.workflowType}`, fn: async () => { const headers = headersWithContext(input.headers); - if (!hasSdkFlag('OpenTelemetryInterceptorsAvoidsExtraYields')) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve(); return next({ ...input, @@ -198,7 +198,7 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, fn: async () => { const headers = headersWithContext(input.headers); - if (!hasSdkFlag('OpenTelemetryInterceptorsAvoidsExtraYields')) await Promise.resolve(); + if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve(); return next({ ...input, diff --git a/packages/test/src/test-flags.ts b/packages/test/src/test-flags.ts index 8c86398e7..4e59de516 100644 --- a/packages/test/src/test-flags.ts +++ b/packages/test/src/test-flags.ts @@ -14,7 +14,7 @@ function composeConditions(conditions: Conditions): NonNullable[numb }; } -test('OpenTelemetryHandleSignalInterceptorInsertYield enabled by version', (t) => { +test('OpenTelemetryInterceptorsTracesInboundSignals enabled by version', (t) => { const cases = [ { version: undefined, expected: false }, { version: '1.0.0', expected: false }, @@ -23,45 +23,49 @@ test('OpenTelemetryHandleSignalInterceptorInsertYield enabled by version', (t) = { version: '1.11.6', expected: true }, { version: '1.12.0', expected: true }, { version: '1.13.1', expected: true }, - { version: '1.13.2', expected: false }, - { version: '1.14.0', expected: false }, + { version: '1.13.2', expected: true }, + { version: '1.14.0', expected: true }, ]; for (const { version, expected } of cases) { - const actual = composeConditions(SdkFlags.OpenTelemetryHandleSignalInterceptorInsertYield.alternativeConditions)({ + const actual = composeConditions(SdkFlags.OpenTelemetryInterceptorsTracesInboundSignals.alternativeConditions)({ info: {} as WorkflowInfo, sdkVersion: version, }); t.is( actual, expected, - `Expected OpenTelemetryHandleSignalInterceptorInsertYield on ${version} to evaluate as ${expected}` + `Expected OpenTelemetryInterceptorsTracesInboundSignals on ${version} to evaluate as ${expected}` ); } }); -test('OpenTelemetryInterceptorInsertYield enabled by version', (t) => { +test('OpenTelemetryInterceporsAvoidsExtraYields enabled by version', (t) => { const cases = [ // If there isn't any SDK version available we enable this flag as these yields were present since the initial version of the OTEL interceptors - { version: undefined, expected: true }, - { version: '0.1.0', expected: true }, - { version: '1.0.0', expected: true }, - { version: '1.9.0-rc.0', expected: true }, - { version: '1.11.3', expected: true }, - { version: '1.13.1', expected: true }, - { version: '1.13.2', expected: false }, - { version: '1.14.0', expected: false }, - { version: '2.0.0', expected: false }, + { version: undefined, expected: false }, + { version: '0.1.0', expected: false }, + { version: '1.0.0', expected: false }, + { version: '1.9.0-rc.0', expected: false }, + { version: '1.11.3', expected: false }, + { version: '1.13.1', expected: false }, + { version: '1.13.2', expected: true }, + { version: '1.14.0', expected: true }, + { version: '2.0.0', expected: true }, ]; for (const { version, expected } of cases) { - const actual = composeConditions(SdkFlags.OpenTelemetryInterceptorInsertYield.alternativeConditions)({ + const actual = composeConditions(SdkFlags.OpenTelemetryInterceporsAvoidsExtraYields.alternativeConditions)({ info: {} as WorkflowInfo, sdkVersion: version, }); - t.is(actual, expected, `Expected OpenTelemetryInterceptorInsertYield on ${version} to evaluate as ${expected}`); + t.is( + actual, + expected, + `Expected OpenTelemetryInterceporsAvoidsExtraYields on ${version} to evaluate as ${expected}` + ); } }); -test('OpenTelemetryScheduleLocalActivityInterceptorInsertYield enabled by version', (t) => { +test('OpenTelemetryInterceptorsTracesLocalActivities enabled by version', (t) => { const cases = [ { version: undefined, expected: false }, { version: '1.0.0', expected: false }, @@ -70,20 +74,18 @@ test('OpenTelemetryScheduleLocalActivityInterceptorInsertYield enabled by versio { version: '1.11.6', expected: true }, { version: '1.12.0', expected: true }, { version: '1.13.1', expected: true }, - { version: '1.13.2', expected: false }, - { version: '1.14.0', expected: false }, + { version: '1.13.2', expected: true }, + { version: '1.14.0', expected: true }, ]; for (const { version, expected } of cases) { - const actual = composeConditions( - SdkFlags.OpenTelemetryScheduleLocalActivityInterceptorInsertYield.alternativeConditions - )({ + const actual = composeConditions(SdkFlags.OpenTelemetryInterceptorsTracesLocalActivities.alternativeConditions)({ info: {} as WorkflowInfo, sdkVersion: version, }); t.is( actual, expected, - `Expected OpenTelemetryScheduleLocalActivityInterceptorInsertYield on ${version} to evaluate as ${expected}` + `Expected OpenTelemetryInterceptorsTracesLocalActivities on ${version} to evaluate as ${expected}` ); } }); diff --git a/packages/workflow/src/flags.ts b/packages/workflow/src/flags.ts index dbcc3bfaf..40c994c8b 100644 --- a/packages/workflow/src/flags.ts +++ b/packages/workflow/src/flags.ts @@ -46,37 +46,31 @@ export const SdkFlags = { ProcessWorkflowActivationJobsAsSingleBatch: defineFlag(2, true, [buildIdSdkVersionMatches(/1\.11\.[01]/)]), /** - * In 1.11.3 and previous versions, the interceptor for `handleSignal` provided - * by @temporalio/interceptors-opentelemetry did not have a yield point in it. - * A yield point was accidentally added in later versions. This added yield point - * can cause NDE if there was a signal handler and the workflow was started with a signal. + * In 1.11.5 the `handleSignal` interceptor was added to @temporalio/interceptors-opentelemetry + * which added a yield point. The added yield point can cause NDE if there was a signal handler and + * the workflow was started with a signal. * * This yield point was removed in 1.13.2, but in order to prevent workflows from the * affected versions resulting in NDE, we have to inject the yield point on replay. - * This flag should be enabled for SDK versions newer than 1.11.3 and older than 1.13.2. - * * @since Introduced in 1.13.2. */ - OpenTelemetryInterceptorsTracesInboundSignals: defineFlag(3, false, [isAtLeast({ major: 1, minor: 11, patch: 5 })]), + OpenTelemetryInterceptorsTracesInboundSignals: defineFlag(3, true, [isAtLeast({ major: 1, minor: 11, patch: 5 })]), /** * In 1.11.6, the `scheduleLocalActivity` interceptor was added to * `@temporalio/interceptors-opentelemetry` which added a yield point to the * outbound interceptor. This yield point was removed in 1.13.2. * - * If replaying a workflow from 1.11.6 up to 1.13.1, we insert a yield point - * in the interceptor to match the behavior. - * * @since Introduced in 1.13.2 */ - OpenTelemetryInterceptorsTracesLocalActivities: defineFlag(4, false, [isAtLeast({ major: 1, minor: 11, patch: 6 })]), + OpenTelemetryInterceptorsTracesLocalActivities: defineFlag(4, true, [isAtLeast({ major: 1, minor: 11, patch: 6 })]), /** * The interceptors provided by @temporalio/interceptors-opentelemetry initially had unnecessary * yield points on calling to `extractContextFromHeaders`. * If replaying a workflow created from these versions a yield point is injected to prevent any NDE. * - * If the history does not include the SDK version, default to enabled since the yields were present since the OTEL + * If the history does not include the SDK version, default to extra yields since the yields were present since the OTEL * package was created. * * @since Introduced in 1.13.2 @@ -120,28 +114,13 @@ type SemVer = { }; /** - * Creates an `AltConditionFn` that checks if the SDK version is before the provided version. - * An optional default can be provided in case the SDK version is not available. - */ -function isBefore(compare: SemVer, missingDefault?: boolean): AltConditionFn { - return isCompared(compare, 1, missingDefault); -} - -/** - * Creates an `AltConditionFn` that checks if the SDK version is after the provided version. + * Creates an `AltConditionFn` that checks if the SDK version is equal to or after the provided version. * An optional default can be provided in case the SDK version is not available. */ -function isAfter(compare: SemVer, missingDefault?: boolean): AltConditionFn { - return isCompared(compare, -1, missingDefault); -} - -/** - * Creates an `AltConditionFn` that checks if the SDK version is between the provided versions. - * The range check is exclusive. - * An optional default can be provided in case the SDK version is not available. - */ -function isBetween(lowEnd: SemVer, highEnd: SemVer, missingDefault?: boolean): AltConditionFn { - return (ctx) => isAfter(lowEnd, missingDefault)(ctx) && isBefore(highEnd, missingDefault)(ctx); +function isAtLeast(compare: SemVer, missingDefault?: boolean): AltConditionFn { + return (ctx) => { + return isCompared(compare, -1, missingDefault)(ctx) || isCompared(compare, 0, missingDefault)(ctx); + }; } function isCompared(compare: SemVer, comparison: -1 | 0 | 1, missingDefault: boolean = false): AltConditionFn {