diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ddd464f..91dc56064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,13 @@ to include examples, links to docs, or any other relevant information. ### Added +- **Experimental**: Event Groups tag logically related commands so that the UI and CLI can + visualize, analyze, and debug them together. Create a group with + `workflow.create_event_group(label)`, then attach it either per call + (`workflow.start_activity(..., event_groups=[group])`) or ambiently to everything issued inside + `with group.scope():`. Each signal and update handler is also implicitly wrapped in a group of + its own. Requires a server that understands the Event Groups fields. + ### Changed ### Deprecated diff --git a/README.md b/README.md index 7a1caedd9..b7b1810c7 100644 --- a/README.md +++ b/README.md @@ -976,6 +976,49 @@ await workflow.wait_condition(workflow.all_handlers_finished) * `await handle.signal()` can be called on the handle to signal the external workflow * `await handle.cancel()` can be called on the handle to send a cancel to the external workflow +#### Event Groups + +Event Groups regroup logically related events of a Workflow Execution's history, so that UIs and other tools can +present them together. A group is created with `workflow.create_event_group(label)` and can be attached to the +commands a workflow produces, either explicitly through the `event_groups` option of the API producing the command, +or implicitly to every command produced within `group.scope()`: + +```python +@workflow.defn +class MyWorkflow: + @workflow.run + async def run(self) -> None: + payment_group = workflow.create_event_group("payment-processing") + customer_group = workflow.create_event_group( + "customer-james-watkins", id="customer-123456" + ) + + # Explicit attachment of Event Groups to a single command + await workflow.execute_activity( + my_activity, + arg, + start_to_close_timeout=timedelta(minutes=1), + event_groups=[payment_group, customer_group], + ) + + # Scope-based propagation, applying to every command produced in the block + with payment_group.scope(), customer_group.scope(): + await authorize_payment(...) + await capture_payment(...) +``` + +Scopes nest, and coroutines started inside a scope inherit it, since they capture the context active at their +creation. Two Event Groups group events together if and only if they have the same id; by default the id is derived +deterministically from the label, so two groups created with the same label in the same execution are the same group. +Pass an explicit `id` to distinguish groups that share a label, or to group events under a business identifier. Note +that a derived id is a hash of the label, so avoid putting sensitive information in labels of groups without an +explicit id. + +The SDK also creates Event Groups implicitly around the workflow main method, signal handlers, and update handlers, so +that the commands they produce are grouped with the event that triggered them. + +WARNING: Event Groups is an experimental API and may change without notice. + #### Testing Workflow testing can be done in an integration-test fashion against a real server, however it is hard to simulate diff --git a/temporalio/bridge/Cargo.lock b/temporalio/bridge/Cargo.lock index 2352c004f..cfa4f9f5f 100644 --- a/temporalio/bridge/Cargo.lock +++ b/temporalio/bridge/Cargo.lock @@ -2582,7 +2582,7 @@ dependencies = [ [[package]] name = "temporalio-client" -version = "0.6.0" +version = "0.7.0" dependencies = [ "anyhow", "async-trait", @@ -2614,7 +2614,7 @@ dependencies = [ [[package]] name = "temporalio-common" -version = "0.6.0" +version = "0.7.0" dependencies = [ "anyhow", "async-trait", @@ -2655,7 +2655,7 @@ dependencies = [ [[package]] name = "temporalio-common-wasm" -version = "0.6.0" +version = "0.7.0" dependencies = [ "anyhow", "async-trait", @@ -2680,7 +2680,7 @@ dependencies = [ [[package]] name = "temporalio-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "proc-macro2", "quote", @@ -2689,7 +2689,7 @@ dependencies = [ [[package]] name = "temporalio-protos" -version = "0.6.0" +version = "0.7.0" dependencies = [ "anyhow", "base64", @@ -2710,7 +2710,7 @@ dependencies = [ [[package]] name = "temporalio-sdk-core" -version = "0.6.0" +version = "0.7.0" dependencies = [ "anyhow", "async-trait", diff --git a/temporalio/bridge/Cargo.toml b/temporalio/bridge/Cargo.toml index b9391a0bb..3e4b3d37b 100644 --- a/temporalio/bridge/Cargo.toml +++ b/temporalio/bridge/Cargo.toml @@ -58,15 +58,15 @@ pythonize = "0.29" # default-features disabled so the rustls provider isn't pinned to `tls-ring`; # the bridge's tls-ring/tls-aws-lc features (above) select it. Re-add the # crate's non-TLS default (`envconfig`). -temporalio-client = { version = "0.6", path = "./sdk-core/crates/client", default-features = false, features = [ +temporalio-client = { version = "0.7", path = "./sdk-core/crates/client", default-features = false, features = [ "envconfig", ] } -temporalio-common = { version = "0.6", path = "./sdk-core/crates/common", features = [ +temporalio-common = { version = "0.7", path = "./sdk-core/crates/common", features = [ "envconfig", "otel" ]} # default-features disabled (drops the pinned `tls-ring`); re-add the non-TLS # defaults the bridge relied on (`envconfig`, `prometheus`) plus `ephemeral-server`. -temporalio-sdk-core = { version = "0.6", path = "./sdk-core/crates/sdk-core", default-features = false, features = [ +temporalio-sdk-core = { version = "0.7", path = "./sdk-core/crates/sdk-core", default-features = false, features = [ "ephemeral-server", "envconfig", "prometheus", diff --git a/temporalio/bridge/_visitor.py b/temporalio/bridge/_visitor.py index 2bccd3b4a..5882a1de2 100644 --- a/temporalio/bridge/_visitor.py +++ b/temporalio/bridge/_visitor.py @@ -428,6 +428,12 @@ async def _visit_coresdk_workflow_commands_ContinueAsNewWorkflowExecution( fs, o.search_attributes ) + async def _visit_coresdk_workflow_commands_CancelWorkflowExecution( + self, fs: VisitorFunctions, o: Any + ): + if o.HasField("details"): + await self._visit_temporal_api_common_v1_Payloads(fs, o.details) + async def _visit_coresdk_workflow_commands_StartChildWorkflowExecution( self, fs: VisitorFunctions, o: Any ): @@ -513,6 +519,10 @@ async def _visit_coresdk_workflow_commands_WorkflowCommand( await self._visit_coresdk_workflow_commands_ContinueAsNewWorkflowExecution( fs, o.continue_as_new_workflow_execution ) + elif o.HasField("cancel_workflow_execution"): + await self._visit_coresdk_workflow_commands_CancelWorkflowExecution( + fs, o.cancel_workflow_execution + ) elif o.HasField("start_child_workflow_execution"): await self._visit_coresdk_workflow_commands_StartChildWorkflowExecution( fs, o.start_child_workflow_execution diff --git a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py index 22db825b5..0caca7d48 100644 --- a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py +++ b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py @@ -44,7 +44,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n?temporal/sdk/core/workflow_activation/workflow_activation.proto\x12\x1b\x63oresdk.workflow_activation\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a%temporal/api/failure/v1/message.proto\x1a$temporal/api/update/v1/message.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a\x37temporal/sdk/core/activity_result/activity_result.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a%temporal/sdk/core/common/common.proto\x1a#temporal/sdk/core/nexus/nexus.proto"\xa2\x04\n\x12WorkflowActivation\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12-\n\ttimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x14\n\x0cis_replaying\x18\x03 \x01(\x08\x12\x16\n\x0ehistory_length\x18\x04 \x01(\r\x12@\n\x04jobs\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.WorkflowActivationJob\x12 \n\x18\x61vailable_internal_flags\x18\x06 \x03(\r\x12\x1a\n\x12history_size_bytes\x18\x07 \x01(\x04\x12!\n\x19\x63ontinue_as_new_suggested\x18\x08 \x01(\x08\x12T\n#deployment_version_for_current_task\x18\t \x01(\x0b\x32\'.coresdk.common.WorkerDeploymentVersion\x12\x18\n\x10last_sdk_version\x18\n \x01(\t\x12Z\n\x1fsuggest_continue_as_new_reasons\x18\x0b \x03(\x0e\x32\x31.temporal.api.enums.v1.SuggestContinueAsNewReason\x12\x30\n(target_worker_deployment_version_changed\x18\x0c \x01(\x08"\xe0\n\n\x15WorkflowActivationJob\x12N\n\x13initialize_workflow\x18\x01 \x01(\x0b\x32/.coresdk.workflow_activation.InitializeWorkflowH\x00\x12<\n\nfire_timer\x18\x02 \x01(\x0b\x32&.coresdk.workflow_activation.FireTimerH\x00\x12K\n\x12update_random_seed\x18\x04 \x01(\x0b\x32-.coresdk.workflow_activation.UpdateRandomSeedH\x00\x12\x44\n\x0equery_workflow\x18\x05 \x01(\x0b\x32*.coresdk.workflow_activation.QueryWorkflowH\x00\x12\x46\n\x0f\x63\x61ncel_workflow\x18\x06 \x01(\x0b\x32+.coresdk.workflow_activation.CancelWorkflowH\x00\x12\x46\n\x0fsignal_workflow\x18\x07 \x01(\x0b\x32+.coresdk.workflow_activation.SignalWorkflowH\x00\x12H\n\x10resolve_activity\x18\x08 \x01(\x0b\x32,.coresdk.workflow_activation.ResolveActivityH\x00\x12G\n\x10notify_has_patch\x18\t \x01(\x0b\x32+.coresdk.workflow_activation.NotifyHasPatchH\x00\x12q\n&resolve_child_workflow_execution_start\x18\n \x01(\x0b\x32?.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartH\x00\x12\x66\n resolve_child_workflow_execution\x18\x0b \x01(\x0b\x32:.coresdk.workflow_activation.ResolveChildWorkflowExecutionH\x00\x12\x66\n resolve_signal_external_workflow\x18\x0c \x01(\x0b\x32:.coresdk.workflow_activation.ResolveSignalExternalWorkflowH\x00\x12u\n(resolve_request_cancel_external_workflow\x18\r \x01(\x0b\x32\x41.coresdk.workflow_activation.ResolveRequestCancelExternalWorkflowH\x00\x12:\n\tdo_update\x18\x0e \x01(\x0b\x32%.coresdk.workflow_activation.DoUpdateH\x00\x12`\n\x1dresolve_nexus_operation_start\x18\x0f \x01(\x0b\x32\x37.coresdk.workflow_activation.ResolveNexusOperationStartH\x00\x12U\n\x17resolve_nexus_operation\x18\x10 \x01(\x0b\x32\x32.coresdk.workflow_activation.ResolveNexusOperationH\x00\x12I\n\x11remove_from_cache\x18\x32 \x01(\x0b\x32,.coresdk.workflow_activation.RemoveFromCacheH\x00\x42\t\n\x07variant"\x9a\x0b\n\x12InitializeWorkflow\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x17\n\x0frandomness_seed\x18\x04 \x01(\x04\x12M\n\x07headers\x18\x05 \x03(\x0b\x32<.coresdk.workflow_activation.InitializeWorkflow.HeadersEntry\x12\x10\n\x08identity\x18\x06 \x01(\t\x12I\n\x14parent_workflow_info\x18\x07 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecution\x12=\n\x1aworkflow_execution_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\'\n\x1f\x63ontinued_from_execution_run_id\x18\x0b \x01(\t\x12J\n\x13\x63ontinued_initiator\x18\x0c \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12;\n\x11\x63ontinued_failure\x18\r \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\x0e \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x1e\n\x16\x66irst_execution_run_id\x18\x0f \x01(\t\x12\x39\n\x0cretry_policy\x18\x10 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x0f\n\x07\x61ttempt\x18\x11 \x01(\x05\x12\x15\n\rcron_schedule\x18\x12 \x01(\t\x12\x46\n"workflow_execution_expiration_time\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n"cron_schedule_to_schedule_interval\x18\x14 \x01(\x0b\x32\x19.google.protobuf.Duration\x12*\n\x04memo\x18\x15 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x16 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\nstart_time\x18\x17 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12@\n\rroot_workflow\x18\x18 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x32\n\x08priority\x18\x19 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12\x1c\n\x14originating_event_id\x18\x1a \x01(\x03\x12!\n\x19original_execution_run_id\x18\x1b \x01(\t\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x18\n\tFireTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"m\n\x0fResolveActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.activity_result.ActivityResolution\x12\x10\n\x08is_local\x18\x03 \x01(\x08"\xd1\x02\n"ResolveChildWorkflowExecutionStart\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12[\n\tsucceeded\x18\x02 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartSuccessH\x00\x12X\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartFailureH\x00\x12]\n\tcancelled\x18\x04 \x01(\x0b\x32H.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartCancelledH\x00\x42\x08\n\x06status";\n)ResolveChildWorkflowExecutionStartSuccess\x12\x0e\n\x06run_id\x18\x01 \x01(\t"\xa6\x01\n)ResolveChildWorkflowExecutionStartFailure\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x15\n\rworkflow_type\x18\x02 \x01(\t\x12M\n\x05\x63\x61use\x18\x03 \x01(\x0e\x32>.coresdk.child_workflow.StartChildWorkflowExecutionFailedCause"`\n+ResolveChildWorkflowExecutionStartCancelled\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"i\n\x1dResolveChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.child_workflow.ChildWorkflowResult"+\n\x10UpdateRandomSeed\x12\x17\n\x0frandomness_seed\x18\x01 \x01(\x04"\x84\x02\n\rQueryWorkflow\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12\x12\n\nquery_type\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12H\n\x07headers\x18\x05 \x03(\x0b\x32\x37.coresdk.workflow_activation.QueryWorkflow.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01" \n\x0e\x43\x61ncelWorkflow\x12\x0e\n\x06reason\x18\x01 \x01(\t"\xa1\x02\n\x0eSignalWorkflow\x12\x13\n\x0bsignal_name\x18\x01 \x01(\t\x12.\n\x05input\x18\x02 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x10\n\x08identity\x18\x03 \x01(\t\x12I\n\x07headers\x18\x05 \x03(\x0b\x32\x38.coresdk.workflow_activation.SignalWorkflow.HeadersEntry\x12\x1c\n\x14originating_event_id\x18\x06 \x01(\x03\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01""\n\x0eNotifyHasPatch\x12\x10\n\x08patch_id\x18\x01 \x01(\t"_\n\x1dResolveSignalExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"f\n$ResolveRequestCancelExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xcb\x02\n\x08\x44oUpdate\x12\n\n\x02id\x18\x01 \x01(\t\x12\x1c\n\x14protocol_instance_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12.\n\x05input\x18\x04 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x43\n\x07headers\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.DoUpdate.HeadersEntry\x12*\n\x04meta\x18\x06 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12\x15\n\rrun_validator\x18\x07 \x01(\x08\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x9a\x01\n\x1aResolveNexusOperationStart\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x19\n\x0foperation_token\x18\x02 \x01(\tH\x00\x12\x16\n\x0cstarted_sync\x18\x03 \x01(\x08H\x00\x12\x32\n\x06\x66\x61iled\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\x08\n\x06status"Y\n\x15ResolveNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x33\n\x06result\x18\x02 \x01(\x0b\x32#.coresdk.nexus.NexusOperationResult"\xe0\x02\n\x0fRemoveFromCache\x12\x0f\n\x07message\x18\x01 \x01(\t\x12K\n\x06reason\x18\x02 \x01(\x0e\x32;.coresdk.workflow_activation.RemoveFromCache.EvictionReason"\xee\x01\n\x0e\x45victionReason\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0e\n\nCACHE_FULL\x10\x01\x12\x0e\n\nCACHE_MISS\x10\x02\x12\x12\n\x0eNONDETERMINISM\x10\x03\x12\r\n\tLANG_FAIL\x10\x04\x12\x12\n\x0eLANG_REQUESTED\x10\x05\x12\x12\n\x0eTASK_NOT_FOUND\x10\x06\x12\x15\n\x11UNHANDLED_COMMAND\x10\x07\x12\t\n\x05\x46\x41TAL\x10\x08\x12\x1f\n\x1bPAGINATION_OR_HISTORY_FETCH\x10\t\x12\x1d\n\x19WORKFLOW_EXECUTION_ENDING\x10\nB8\xea\x02\x35Temporalio::Internal::Bridge::Api::WorkflowActivationb\x06proto3' + b'\n?temporal/sdk/core/workflow_activation/workflow_activation.proto\x12\x1b\x63oresdk.workflow_activation\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a%temporal/api/failure/v1/message.proto\x1a$temporal/api/update/v1/message.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a\x37temporal/sdk/core/activity_result/activity_result.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a%temporal/sdk/core/common/common.proto\x1a#temporal/sdk/core/nexus/nexus.proto"\xa2\x04\n\x12WorkflowActivation\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12-\n\ttimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x14\n\x0cis_replaying\x18\x03 \x01(\x08\x12\x16\n\x0ehistory_length\x18\x04 \x01(\r\x12@\n\x04jobs\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.WorkflowActivationJob\x12 \n\x18\x61vailable_internal_flags\x18\x06 \x03(\r\x12\x1a\n\x12history_size_bytes\x18\x07 \x01(\x04\x12!\n\x19\x63ontinue_as_new_suggested\x18\x08 \x01(\x08\x12T\n#deployment_version_for_current_task\x18\t \x01(\x0b\x32\'.coresdk.common.WorkerDeploymentVersion\x12\x18\n\x10last_sdk_version\x18\n \x01(\t\x12Z\n\x1fsuggest_continue_as_new_reasons\x18\x0b \x03(\x0e\x32\x31.temporal.api.enums.v1.SuggestContinueAsNewReason\x12\x30\n(target_worker_deployment_version_changed\x18\x0c \x01(\x08"\xe0\n\n\x15WorkflowActivationJob\x12N\n\x13initialize_workflow\x18\x01 \x01(\x0b\x32/.coresdk.workflow_activation.InitializeWorkflowH\x00\x12<\n\nfire_timer\x18\x02 \x01(\x0b\x32&.coresdk.workflow_activation.FireTimerH\x00\x12K\n\x12update_random_seed\x18\x04 \x01(\x0b\x32-.coresdk.workflow_activation.UpdateRandomSeedH\x00\x12\x44\n\x0equery_workflow\x18\x05 \x01(\x0b\x32*.coresdk.workflow_activation.QueryWorkflowH\x00\x12\x46\n\x0f\x63\x61ncel_workflow\x18\x06 \x01(\x0b\x32+.coresdk.workflow_activation.CancelWorkflowH\x00\x12\x46\n\x0fsignal_workflow\x18\x07 \x01(\x0b\x32+.coresdk.workflow_activation.SignalWorkflowH\x00\x12H\n\x10resolve_activity\x18\x08 \x01(\x0b\x32,.coresdk.workflow_activation.ResolveActivityH\x00\x12G\n\x10notify_has_patch\x18\t \x01(\x0b\x32+.coresdk.workflow_activation.NotifyHasPatchH\x00\x12q\n&resolve_child_workflow_execution_start\x18\n \x01(\x0b\x32?.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartH\x00\x12\x66\n resolve_child_workflow_execution\x18\x0b \x01(\x0b\x32:.coresdk.workflow_activation.ResolveChildWorkflowExecutionH\x00\x12\x66\n resolve_signal_external_workflow\x18\x0c \x01(\x0b\x32:.coresdk.workflow_activation.ResolveSignalExternalWorkflowH\x00\x12u\n(resolve_request_cancel_external_workflow\x18\r \x01(\x0b\x32\x41.coresdk.workflow_activation.ResolveRequestCancelExternalWorkflowH\x00\x12:\n\tdo_update\x18\x0e \x01(\x0b\x32%.coresdk.workflow_activation.DoUpdateH\x00\x12`\n\x1dresolve_nexus_operation_start\x18\x0f \x01(\x0b\x32\x37.coresdk.workflow_activation.ResolveNexusOperationStartH\x00\x12U\n\x17resolve_nexus_operation\x18\x10 \x01(\x0b\x32\x32.coresdk.workflow_activation.ResolveNexusOperationH\x00\x12I\n\x11remove_from_cache\x18\x32 \x01(\x0b\x32,.coresdk.workflow_activation.RemoveFromCacheH\x00\x42\t\n\x07variant"\xfc\n\n\x12InitializeWorkflow\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x17\n\x0frandomness_seed\x18\x04 \x01(\x04\x12M\n\x07headers\x18\x05 \x03(\x0b\x32<.coresdk.workflow_activation.InitializeWorkflow.HeadersEntry\x12\x10\n\x08identity\x18\x06 \x01(\t\x12I\n\x14parent_workflow_info\x18\x07 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecution\x12=\n\x1aworkflow_execution_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\'\n\x1f\x63ontinued_from_execution_run_id\x18\x0b \x01(\t\x12J\n\x13\x63ontinued_initiator\x18\x0c \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12;\n\x11\x63ontinued_failure\x18\r \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\x0e \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x1e\n\x16\x66irst_execution_run_id\x18\x0f \x01(\t\x12\x39\n\x0cretry_policy\x18\x10 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x0f\n\x07\x61ttempt\x18\x11 \x01(\x05\x12\x15\n\rcron_schedule\x18\x12 \x01(\t\x12\x46\n"workflow_execution_expiration_time\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n"cron_schedule_to_schedule_interval\x18\x14 \x01(\x0b\x32\x19.google.protobuf.Duration\x12*\n\x04memo\x18\x15 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x16 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\nstart_time\x18\x17 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12@\n\rroot_workflow\x18\x18 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x32\n\x08priority\x18\x19 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12!\n\x19original_execution_run_id\x18\x1a \x01(\t\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x18\n\tFireTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"m\n\x0fResolveActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.activity_result.ActivityResolution\x12\x10\n\x08is_local\x18\x03 \x01(\x08"\xd1\x02\n"ResolveChildWorkflowExecutionStart\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12[\n\tsucceeded\x18\x02 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartSuccessH\x00\x12X\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartFailureH\x00\x12]\n\tcancelled\x18\x04 \x01(\x0b\x32H.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartCancelledH\x00\x42\x08\n\x06status";\n)ResolveChildWorkflowExecutionStartSuccess\x12\x0e\n\x06run_id\x18\x01 \x01(\t"\xa6\x01\n)ResolveChildWorkflowExecutionStartFailure\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x15\n\rworkflow_type\x18\x02 \x01(\t\x12M\n\x05\x63\x61use\x18\x03 \x01(\x0e\x32>.coresdk.child_workflow.StartChildWorkflowExecutionFailedCause"`\n+ResolveChildWorkflowExecutionStartCancelled\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"i\n\x1dResolveChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.child_workflow.ChildWorkflowResult"+\n\x10UpdateRandomSeed\x12\x17\n\x0frandomness_seed\x18\x01 \x01(\x04"\x84\x02\n\rQueryWorkflow\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12\x12\n\nquery_type\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12H\n\x07headers\x18\x05 \x03(\x0b\x32\x37.coresdk.workflow_activation.QueryWorkflow.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01" \n\x0e\x43\x61ncelWorkflow\x12\x0e\n\x06reason\x18\x01 \x01(\t"\xa1\x02\n\x0eSignalWorkflow\x12\x13\n\x0bsignal_name\x18\x01 \x01(\t\x12.\n\x05input\x18\x02 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x10\n\x08identity\x18\x03 \x01(\t\x12I\n\x07headers\x18\x05 \x03(\x0b\x32\x38.coresdk.workflow_activation.SignalWorkflow.HeadersEntry\x12\x1c\n\x14originating_event_id\x18\x06 \x01(\x03\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01""\n\x0eNotifyHasPatch\x12\x10\n\x08patch_id\x18\x01 \x01(\t"_\n\x1dResolveSignalExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"f\n$ResolveRequestCancelExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xcb\x02\n\x08\x44oUpdate\x12\n\n\x02id\x18\x01 \x01(\t\x12\x1c\n\x14protocol_instance_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12.\n\x05input\x18\x04 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x43\n\x07headers\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.DoUpdate.HeadersEntry\x12*\n\x04meta\x18\x06 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12\x15\n\rrun_validator\x18\x07 \x01(\x08\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x9a\x01\n\x1aResolveNexusOperationStart\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x19\n\x0foperation_token\x18\x02 \x01(\tH\x00\x12\x16\n\x0cstarted_sync\x18\x03 \x01(\x08H\x00\x12\x32\n\x06\x66\x61iled\x18\x04 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\x08\n\x06status"Y\n\x15ResolveNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x33\n\x06result\x18\x02 \x01(\x0b\x32#.coresdk.nexus.NexusOperationResult"\xe0\x02\n\x0fRemoveFromCache\x12\x0f\n\x07message\x18\x01 \x01(\t\x12K\n\x06reason\x18\x02 \x01(\x0e\x32;.coresdk.workflow_activation.RemoveFromCache.EvictionReason"\xee\x01\n\x0e\x45victionReason\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0e\n\nCACHE_FULL\x10\x01\x12\x0e\n\nCACHE_MISS\x10\x02\x12\x12\n\x0eNONDETERMINISM\x10\x03\x12\r\n\tLANG_FAIL\x10\x04\x12\x12\n\x0eLANG_REQUESTED\x10\x05\x12\x12\n\x0eTASK_NOT_FOUND\x10\x06\x12\x15\n\x11UNHANDLED_COMMAND\x10\x07\x12\t\n\x05\x46\x41TAL\x10\x08\x12\x1f\n\x1bPAGINATION_OR_HISTORY_FETCH\x10\t\x12\x1d\n\x19WORKFLOW_EXECUTION_ENDING\x10\nB8\xea\x02\x35Temporalio::Internal::Bridge::Api::WorkflowActivationb\x06proto3' ) @@ -381,51 +381,51 @@ _WORKFLOWACTIVATIONJOB._serialized_start = 1081 _WORKFLOWACTIVATIONJOB._serialized_end = 2457 _INITIALIZEWORKFLOW._serialized_start = 2460 - _INITIALIZEWORKFLOW._serialized_end = 3894 - _INITIALIZEWORKFLOW_HEADERSENTRY._serialized_start = 3815 - _INITIALIZEWORKFLOW_HEADERSENTRY._serialized_end = 3894 - _FIRETIMER._serialized_start = 3896 - _FIRETIMER._serialized_end = 3920 - _RESOLVEACTIVITY._serialized_start = 3922 - _RESOLVEACTIVITY._serialized_end = 4031 - _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_start = 4034 - _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_end = 4371 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_start = 4373 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_end = 4432 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_start = 4435 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_end = 4601 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_start = 4603 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_end = 4699 - _RESOLVECHILDWORKFLOWEXECUTION._serialized_start = 4701 - _RESOLVECHILDWORKFLOWEXECUTION._serialized_end = 4806 - _UPDATERANDOMSEED._serialized_start = 4808 - _UPDATERANDOMSEED._serialized_end = 4851 - _QUERYWORKFLOW._serialized_start = 4854 - _QUERYWORKFLOW._serialized_end = 5114 - _QUERYWORKFLOW_HEADERSENTRY._serialized_start = 3815 - _QUERYWORKFLOW_HEADERSENTRY._serialized_end = 3894 - _CANCELWORKFLOW._serialized_start = 5116 - _CANCELWORKFLOW._serialized_end = 5148 - _SIGNALWORKFLOW._serialized_start = 5151 - _SIGNALWORKFLOW._serialized_end = 5440 - _SIGNALWORKFLOW_HEADERSENTRY._serialized_start = 3815 - _SIGNALWORKFLOW_HEADERSENTRY._serialized_end = 3894 - _NOTIFYHASPATCH._serialized_start = 5442 - _NOTIFYHASPATCH._serialized_end = 5476 - _RESOLVESIGNALEXTERNALWORKFLOW._serialized_start = 5478 - _RESOLVESIGNALEXTERNALWORKFLOW._serialized_end = 5573 - _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_start = 5575 - _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_end = 5677 - _DOUPDATE._serialized_start = 5680 - _DOUPDATE._serialized_end = 6011 - _DOUPDATE_HEADERSENTRY._serialized_start = 3815 - _DOUPDATE_HEADERSENTRY._serialized_end = 3894 - _RESOLVENEXUSOPERATIONSTART._serialized_start = 6014 - _RESOLVENEXUSOPERATIONSTART._serialized_end = 6168 - _RESOLVENEXUSOPERATION._serialized_start = 6170 - _RESOLVENEXUSOPERATION._serialized_end = 6259 - _REMOVEFROMCACHE._serialized_start = 6262 - _REMOVEFROMCACHE._serialized_end = 6614 - _REMOVEFROMCACHE_EVICTIONREASON._serialized_start = 6376 - _REMOVEFROMCACHE_EVICTIONREASON._serialized_end = 6614 + _INITIALIZEWORKFLOW._serialized_end = 3864 + _INITIALIZEWORKFLOW_HEADERSENTRY._serialized_start = 3785 + _INITIALIZEWORKFLOW_HEADERSENTRY._serialized_end = 3864 + _FIRETIMER._serialized_start = 3866 + _FIRETIMER._serialized_end = 3890 + _RESOLVEACTIVITY._serialized_start = 3892 + _RESOLVEACTIVITY._serialized_end = 4001 + _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_start = 4004 + _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_end = 4341 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_start = 4343 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_end = 4402 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_start = 4405 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_end = 4571 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_start = 4573 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_end = 4669 + _RESOLVECHILDWORKFLOWEXECUTION._serialized_start = 4671 + _RESOLVECHILDWORKFLOWEXECUTION._serialized_end = 4776 + _UPDATERANDOMSEED._serialized_start = 4778 + _UPDATERANDOMSEED._serialized_end = 4821 + _QUERYWORKFLOW._serialized_start = 4824 + _QUERYWORKFLOW._serialized_end = 5084 + _QUERYWORKFLOW_HEADERSENTRY._serialized_start = 3785 + _QUERYWORKFLOW_HEADERSENTRY._serialized_end = 3864 + _CANCELWORKFLOW._serialized_start = 5086 + _CANCELWORKFLOW._serialized_end = 5118 + _SIGNALWORKFLOW._serialized_start = 5121 + _SIGNALWORKFLOW._serialized_end = 5410 + _SIGNALWORKFLOW_HEADERSENTRY._serialized_start = 3785 + _SIGNALWORKFLOW_HEADERSENTRY._serialized_end = 3864 + _NOTIFYHASPATCH._serialized_start = 5412 + _NOTIFYHASPATCH._serialized_end = 5446 + _RESOLVESIGNALEXTERNALWORKFLOW._serialized_start = 5448 + _RESOLVESIGNALEXTERNALWORKFLOW._serialized_end = 5543 + _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_start = 5545 + _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_end = 5647 + _DOUPDATE._serialized_start = 5650 + _DOUPDATE._serialized_end = 5981 + _DOUPDATE_HEADERSENTRY._serialized_start = 3785 + _DOUPDATE_HEADERSENTRY._serialized_end = 3864 + _RESOLVENEXUSOPERATIONSTART._serialized_start = 5984 + _RESOLVENEXUSOPERATIONSTART._serialized_end = 6138 + _RESOLVENEXUSOPERATION._serialized_start = 6140 + _RESOLVENEXUSOPERATION._serialized_end = 6229 + _REMOVEFROMCACHE._serialized_start = 6232 + _REMOVEFROMCACHE._serialized_end = 6584 + _REMOVEFROMCACHE_EVICTIONREASON._serialized_start = 6346 + _REMOVEFROMCACHE_EVICTIONREASON._serialized_end = 6584 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi index 2b641a34b..afc7eb4ab 100644 --- a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi +++ b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi @@ -480,7 +480,6 @@ class InitializeWorkflow(google.protobuf.message.Message): START_TIME_FIELD_NUMBER: builtins.int ROOT_WORKFLOW_FIELD_NUMBER: builtins.int PRIORITY_FIELD_NUMBER: builtins.int - ORIGINATING_EVENT_ID_FIELD_NUMBER: builtins.int ORIGINAL_EXECUTION_RUN_ID_FIELD_NUMBER: builtins.int workflow_type: builtins.str """The identifier the lang-specific sdk uses to execute workflow code""" @@ -584,8 +583,6 @@ class InitializeWorkflow(google.protobuf.message.Message): @property def priority(self) -> temporalio.api.common.v1.message_pb2.Priority: """Priority of this workflow execution""" - originating_event_id: builtins.int - """Event ID of the `WORKFLOW_EXECUTION_STARTED` history event that triggered this job.""" original_execution_run_id: builtins.str """The run id recorded on the `WORKFLOW_EXECUTION_STARTED` event. Unlike the execution's current run id, this value is preserved across workflow resets. Mirrors the `original_execution_run_id` @@ -631,7 +628,6 @@ class InitializeWorkflow(google.protobuf.message.Message): root_workflow: temporalio.api.common.v1.message_pb2.WorkflowExecution | None = ..., priority: temporalio.api.common.v1.message_pb2.Priority | None = ..., - originating_event_id: builtins.int = ..., original_execution_run_id: builtins.str = ..., ) -> None: ... def HasField( @@ -696,8 +692,6 @@ class InitializeWorkflow(google.protobuf.message.Message): b"memo", "original_execution_run_id", b"original_execution_run_id", - "originating_event_id", - b"originating_event_id", "parent_workflow_info", b"parent_workflow_info", "priority", diff --git a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py index 3f8bdaf69..64fa8a255 100644 --- a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py +++ b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py @@ -45,7 +45,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n;temporal/sdk/core/workflow_commands/workflow_commands.proto\x12\x19\x63oresdk.workflow_commands\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto\x1a,temporal/api/sdk/v1/event_group_marker.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a#temporal/sdk/core/nexus/nexus.proto\x1a%temporal/sdk/core/common/common.proto"\xa9\x10\n\x0fWorkflowCommand\x12\x38\n\ruser_metadata\x18\x64 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12\x42\n\x13\x65vent_group_markers\x18\x65 \x03(\x0b\x32%.temporal.api.sdk.v1.EventGroupMarker\x12<\n\x0bstart_timer\x18\x01 \x01(\x0b\x32%.coresdk.workflow_commands.StartTimerH\x00\x12H\n\x11schedule_activity\x18\x02 \x01(\x0b\x32+.coresdk.workflow_commands.ScheduleActivityH\x00\x12\x42\n\x10respond_to_query\x18\x03 \x01(\x0b\x32&.coresdk.workflow_commands.QueryResultH\x00\x12S\n\x17request_cancel_activity\x18\x04 \x01(\x0b\x32\x30.coresdk.workflow_commands.RequestCancelActivityH\x00\x12>\n\x0c\x63\x61ncel_timer\x18\x05 \x01(\x0b\x32&.coresdk.workflow_commands.CancelTimerH\x00\x12[\n\x1b\x63omplete_workflow_execution\x18\x06 \x01(\x0b\x32\x34.coresdk.workflow_commands.CompleteWorkflowExecutionH\x00\x12S\n\x17\x66\x61il_workflow_execution\x18\x07 \x01(\x0b\x32\x30.coresdk.workflow_commands.FailWorkflowExecutionH\x00\x12g\n"continue_as_new_workflow_execution\x18\x08 \x01(\x0b\x32\x39.coresdk.workflow_commands.ContinueAsNewWorkflowExecutionH\x00\x12W\n\x19\x63\x61ncel_workflow_execution\x18\t \x01(\x0b\x32\x32.coresdk.workflow_commands.CancelWorkflowExecutionH\x00\x12\x45\n\x10set_patch_marker\x18\n \x01(\x0b\x32).coresdk.workflow_commands.SetPatchMarkerH\x00\x12`\n\x1estart_child_workflow_execution\x18\x0b \x01(\x0b\x32\x36.coresdk.workflow_commands.StartChildWorkflowExecutionH\x00\x12\x62\n\x1f\x63\x61ncel_child_workflow_execution\x18\x0c \x01(\x0b\x32\x37.coresdk.workflow_commands.CancelChildWorkflowExecutionH\x00\x12w\n*request_cancel_external_workflow_execution\x18\r \x01(\x0b\x32\x41.coresdk.workflow_commands.RequestCancelExternalWorkflowExecutionH\x00\x12h\n"signal_external_workflow_execution\x18\x0e \x01(\x0b\x32:.coresdk.workflow_commands.SignalExternalWorkflowExecutionH\x00\x12Q\n\x16\x63\x61ncel_signal_workflow\x18\x0f \x01(\x0b\x32/.coresdk.workflow_commands.CancelSignalWorkflowH\x00\x12S\n\x17schedule_local_activity\x18\x10 \x01(\x0b\x32\x30.coresdk.workflow_commands.ScheduleLocalActivityH\x00\x12^\n\x1drequest_cancel_local_activity\x18\x11 \x01(\x0b\x32\x35.coresdk.workflow_commands.RequestCancelLocalActivityH\x00\x12\x66\n!upsert_workflow_search_attributes\x18\x12 \x01(\x0b\x32\x39.coresdk.workflow_commands.UpsertWorkflowSearchAttributesH\x00\x12Y\n\x1amodify_workflow_properties\x18\x13 \x01(\x0b\x32\x33.coresdk.workflow_commands.ModifyWorkflowPropertiesH\x00\x12\x44\n\x0fupdate_response\x18\x14 \x01(\x0b\x32).coresdk.workflow_commands.UpdateResponseH\x00\x12U\n\x18schedule_nexus_operation\x18\x15 \x01(\x0b\x32\x31.coresdk.workflow_commands.ScheduleNexusOperationH\x00\x12`\n\x1erequest_cancel_nexus_operation\x18\x16 \x01(\x0b\x32\x36.coresdk.workflow_commands.RequestCancelNexusOperationH\x00\x42\t\n\x07variant"S\n\nStartTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x38\n\x15start_to_fire_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"\x1a\n\x0b\x43\x61ncelTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"\xb8\x06\n\x10ScheduleActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12I\n\x07headers\x18\x06 \x03(\x0b\x32\x38.coresdk.workflow_commands.ScheduleActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x12\x1e\n\x16\x64o_not_eagerly_execute\x18\x0e \x01(\x08\x12;\n\x11versioning_intent\x18\x0f \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12\x32\n\x08priority\x18\x10 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\xee\x05\n\x15ScheduleLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x04 \x01(\r\x12:\n\x16original_schedule_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12N\n\x07headers\x18\x06 \x03(\x0b\x32=.coresdk.workflow_commands.ScheduleLocalActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0b \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x38\n\x15local_retry_threshold\x18\x0c \x01(\x0b\x32\x19.google.protobuf.Duration\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"$\n\x15RequestCancelActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r")\n\x1aRequestCancelLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r"\x9c\x01\n\x0bQueryResult\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12<\n\tsucceeded\x18\x02 \x01(\x0b\x32\'.coresdk.workflow_commands.QuerySuccessH\x00\x12\x32\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\t\n\x07variant"A\n\x0cQuerySuccess\x12\x31\n\x08response\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"L\n\x19\x43ompleteWorkflowExecution\x12/\n\x06result\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"J\n\x15\x46\x61ilWorkflowExecution\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\x92\x07\n\x1e\x43ontinueAsNewWorkflowExecution\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x37\n\x14workflow_run_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12Q\n\x04memo\x18\x06 \x03(\x0b\x32\x43.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.MemoEntry\x12W\n\x07headers\x18\x07 \x03(\x0b\x32\x46.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.HeadersEntry\x12\x43\n\x11search_attributes\x18\x08 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x39\n\x0cretry_policy\x18\t \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12;\n\x11versioning_intent\x18\n \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12[\n\x1binitial_versioning_behavior\x18\x0b \x01(\x0e\x32\x36.temporal.api.enums.v1.ContinueAsNewVersioningBehavior\x12\x39\n\x16\x62\x61\x63koff_start_interval\x18\x0c \x01(\x0b\x32\x19.google.protobuf.Duration\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x19\n\x17\x43\x61ncelWorkflowExecution"6\n\x0eSetPatchMarker\x12\x10\n\x08patch_id\x18\x01 \x01(\t\x12\x12\n\ndeprecated\x18\x02 \x01(\x08"\x96\t\n\x1bStartChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x03 \x01(\t\x12\x15\n\rworkflow_type\x18\x04 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12.\n\x05input\x18\x06 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12=\n\x1aworkflow_execution_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x46\n\x13parent_close_policy\x18\n \x01(\x0e\x32).coresdk.child_workflow.ParentClosePolicy\x12N\n\x18workflow_id_reuse_policy\x18\x0c \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\r \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\x0e \x01(\t\x12T\n\x07headers\x18\x0f \x03(\x0b\x32\x43.coresdk.workflow_commands.StartChildWorkflowExecution.HeadersEntry\x12N\n\x04memo\x18\x10 \x03(\x0b\x32@.coresdk.workflow_commands.StartChildWorkflowExecution.MemoEntry\x12\x43\n\x11search_attributes\x18\x11 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12P\n\x11\x63\x61ncellation_type\x18\x12 \x01(\x0e\x32\x35.coresdk.child_workflow.ChildWorkflowCancellationType\x12;\n\x11versioning_intent\x18\x13 \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12\x32\n\x08priority\x18\x14 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"J\n\x1c\x43\x61ncelChildWorkflowExecution\x12\x1a\n\x12\x63hild_workflow_seq\x18\x01 \x01(\r\x12\x0e\n\x06reason\x18\x02 \x01(\t"\x8e\x01\n&RequestCancelExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12G\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecution\x12\x0e\n\x06reason\x18\x03 \x01(\t"\x8f\x03\n\x1fSignalExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12I\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecutionH\x00\x12\x1b\n\x11\x63hild_workflow_id\x18\x03 \x01(\tH\x00\x12\x13\n\x0bsignal_name\x18\x04 \x01(\t\x12-\n\x04\x61rgs\x18\x05 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12X\n\x07headers\x18\x06 \x03(\x0b\x32G.coresdk.workflow_commands.SignalExternalWorkflowExecution.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x42\x08\n\x06target"#\n\x14\x43\x61ncelSignalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r"e\n\x1eUpsertWorkflowSearchAttributes\x12\x43\n\x11search_attributes\x18\x01 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes"O\n\x18ModifyWorkflowProperties\x12\x33\n\rupserted_memo\x18\x01 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\xd2\x01\n\x0eUpdateResponse\x12\x1c\n\x14protocol_instance_id\x18\x01 \x01(\t\x12*\n\x08\x61\x63\x63\x65pted\x18\x02 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x34\n\x08rejected\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x34\n\tcompleted\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.PayloadH\x00\x42\n\n\x08response"\x9a\x04\n\x16ScheduleNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x10\n\x08\x65ndpoint\x18\x02 \x01(\t\x12\x0f\n\x07service\x18\x03 \x01(\t\x12\x11\n\toperation\x18\x04 \x01(\t\x12.\n\x05input\x18\x05 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12X\n\x0cnexus_header\x18\x07 \x03(\x0b\x32\x42.coresdk.workflow_commands.ScheduleNexusOperation.NexusHeaderEntry\x12H\n\x11\x63\x61ncellation_type\x18\x08 \x01(\x0e\x32-.coresdk.nexus.NexusOperationCancellationType\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x1a\x32\n\x10NexusHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"*\n\x1bRequestCancelNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r*X\n\x18\x41\x63tivityCancellationType\x12\x0e\n\nTRY_CANCEL\x10\x00\x12\x1f\n\x1bWAIT_CANCELLATION_COMPLETED\x10\x01\x12\x0b\n\x07\x41\x42\x41NDON\x10\x02\x42\x36\xea\x02\x33Temporalio::Internal::Bridge::Api::WorkflowCommandsb\x06proto3' + b'\n;temporal/sdk/core/workflow_commands/workflow_commands.proto\x12\x19\x63oresdk.workflow_commands\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a\'temporal/api/sdk/v1/user_metadata.proto\x1a,temporal/api/sdk/v1/event_group_marker.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a#temporal/sdk/core/nexus/nexus.proto\x1a%temporal/sdk/core/common/common.proto"\xa9\x10\n\x0fWorkflowCommand\x12\x38\n\ruser_metadata\x18\x64 \x01(\x0b\x32!.temporal.api.sdk.v1.UserMetadata\x12\x42\n\x13\x65vent_group_markers\x18\x65 \x03(\x0b\x32%.temporal.api.sdk.v1.EventGroupMarker\x12<\n\x0bstart_timer\x18\x01 \x01(\x0b\x32%.coresdk.workflow_commands.StartTimerH\x00\x12H\n\x11schedule_activity\x18\x02 \x01(\x0b\x32+.coresdk.workflow_commands.ScheduleActivityH\x00\x12\x42\n\x10respond_to_query\x18\x03 \x01(\x0b\x32&.coresdk.workflow_commands.QueryResultH\x00\x12S\n\x17request_cancel_activity\x18\x04 \x01(\x0b\x32\x30.coresdk.workflow_commands.RequestCancelActivityH\x00\x12>\n\x0c\x63\x61ncel_timer\x18\x05 \x01(\x0b\x32&.coresdk.workflow_commands.CancelTimerH\x00\x12[\n\x1b\x63omplete_workflow_execution\x18\x06 \x01(\x0b\x32\x34.coresdk.workflow_commands.CompleteWorkflowExecutionH\x00\x12S\n\x17\x66\x61il_workflow_execution\x18\x07 \x01(\x0b\x32\x30.coresdk.workflow_commands.FailWorkflowExecutionH\x00\x12g\n"continue_as_new_workflow_execution\x18\x08 \x01(\x0b\x32\x39.coresdk.workflow_commands.ContinueAsNewWorkflowExecutionH\x00\x12W\n\x19\x63\x61ncel_workflow_execution\x18\t \x01(\x0b\x32\x32.coresdk.workflow_commands.CancelWorkflowExecutionH\x00\x12\x45\n\x10set_patch_marker\x18\n \x01(\x0b\x32).coresdk.workflow_commands.SetPatchMarkerH\x00\x12`\n\x1estart_child_workflow_execution\x18\x0b \x01(\x0b\x32\x36.coresdk.workflow_commands.StartChildWorkflowExecutionH\x00\x12\x62\n\x1f\x63\x61ncel_child_workflow_execution\x18\x0c \x01(\x0b\x32\x37.coresdk.workflow_commands.CancelChildWorkflowExecutionH\x00\x12w\n*request_cancel_external_workflow_execution\x18\r \x01(\x0b\x32\x41.coresdk.workflow_commands.RequestCancelExternalWorkflowExecutionH\x00\x12h\n"signal_external_workflow_execution\x18\x0e \x01(\x0b\x32:.coresdk.workflow_commands.SignalExternalWorkflowExecutionH\x00\x12Q\n\x16\x63\x61ncel_signal_workflow\x18\x0f \x01(\x0b\x32/.coresdk.workflow_commands.CancelSignalWorkflowH\x00\x12S\n\x17schedule_local_activity\x18\x10 \x01(\x0b\x32\x30.coresdk.workflow_commands.ScheduleLocalActivityH\x00\x12^\n\x1drequest_cancel_local_activity\x18\x11 \x01(\x0b\x32\x35.coresdk.workflow_commands.RequestCancelLocalActivityH\x00\x12\x66\n!upsert_workflow_search_attributes\x18\x12 \x01(\x0b\x32\x39.coresdk.workflow_commands.UpsertWorkflowSearchAttributesH\x00\x12Y\n\x1amodify_workflow_properties\x18\x13 \x01(\x0b\x32\x33.coresdk.workflow_commands.ModifyWorkflowPropertiesH\x00\x12\x44\n\x0fupdate_response\x18\x14 \x01(\x0b\x32).coresdk.workflow_commands.UpdateResponseH\x00\x12U\n\x18schedule_nexus_operation\x18\x15 \x01(\x0b\x32\x31.coresdk.workflow_commands.ScheduleNexusOperationH\x00\x12`\n\x1erequest_cancel_nexus_operation\x18\x16 \x01(\x0b\x32\x36.coresdk.workflow_commands.RequestCancelNexusOperationH\x00\x42\t\n\x07variant"S\n\nStartTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x38\n\x15start_to_fire_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"\x1a\n\x0b\x43\x61ncelTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"\xb8\x06\n\x10ScheduleActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12I\n\x07headers\x18\x06 \x03(\x0b\x32\x38.coresdk.workflow_commands.ScheduleActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x12\x1e\n\x16\x64o_not_eagerly_execute\x18\x0e \x01(\x08\x12;\n\x11versioning_intent\x18\x0f \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12\x32\n\x08priority\x18\x10 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\xee\x05\n\x15ScheduleLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x04 \x01(\r\x12:\n\x16original_schedule_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12N\n\x07headers\x18\x06 \x03(\x0b\x32=.coresdk.workflow_commands.ScheduleLocalActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0b \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x38\n\x15local_retry_threshold\x18\x0c \x01(\x0b\x32\x19.google.protobuf.Duration\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"$\n\x15RequestCancelActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r")\n\x1aRequestCancelLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r"\x9c\x01\n\x0bQueryResult\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12<\n\tsucceeded\x18\x02 \x01(\x0b\x32\'.coresdk.workflow_commands.QuerySuccessH\x00\x12\x32\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\t\n\x07variant"A\n\x0cQuerySuccess\x12\x31\n\x08response\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"L\n\x19\x43ompleteWorkflowExecution\x12/\n\x06result\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"J\n\x15\x46\x61ilWorkflowExecution\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\x92\x07\n\x1e\x43ontinueAsNewWorkflowExecution\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x37\n\x14workflow_run_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12Q\n\x04memo\x18\x06 \x03(\x0b\x32\x43.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.MemoEntry\x12W\n\x07headers\x18\x07 \x03(\x0b\x32\x46.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.HeadersEntry\x12\x43\n\x11search_attributes\x18\x08 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12\x39\n\x0cretry_policy\x18\t \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12;\n\x11versioning_intent\x18\n \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12[\n\x1binitial_versioning_behavior\x18\x0b \x01(\x0e\x32\x36.temporal.api.enums.v1.ContinueAsNewVersioningBehavior\x12\x39\n\x16\x62\x61\x63koff_start_interval\x18\x0c \x01(\x0b\x32\x19.google.protobuf.Duration\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"L\n\x17\x43\x61ncelWorkflowExecution\x12\x31\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32 .temporal.api.common.v1.Payloads"6\n\x0eSetPatchMarker\x12\x10\n\x08patch_id\x18\x01 \x01(\t\x12\x12\n\ndeprecated\x18\x02 \x01(\x08"\x96\t\n\x1bStartChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x03 \x01(\t\x12\x15\n\rworkflow_type\x18\x04 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12.\n\x05input\x18\x06 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12=\n\x1aworkflow_execution_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x46\n\x13parent_close_policy\x18\n \x01(\x0e\x32).coresdk.child_workflow.ParentClosePolicy\x12N\n\x18workflow_id_reuse_policy\x18\x0c \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\r \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\x0e \x01(\t\x12T\n\x07headers\x18\x0f \x03(\x0b\x32\x43.coresdk.workflow_commands.StartChildWorkflowExecution.HeadersEntry\x12N\n\x04memo\x18\x10 \x03(\x0b\x32@.coresdk.workflow_commands.StartChildWorkflowExecution.MemoEntry\x12\x43\n\x11search_attributes\x18\x11 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12P\n\x11\x63\x61ncellation_type\x18\x12 \x01(\x0e\x32\x35.coresdk.child_workflow.ChildWorkflowCancellationType\x12;\n\x11versioning_intent\x18\x13 \x01(\x0e\x32 .coresdk.common.VersioningIntent\x12\x32\n\x08priority\x18\x14 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"J\n\x1c\x43\x61ncelChildWorkflowExecution\x12\x1a\n\x12\x63hild_workflow_seq\x18\x01 \x01(\r\x12\x0e\n\x06reason\x18\x02 \x01(\t"\x8e\x01\n&RequestCancelExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12G\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecution\x12\x0e\n\x06reason\x18\x03 \x01(\t"\x8f\x03\n\x1fSignalExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12I\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecutionH\x00\x12\x1b\n\x11\x63hild_workflow_id\x18\x03 \x01(\tH\x00\x12\x13\n\x0bsignal_name\x18\x04 \x01(\t\x12-\n\x04\x61rgs\x18\x05 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12X\n\x07headers\x18\x06 \x03(\x0b\x32G.coresdk.workflow_commands.SignalExternalWorkflowExecution.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x42\x08\n\x06target"#\n\x14\x43\x61ncelSignalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r"e\n\x1eUpsertWorkflowSearchAttributes\x12\x43\n\x11search_attributes\x18\x01 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes"O\n\x18ModifyWorkflowProperties\x12\x33\n\rupserted_memo\x18\x01 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\xd2\x01\n\x0eUpdateResponse\x12\x1c\n\x14protocol_instance_id\x18\x01 \x01(\t\x12*\n\x08\x61\x63\x63\x65pted\x18\x02 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x34\n\x08rejected\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x34\n\tcompleted\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.PayloadH\x00\x42\n\n\x08response"\x9a\x04\n\x16ScheduleNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x10\n\x08\x65ndpoint\x18\x02 \x01(\t\x12\x0f\n\x07service\x18\x03 \x01(\t\x12\x11\n\toperation\x18\x04 \x01(\t\x12.\n\x05input\x18\x05 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12X\n\x0cnexus_header\x18\x07 \x03(\x0b\x32\x42.coresdk.workflow_commands.ScheduleNexusOperation.NexusHeaderEntry\x12H\n\x11\x63\x61ncellation_type\x18\x08 \x01(\x0e\x32-.coresdk.nexus.NexusOperationCancellationType\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x1a\x32\n\x10NexusHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"*\n\x1bRequestCancelNexusOperation\x12\x0b\n\x03seq\x18\x01 \x01(\r*X\n\x18\x41\x63tivityCancellationType\x12\x0e\n\nTRY_CANCEL\x10\x00\x12\x1f\n\x1bWAIT_CANCELLATION_COMPLETED\x10\x01\x12\x0b\n\x07\x41\x42\x41NDON\x10\x02\x42\x36\xea\x02\x33Temporalio::Internal::Bridge::Api::WorkflowCommandsb\x06proto3' ) _ACTIVITYCANCELLATIONTYPE = DESCRIPTOR.enum_types_by_name["ActivityCancellationType"] @@ -484,8 +484,8 @@ _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_options = b"8\001" _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._options = None _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._serialized_options = b"8\001" - _ACTIVITYCANCELLATIONTYPE._serialized_start = 8582 - _ACTIVITYCANCELLATIONTYPE._serialized_end = 8670 + _ACTIVITYCANCELLATIONTYPE._serialized_start = 8633 + _ACTIVITYCANCELLATIONTYPE._serialized_end = 8721 _WORKFLOWCOMMAND._serialized_start = 518 _WORKFLOWCOMMAND._serialized_end = 2607 _STARTTIMER._serialized_start = 2609 @@ -519,35 +519,35 @@ _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 3468 _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 3547 _CANCELWORKFLOWEXECUTION._serialized_start = 5680 - _CANCELWORKFLOWEXECUTION._serialized_end = 5705 - _SETPATCHMARKER._serialized_start = 5707 - _SETPATCHMARKER._serialized_end = 5761 - _STARTCHILDWORKFLOWEXECUTION._serialized_start = 5764 - _STARTCHILDWORKFLOWEXECUTION._serialized_end = 6938 + _CANCELWORKFLOWEXECUTION._serialized_end = 5756 + _SETPATCHMARKER._serialized_start = 5758 + _SETPATCHMARKER._serialized_end = 5812 + _STARTCHILDWORKFLOWEXECUTION._serialized_start = 5815 + _STARTCHILDWORKFLOWEXECUTION._serialized_end = 6989 _STARTCHILDWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 3468 _STARTCHILDWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 3547 _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_start = 5521 _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_end = 5597 - _CANCELCHILDWORKFLOWEXECUTION._serialized_start = 6940 - _CANCELCHILDWORKFLOWEXECUTION._serialized_end = 7014 - _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_start = 7017 - _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_end = 7159 - _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_start = 7162 - _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_end = 7561 + _CANCELCHILDWORKFLOWEXECUTION._serialized_start = 6991 + _CANCELCHILDWORKFLOWEXECUTION._serialized_end = 7065 + _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_start = 7068 + _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_end = 7210 + _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_start = 7213 + _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_end = 7612 _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 3468 _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 3547 - _CANCELSIGNALWORKFLOW._serialized_start = 7563 - _CANCELSIGNALWORKFLOW._serialized_end = 7598 - _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_start = 7600 - _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_end = 7701 - _MODIFYWORKFLOWPROPERTIES._serialized_start = 7703 - _MODIFYWORKFLOWPROPERTIES._serialized_end = 7782 - _UPDATERESPONSE._serialized_start = 7785 - _UPDATERESPONSE._serialized_end = 7995 - _SCHEDULENEXUSOPERATION._serialized_start = 7998 - _SCHEDULENEXUSOPERATION._serialized_end = 8536 - _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._serialized_start = 8486 - _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._serialized_end = 8536 - _REQUESTCANCELNEXUSOPERATION._serialized_start = 8538 - _REQUESTCANCELNEXUSOPERATION._serialized_end = 8580 + _CANCELSIGNALWORKFLOW._serialized_start = 7614 + _CANCELSIGNALWORKFLOW._serialized_end = 7649 + _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_start = 7651 + _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_end = 7752 + _MODIFYWORKFLOWPROPERTIES._serialized_start = 7754 + _MODIFYWORKFLOWPROPERTIES._serialized_end = 7833 + _UPDATERESPONSE._serialized_start = 7836 + _UPDATERESPONSE._serialized_end = 8046 + _SCHEDULENEXUSOPERATION._serialized_start = 8049 + _SCHEDULENEXUSOPERATION._serialized_end = 8587 + _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._serialized_start = 8537 + _SCHEDULENEXUSOPERATION_NEXUSHEADERENTRY._serialized_end = 8587 + _REQUESTCANCELNEXUSOPERATION._serialized_start = 8589 + _REQUESTCANCELNEXUSOPERATION._serialized_end = 8631 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi index 31d461053..510643132 100644 --- a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi +++ b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi @@ -1102,8 +1102,19 @@ class CancelWorkflowExecution(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor + DETAILS_FIELD_NUMBER: builtins.int + @property + def details(self) -> temporalio.api.common.v1.message_pb2.Payloads: ... def __init__( self, + *, + details: temporalio.api.common.v1.message_pb2.Payloads | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["details", b"details"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["details", b"details"] ) -> None: ... global___CancelWorkflowExecution = CancelWorkflowExecution diff --git a/temporalio/bridge/sdk-core b/temporalio/bridge/sdk-core index 00677170a..5d63f1084 160000 --- a/temporalio/bridge/sdk-core +++ b/temporalio/bridge/sdk-core @@ -1 +1 @@ -Subproject commit 00677170aa7dc62d90bf3d6d3d9f97717a49ea77 +Subproject commit 5d63f108423c4a2a08fb1072751014ad69eea178 diff --git a/temporalio/worker/_interceptor.py b/temporalio/worker/_interceptor.py index 4acf3c5d1..235bdb7cb 100644 --- a/temporalio/worker/_interceptor.py +++ b/temporalio/worker/_interceptor.py @@ -174,6 +174,7 @@ class ContinueAsNewInput: headers: Mapping[str, temporalio.api.common.v1.Payload] versioning_intent: VersioningIntent | None initial_versioning_behavior: ContinueAsNewVersioningBehavior | None + event_groups: Sequence[temporalio.workflow.EventGroup] | None # The types may be absent arg_types: list[type] | None @@ -260,6 +261,7 @@ class StartActivityInput: disable_eager_execution: bool versioning_intent: VersioningIntent | None summary: str | None + event_groups: Sequence[temporalio.workflow.EventGroup] | None priority: temporalio.common.Priority # The types may be absent arg_types: list[type] | None @@ -290,6 +292,7 @@ class StartChildWorkflowInput: versioning_intent: VersioningIntent | None static_summary: str | None static_details: str | None + event_groups: Sequence[temporalio.workflow.EventGroup] | None priority: temporalio.common.Priority # The types may be absent arg_types: list[type] | None @@ -310,6 +313,7 @@ class StartNexusOperationInput(Generic[InputT, OutputT]): cancellation_type: temporalio.workflow.NexusOperationCancellationType headers: Mapping[str, str] | None summary: str | None + event_groups: Sequence[temporalio.workflow.EventGroup] | None = None output_type: type[OutputT] | None = None def __post_init__(self) -> None: @@ -363,6 +367,7 @@ class StartLocalActivityInput: cancellation_type: temporalio.workflow.ActivityCancellationType headers: Mapping[str, temporalio.api.common.v1.Payload] summary: str | None + event_groups: Sequence[temporalio.workflow.EventGroup] | None # The types may be absent arg_types: list[type] | None diff --git a/temporalio/worker/_workflow.py b/temporalio/worker/_workflow.py index 1b217b4a5..f50955c4c 100644 --- a/temporalio/worker/_workflow.py +++ b/temporalio/worker/_workflow.py @@ -694,6 +694,7 @@ def _create_workflow_instance( first_execution_run_id=init.first_execution_run_id, headers=dict(init.headers), namespace=self._namespace, + original_execution_run_id=init.original_execution_run_id or act.run_id, parent=parent, root=root, raw_memo=dict(init.memo.fields), diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index eec5f903c..8bcb846cd 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -718,7 +718,8 @@ async def run_update() -> None: command = None # type: ignore # Run the handler - success = await self._inbound.handle_update_handler(handler_input) + with temporalio.workflow._inbound_update_event_group(job.id).scope(): + success = await self._inbound.handle_update_handler(handler_input) result_payloads = self._workflow_context_payload_converter.to_payloads( [success] ) @@ -1132,7 +1133,7 @@ def _apply_signal_workflow( self._process_signal_job(signal_defn, job) def _apply_initialize_workflow( - self, _job: temporalio.bridge.proto.workflow_activation.InitializeWorkflow + self, job: temporalio.bridge.proto.workflow_activation.InitializeWorkflow ) -> None: # Async call to run on the scheduler thread. This will be wrapped in # another function which applies exception handling. @@ -1229,6 +1230,7 @@ def workflow_continue_as_new( versioning_intent: temporalio.workflow.VersioningIntent | None, initial_versioning_behavior: temporalio.workflow.ContinueAsNewVersioningBehavior | None, + event_groups: Sequence[temporalio.workflow.EventGroup] | None = None, ) -> NoReturn: self._assert_not_read_only("continue as new") # Use definition if callable @@ -1258,6 +1260,7 @@ def workflow_continue_as_new( arg_types=arg_types, versioning_intent=versioning_intent, initial_versioning_behavior=initial_versioning_behavior, + event_groups=event_groups, ) ) @@ -1383,6 +1386,7 @@ def workflow_upsert_memo(self, updates: Mapping[str, Any]) -> None: return command = self._add_command() + command.event_group_markers.extend(self._event_group_markers(None)) fields = command.modify_workflow_properties.upserted_memo.fields # Updating memo inside info by downcasting to mutable mapping. @@ -1451,6 +1455,7 @@ def workflow_patch(self, id: str, *, deprecated: bool) -> bool: self._patches_memoized[id] = use_patch if use_patch: command = self._add_command() + command.event_group_markers.extend(self._event_group_markers(None)) command.set_patch_marker.patch_id = id command.set_patch_marker.deprecated = deprecated return use_patch @@ -1549,6 +1554,7 @@ def workflow_start_activity( activity_id: str | None, versioning_intent: temporalio.workflow.VersioningIntent | None, summary: str | None = None, + event_groups: Sequence[temporalio.workflow.EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> temporalio.workflow.ActivityHandle[Any]: self._assert_not_read_only("start activity") @@ -1586,6 +1592,7 @@ def workflow_start_activity( ret_type=ret_type, versioning_intent=versioning_intent, summary=summary, + event_groups=event_groups, priority=priority, ) ) @@ -1614,6 +1621,7 @@ async def workflow_start_child_workflow( versioning_intent: temporalio.workflow.VersioningIntent | None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[temporalio.workflow.EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> temporalio.workflow.ChildWorkflowHandle[Any, Any]: # Use definition if callable @@ -1654,6 +1662,7 @@ async def workflow_start_child_workflow( versioning_intent=versioning_intent, static_summary=static_summary, static_details=static_details, + event_groups=event_groups, priority=priority, ) ) @@ -1671,6 +1680,7 @@ def workflow_start_local_activity( cancellation_type: temporalio.workflow.ActivityCancellationType, activity_id: str | None, summary: str | None, + event_groups: Sequence[temporalio.workflow.EventGroup] | None = None, ) -> temporalio.workflow.ActivityHandle[Any]: # Get activity definition if it's callable name: str @@ -1704,6 +1714,7 @@ def workflow_start_local_activity( local_retry_threshold=local_retry_threshold, cancellation_type=cancellation_type, summary=summary, + event_groups=event_groups, headers={}, arg_types=arg_types, ret_type=ret_type, @@ -1723,6 +1734,7 @@ async def workflow_start_nexus_operation( cancellation_type: temporalio.workflow.NexusOperationCancellationType, headers: Mapping[str, str] | None, summary: str | None, + event_groups: Sequence[temporalio.workflow.EventGroup] | None = None, ) -> temporalio.workflow.NexusOperationHandle[OutputT]: # start_nexus_operation return await self._outbound.start_nexus_operation( @@ -1738,6 +1750,7 @@ async def workflow_start_nexus_operation( cancellation_type=cancellation_type, headers=headers, summary=summary, + event_groups=event_groups, ) ) @@ -1751,7 +1764,9 @@ def workflow_upsert_search_attributes( | Sequence[temporalio.common.SearchAttributeUpdate] ), ) -> None: - v = self._add_command().upsert_workflow_search_attributes + command = self._add_command() + command.event_group_markers.extend(self._event_group_markers(None)) + v = command.upsert_workflow_search_attributes # Update the attrs on info, casting to their mutable forms first mut_attrs = cast( @@ -1851,7 +1866,11 @@ def workflow_upsert_search_attributes( ) async def workflow_sleep( - self, duration: float, *, summary: str | None = None + self, + duration: float, + *, + summary: str | None = None, + event_groups: Sequence[temporalio.workflow.EventGroup] | None = None, ) -> None: user_metadata = ( temporalio.api.sdk.v1.UserMetadata( @@ -1863,7 +1882,7 @@ async def workflow_sleep( fut = self.create_future() timer_handle = self._timer_impl( duration, - _TimerOptions(user_metadata=user_metadata), + _TimerOptions(user_metadata=user_metadata, event_groups=event_groups), lambda: fut.set_result(None) if not fut.done() else None, ) fut.add_done_callback( @@ -1877,6 +1896,7 @@ async def workflow_wait_condition( *, timeout: float | None = None, timeout_summary: str | None = None, + event_groups: Sequence[temporalio.workflow.EventGroup] | None = None, ) -> None: self._assert_not_read_only("wait condition") cancellation_requested_before = self._cancel_reason is not None @@ -1906,7 +1926,9 @@ def cancellation_arrived() -> bool: ctxvars = contextvars.copy_context() async def in_context(): - _TimerOptionsCtxVar.set(_TimerOptions(user_metadata=user_metadata)) + _TimerOptionsCtxVar.set( + _TimerOptions(user_metadata=user_metadata, event_groups=event_groups) + ) await asyncio.wait_for(fut, timeout) try: @@ -2011,10 +2033,12 @@ async def run_activity() -> Any: completed_cancellation_flag=_WorkflowLogicFlag.RAISE_ON_CANCELLING_COMPLETED_ACTIVITY, ) except _ActivityDoBackoffError as err: - # We have to sleep then reschedule. Note this sleep can be - # cancelled like any other timer. - await asyncio.sleep( - err.backoff.backoff_duration.ToTimedelta().total_seconds() + # Use workflow_sleep rather than asyncio.sleep so directly + # attached Event Groups on the local activity are copied onto + # the backoff timer, matching the command being retried. + await self.workflow_sleep( + err.backoff.backoff_duration.ToTimedelta().total_seconds(), + event_groups=input.event_groups, ) handle._apply_schedule_command(err.backoff) # We have to put the handle back on the pending activity @@ -2038,6 +2062,7 @@ async def _outbound_signal_child_workflow( ) payloads = payload_converter.to_payloads(input.args) if input.args else None command = self._add_command() + command.event_group_markers.extend(self._event_group_markers(None)) v = command.signal_external_workflow_execution v.child_workflow_id = input.child_workflow_id v.signal_name = input.signal @@ -2058,6 +2083,7 @@ async def _outbound_signal_external_workflow( ) payloads = payload_converter.to_payloads(input.args) if input.args else None command = self._add_command() + command.event_group_markers.extend(self._event_group_markers(None)) v = command.signal_external_workflow_execution v.workflow_execution.namespace = input.namespace v.workflow_execution.workflow_id = input.workflow_id @@ -2178,6 +2204,16 @@ def _add_command(self) -> temporalio.bridge.proto.workflow_commands.WorkflowComm self._assert_not_read_only("add command") return self._current_completion.successful.commands.add() + def _event_group_markers( + self, event_groups: Sequence[temporalio.workflow.EventGroup] | None + ) -> list[temporalio.api.sdk.v1.EventGroupMarker]: + """Snapshot the Event Groups for a command being requested. + + Must be called while still in the requesting code's context, which may + differ from the one the command is built in. + """ + return temporalio.workflow._event_group_markers_to_proto(event_groups) + def _workflow_logic_flag_enabled(self, flag: _WorkflowLogicFlag) -> bool: if flag in self._current_internal_flags: return True @@ -2617,8 +2653,12 @@ def _process_signal_job( def done_callback(_f: Any): self._in_progress_signals.pop(id, None) + async def run_signal() -> None: + with _implicit_event_group_scope(job.originating_event_id): + await self._inbound.handle_signal(input) + task = self.create_task( - self._run_top_level_workflow_function(self._inbound.handle_signal(input)), + self._run_top_level_workflow_function(run_signal()), name=f"signal: {job.signal_name}", ) task.add_done_callback(done_callback) @@ -2890,7 +2930,14 @@ def _timer_impl( # Create, schedule, and return seq = self._next_seq("timer") handle = _TimerHandle( - seq, self.time() + delay, options, callback, args, self, context + seq, + self.time() + delay, + options, + callback, + args, + self, + context, + self._event_group_markers(options.event_groups if options else None), ) handle._apply_start_command(self._add_command(), delay) self._pending_timers[seq] = handle @@ -3163,9 +3210,24 @@ def start_local_activity( return self._instance._outbound_schedule_activity(input) +@contextmanager +def _implicit_event_group_scope(originating_event_id: int) -> Iterator[None]: + """Enter the scope of the implicit Event Group of an inbound signal. + + No group is created if the activation does not identify the originating + event, which happens with servers predating Event Groups. + """ + if not originating_event_id: + yield + return + with temporalio.workflow._inbound_event_group(originating_event_id).scope(): + yield + + @dataclass(frozen=True) class _TimerOptions: user_metadata: temporalio.api.sdk.v1.UserMetadata | None = None + event_groups: Sequence[temporalio.workflow.EventGroup] | None = None _TimerOptionsCtxVar: contextvars.ContextVar[_TimerOptions] = contextvars.ContextVar( @@ -3183,10 +3245,12 @@ def __init__( args: Sequence[Any], loop: asyncio.AbstractEventLoop, context: contextvars.Context | None, + event_group_markers: Sequence[temporalio.api.sdk.v1.EventGroupMarker] = (), ) -> None: super().__init__(when, callback, args, loop, context) self._seq = seq self._options = options + self._event_group_markers = event_group_markers def _apply_start_command( self, @@ -3196,6 +3260,7 @@ def _apply_start_command( command.start_timer.seq = self._seq if self._options and self._options.user_metadata: command.user_metadata.CopyFrom(self._options.user_metadata) + command.event_group_markers.extend(self._event_group_markers) command.start_timer.start_to_fire_timeout.FromNanoseconds(int(delay * 1e9)) def _apply_cancel_command( @@ -3203,6 +3268,7 @@ def _apply_cancel_command( command: temporalio.bridge.proto.workflow_commands.WorkflowCommand, ) -> None: command.cancel_timer.seq = self._seq + command.event_group_markers.extend(self._event_group_markers) class _ActivityDoBackoffError(BaseException): @@ -3242,6 +3308,7 @@ def __init__( is_local=isinstance(self._input, StartLocalActivityInput), ) ) + self._event_group_markers = instance._event_group_markers(input.event_groups) def cancel(self, msg: Any | None = None) -> bool: # Allow the cancel to go through for the task even if we're deleting, @@ -3300,6 +3367,7 @@ def _apply_schedule_command( if isinstance(self._input, StartLocalActivityInput) else command.schedule_activity ) + command.event_group_markers.extend(self._event_group_markers) v.seq = self._seq v.activity_id = self._input.activity_id or str(self._seq) v.activity_type = self._input.activity @@ -3395,6 +3463,7 @@ def __init__( self._failure_converter = self._instance._failure_converter_with_context( workflow_context ) + self._event_group_markers = instance._event_group_markers(input.event_groups) @property def id(self) -> str: @@ -3451,6 +3520,7 @@ def _apply_start_command(self) -> None: ) command = self._instance._add_command() + command.event_group_markers.extend(self._event_group_markers) v = command.start_child_workflow_execution v.seq = self._seq v.namespace = self._instance._info.namespace @@ -3558,6 +3628,7 @@ async def signal( async def cancel(self, *, reason: str = "") -> None: self._instance._assert_not_read_only("cancel external handle") command = self._instance._add_command() + command.event_group_markers.extend(self._instance._event_group_markers(None)) v = command.request_cancel_external_workflow_execution v.workflow_execution.namespace = self._instance._info.namespace v.workflow_execution.workflow_id = self._id @@ -3584,6 +3655,7 @@ def __init__( self._result_fut: asyncio.Future[OutputT | None] = instance.create_future() self._payload_converter = payload_converter self._failure_converter = self._instance._context_free_failure_converter + self._event_group_markers = instance._event_group_markers(input.event_groups) @property def operation_token(self) -> str | None: @@ -3618,6 +3690,7 @@ def _resolve_failure(self, err: BaseException) -> None: def _apply_schedule_command(self) -> None: payload = self._payload_converter.to_payload(self._input.input) command = self._instance._add_command() + command.event_group_markers.extend(self._event_group_markers) v = command.schedule_nexus_operation v.seq = self._seq v.endpoint = self._input.endpoint @@ -3662,6 +3735,7 @@ def __init__( super().__init__("Continue as new") self._instance = instance self._input = input + self._event_group_markers = instance._event_group_markers(input.event_groups) def _apply_command(self) -> None: # Convert arguments before creating command in case it raises error @@ -3684,6 +3758,7 @@ def _apply_command(self) -> None: ) command = self._instance._add_command() + command.event_group_markers.extend(self._event_group_markers) v = command.continue_as_new_workflow_execution v.SetInParent() if self._input.workflow: diff --git a/temporalio/worker/workflow_sandbox/_runner.py b/temporalio/worker/workflow_sandbox/_runner.py index 7f06bfcd6..89ce7b951 100644 --- a/temporalio/worker/workflow_sandbox/_runner.py +++ b/temporalio/worker/workflow_sandbox/_runner.py @@ -41,6 +41,7 @@ first_execution_run_id="sandbox-validate-first-run_id", headers={}, namespace="sandbox-validate-namespace", + original_execution_run_id="sandbox-validate-original-execution-run_id", parent=None, root=None, raw_memo={}, diff --git a/temporalio/workflow/__init__.py b/temporalio/workflow/__init__.py index fa2681139..e26d5af05 100644 --- a/temporalio/workflow/__init__.py +++ b/temporalio/workflow/__init__.py @@ -107,6 +107,13 @@ init, run, ) +from ._event_groups import ( + EventGroup, + _event_group_markers_to_proto, + _inbound_event_group, + _inbound_update_event_group, + create_event_group, +) from ._exceptions import ( ContinueAsNewVersioningBehavior, NondeterminismError, @@ -228,6 +235,8 @@ "uuid4", "uuid7", "wait_condition", + "EventGroup", + "create_event_group", "DynamicWorkflowConfig", "defn", "dynamic_config", @@ -281,6 +290,9 @@ "_release_waiter", "_wait", "_current_update_info", + "_event_group_markers_to_proto", + "_inbound_event_group", + "_inbound_update_event_group", "_Runtime", "_set_current_update_info", "_Definition", diff --git a/temporalio/workflow/_activities.py b/temporalio/workflow/_activities.py index ef883c016..36260e45e 100644 --- a/temporalio/workflow/_activities.py +++ b/temporalio/workflow/_activities.py @@ -25,6 +25,7 @@ SelfType, ) from ._context import _Runtime +from ._event_groups import EventGroup from ._exceptions import VersioningIntent __all__ = [ @@ -97,6 +98,7 @@ class ActivityConfig(TypedDict, total=False): activity_id: str | None versioning_intent: VersioningIntent | None summary: str | None + event_groups: Sequence[EventGroup] | None priority: temporalio.common.Priority @@ -115,6 +117,7 @@ def start_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -134,6 +137,7 @@ def start_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -154,6 +158,7 @@ def start_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -174,6 +179,7 @@ def start_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -194,6 +200,7 @@ def start_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -214,6 +221,7 @@ def start_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -236,6 +244,7 @@ def start_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[Any]: ... @@ -256,6 +265,7 @@ def start_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[Any]: """Start an activity and return its handle. @@ -293,6 +303,9 @@ def start_activity( Deprecated: Use Worker Deployment versioning instead. summary: A single-line fixed summary for this activity that may appear in UI/CLI. This can be in single-line Temporal markdown format. + event_groups: Event Groups to associate this command with, in + addition to those active in the current scope. See + :py:func:`temporalio.workflow.create_event_group`. priority: Priority of the activity. Returns: @@ -312,6 +325,7 @@ def start_activity( activity_id=activity_id, versioning_intent=versioning_intent, summary=summary, + event_groups=event_groups, priority=priority, ) @@ -331,6 +345,7 @@ async def execute_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -350,6 +365,7 @@ async def execute_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -370,6 +386,7 @@ async def execute_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -390,6 +407,7 @@ async def execute_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -410,6 +428,7 @@ async def execute_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -430,6 +449,7 @@ async def execute_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -452,6 +472,7 @@ async def execute_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> Any: ... @@ -472,6 +493,7 @@ async def execute_activity( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> Any: """Start an activity and wait for completion. @@ -494,6 +516,7 @@ async def execute_activity( activity_id=activity_id, versioning_intent=versioning_intent, summary=summary, + event_groups=event_groups, priority=priority, ) @@ -513,6 +536,7 @@ def start_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -532,6 +556,7 @@ def start_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -552,6 +577,7 @@ def start_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -572,6 +598,7 @@ def start_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -592,6 +619,7 @@ def start_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -612,6 +640,7 @@ def start_activity_class( # type: ignore[reportOverlappingOverload] activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -631,6 +660,7 @@ def start_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[Any]: """Start an activity from a callable class. @@ -651,6 +681,7 @@ def start_activity_class( activity_id=activity_id, versioning_intent=versioning_intent, summary=summary, + event_groups=event_groups, priority=priority, ) @@ -670,6 +701,7 @@ async def execute_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -689,6 +721,7 @@ async def execute_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -709,6 +742,7 @@ async def execute_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -729,6 +763,7 @@ async def execute_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -749,6 +784,7 @@ async def execute_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -769,6 +805,7 @@ async def execute_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -788,6 +825,7 @@ async def execute_activity_class( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> Any: """Start an activity from a callable class and wait for completion. @@ -808,6 +846,7 @@ async def execute_activity_class( activity_id=activity_id, versioning_intent=versioning_intent, summary=summary, + event_groups=event_groups, priority=priority, ) @@ -827,6 +866,7 @@ def start_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -846,6 +886,7 @@ def start_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -866,6 +907,7 @@ def start_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -886,6 +928,7 @@ def start_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -906,6 +949,7 @@ def start_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -926,6 +970,7 @@ def start_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[ReturnType]: ... @@ -945,6 +990,7 @@ def start_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[Any]: """Start an activity from a method. @@ -965,6 +1011,7 @@ def start_activity_method( activity_id=activity_id, versioning_intent=versioning_intent, summary=summary, + event_groups=event_groups, priority=priority, ) @@ -984,6 +1031,7 @@ async def execute_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -1003,6 +1051,7 @@ async def execute_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -1023,6 +1072,7 @@ async def execute_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -1043,6 +1093,7 @@ async def execute_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -1063,6 +1114,7 @@ async def execute_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -1083,6 +1135,7 @@ async def execute_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -1102,6 +1155,7 @@ async def execute_activity_method( activity_id: str | None = None, versioning_intent: VersioningIntent | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> Any: """Start an activity from a method and wait for completion. @@ -1124,6 +1178,7 @@ async def execute_activity_method( activity_id=activity_id, versioning_intent=versioning_intent, summary=summary, + event_groups=event_groups, priority=priority, ) @@ -1141,6 +1196,7 @@ class LocalActivityConfig(TypedDict, total=False): cancellation_type: ActivityCancellationType activity_id: str | None summary: str | None + event_groups: Sequence[EventGroup] | None # Overload for async no-param activity @@ -1156,6 +1212,7 @@ def start_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1172,6 +1229,7 @@ def start_local_activity( local_retry_threshold: timedelta | None = None, cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1189,6 +1247,7 @@ def start_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1206,6 +1265,7 @@ def start_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1223,6 +1283,7 @@ def start_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1240,6 +1301,7 @@ def start_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1259,6 +1321,7 @@ def start_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[Any]: ... @@ -1276,6 +1339,7 @@ def start_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[Any]: """Start a local activity and return its handle. @@ -1304,6 +1368,9 @@ def start_local_activity( advanced setting that should not be set unless users are sure they need to. Contact Temporal before setting this value. summary: Optional summary for the activity. + event_groups: Event Groups to associate this command with, in + addition to those active in the current scope. See + :py:func:`temporalio.workflow.create_event_group`. Returns: An activity handle to the activity which is an async task. @@ -1320,6 +1387,7 @@ def start_local_activity( cancellation_type=cancellation_type, activity_id=activity_id, summary=summary, + event_groups=event_groups, ) @@ -1336,6 +1404,7 @@ async def execute_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1352,6 +1421,7 @@ async def execute_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1369,6 +1439,7 @@ async def execute_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1386,6 +1457,7 @@ async def execute_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1403,6 +1475,7 @@ async def execute_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1420,6 +1493,7 @@ async def execute_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1439,6 +1513,7 @@ async def execute_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> Any: ... @@ -1456,6 +1531,7 @@ async def execute_local_activity( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> Any: """Start a local activity and wait for completion. @@ -1475,6 +1551,7 @@ async def execute_local_activity( cancellation_type=cancellation_type, activity_id=activity_id, summary=summary, + event_groups=event_groups, ) @@ -1585,6 +1662,7 @@ def start_local_activity_class( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[Any]: """Start a local activity from a callable class. @@ -1602,6 +1680,7 @@ def start_local_activity_class( cancellation_type=cancellation_type, activity_id=activity_id, summary=summary, + event_groups=event_groups, ) @@ -1618,6 +1697,7 @@ async def execute_local_activity_class( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1634,6 +1714,7 @@ async def execute_local_activity_class( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1651,6 +1732,7 @@ async def execute_local_activity_class( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1668,6 +1750,7 @@ async def execute_local_activity_class( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1685,6 +1768,7 @@ async def execute_local_activity_class( # type: ignore[reportOverlappingOverloa cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1702,6 +1786,7 @@ async def execute_local_activity_class( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1718,6 +1803,7 @@ async def execute_local_activity_class( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> Any: """Start a local activity from a callable class and wait for completion. @@ -1737,6 +1823,7 @@ async def execute_local_activity_class( cancellation_type=cancellation_type, activity_id=activity_id, summary=summary, + event_groups=event_groups, ) @@ -1753,6 +1840,7 @@ def start_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1769,6 +1857,7 @@ def start_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1786,6 +1875,7 @@ def start_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1803,6 +1893,7 @@ def start_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1820,6 +1911,7 @@ def start_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1837,6 +1929,7 @@ def start_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[ReturnType]: ... @@ -1853,6 +1946,7 @@ def start_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[Any]: """Start a local activity from a method. @@ -1870,6 +1964,7 @@ def start_local_activity_method( cancellation_type=cancellation_type, activity_id=activity_id, summary=summary, + event_groups=event_groups, ) @@ -1886,6 +1981,7 @@ async def execute_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1902,6 +1998,7 @@ async def execute_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1919,6 +2016,7 @@ async def execute_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1936,6 +2034,7 @@ async def execute_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1953,6 +2052,7 @@ async def execute_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1970,6 +2070,7 @@ async def execute_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> ReturnType: ... @@ -1986,6 +2087,7 @@ async def execute_local_activity_method( cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, activity_id: str | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> Any: """Start a local activity from a method and wait for completion. @@ -2005,4 +2107,5 @@ async def execute_local_activity_method( cancellation_type=cancellation_type, activity_id=activity_id, summary=summary, + event_groups=event_groups, ) diff --git a/temporalio/workflow/_context.py b/temporalio/workflow/_context.py index b33f83150..05c17cd30 100644 --- a/temporalio/workflow/_context.py +++ b/temporalio/workflow/_context.py @@ -22,6 +22,7 @@ if TYPE_CHECKING: from ._activities import ActivityCancellationType, ActivityHandle + from ._event_groups import EventGroup from ._exceptions import ContinueAsNewVersioningBehavior, VersioningIntent from ._nexus import NexusOperationCancellationType, NexusOperationHandle from ._workflow_ops import ( @@ -90,6 +91,13 @@ class Info: first_execution_run_id: str headers: Mapping[str, temporalio.api.common.v1.Payload] namespace: str + + original_execution_run_id: str + """Run ID recorded on the ``WorkflowExecutionStarted`` event. + + Unlike :py:attr:`run_id`, this value is preserved across workflow resets. + """ + parent: ParentInfo | None root: RootInfo | None priority: temporalio.common.Priority @@ -296,6 +304,7 @@ def workflow_continue_as_new( ), versioning_intent: VersioningIntent | None, initial_versioning_behavior: ContinueAsNewVersioningBehavior | None, + event_groups: Sequence[EventGroup] | None = None, ) -> NoReturn: ... @abstractmethod @@ -413,6 +422,7 @@ def workflow_start_activity( activity_id: str | None, versioning_intent: VersioningIntent | None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ActivityHandle[Any]: ... @@ -440,6 +450,7 @@ async def workflow_start_child_workflow( versioning_intent: VersioningIntent | None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ChildWorkflowHandle[Any, Any]: ... @@ -457,6 +468,7 @@ def workflow_start_local_activity( cancellation_type: ActivityCancellationType, activity_id: str | None, summary: str | None, + event_groups: Sequence[EventGroup] | None = None, ) -> ActivityHandle[Any]: ... @abstractmethod @@ -473,6 +485,7 @@ async def workflow_start_nexus_operation( cancellation_type: NexusOperationCancellationType, headers: Mapping[str, str] | None, summary: str | None, + event_groups: Sequence[EventGroup] | None = None, ) -> NexusOperationHandle[OutputT]: ... @abstractmethod @@ -489,7 +502,11 @@ def workflow_upsert_search_attributes( @abstractmethod async def workflow_sleep( - self, duration: float, *, summary: str | None = None + self, + duration: float, + *, + summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> None: ... @abstractmethod @@ -499,6 +516,7 @@ async def workflow_wait_condition( *, timeout: float | None = None, timeout_summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> None: ... @abstractmethod @@ -929,19 +947,28 @@ def uuid7() -> uuid.UUID: ) -async def sleep(duration: float | timedelta, *, summary: str | None = None) -> None: +async def sleep( + duration: float | timedelta, + *, + summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, +) -> None: """Sleep for the given duration. Args: duration: Duration to sleep in seconds or as a timedelta. summary: A single-line fixed summary for this timer that may appear in UI/CLI. This can be in single-line Temporal markdown format. + event_groups: Event Groups to associate this command with, in addition + to those active in the current scope. See + :py:func:`temporalio.workflow.create_event_group`. """ await _Runtime.current().workflow_sleep( duration=( duration.total_seconds() if isinstance(duration, timedelta) else duration ), summary=summary, + event_groups=event_groups, ) @@ -950,6 +977,7 @@ async def wait_condition( *, timeout: timedelta | float | None = None, timeout_summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> None: """Wait on a callback to become true. @@ -968,9 +996,14 @@ async def wait_condition( timeout_summary: Optional simple string identifying the timer (created if ``timeout`` is present) that may be visible in UI/CLI. While it can be normal text, it is best to treat as a timer ID. + event_groups: Event Groups to associate the timer command (created if + ``timeout`` is present) with, in addition to those active in the + current scope. See + :py:func:`temporalio.workflow.create_event_group`. """ await _Runtime.current().workflow_wait_condition( fn, timeout=timeout.total_seconds() if isinstance(timeout, timedelta) else timeout, timeout_summary=timeout_summary, + event_groups=event_groups, ) diff --git a/temporalio/workflow/_event_groups.py b/temporalio/workflow/_event_groups.py new file mode 100644 index 000000000..cd24f91c4 --- /dev/null +++ b/temporalio/workflow/_event_groups.py @@ -0,0 +1,242 @@ +"""Event Groups, a way to regroup logically related workflow events. + +.. warning:: + Event Groups is an experimental API and may change without notice. +""" + +from __future__ import annotations + +import contextvars +import hashlib +from abc import ABC, abstractmethod +from collections.abc import Iterator, Sequence +from contextlib import contextmanager +from dataclasses import dataclass + +import temporalio.api.sdk.v1 +import temporalio.converter + +from ._context import _Runtime + +__all__ = [ + "EventGroup", + "create_event_group", +] + + +class EventGroup(ABC): + """A discrete token associating workflow commands, and the history events + they produce, with a logical group for UI and observability purposes. + + Multiple Event Groups may be attached to a single command, and a single + Event Group may be attached to multiple commands. + + Instances are created with :py:func:`create_event_group`. They may be + attached to specific commands via the ``event_groups`` option of the API + producing the command, or to every command produced within a block of + workflow code via :py:meth:`scope`. + + .. warning:: + Event Groups is an experimental API and may change without notice. + """ + + @contextmanager + def scope(self) -> Iterator[None]: + """Context manager attaching this Event Group to every command produced + within it. + + Scopes nest and compose: a command produced inside an inner scope + carries the Event Groups of all enclosing scopes. Coroutines started + within a scope inherit it, since they capture the context active at + their creation. + + Only usable from within a workflow. + + .. warning:: + Event Groups is an experimental API and may change without notice. + """ + _Runtime.current() + token = _active_event_groups.set(self._applied_over(_active_event_groups.get())) + try: + yield + finally: + try: + _active_event_groups.reset(token) + except ValueError: + # Unwinding from a context other than the one the scope was + # entered in, which happens when a coroutine suspended inside + # the scope is closed rather than resumed. The context the + # value was set in is being discarded anyway. + pass + + @abstractmethod + def _applied_over(self, active: _ActiveEventGroups) -> _ActiveEventGroups: + """Return the active set resulting from entering this group's scope.""" + ... + + @abstractmethod + def _to_proto(self) -> temporalio.api.sdk.v1.EventGroupMarker: + """Serialize as the marker attached to a workflow command.""" + ... + + +class _LabelEventGroup(EventGroup): + """An Event Group explicitly created by workflow code.""" + + def __init__(self, id: str, label: str) -> None: + self._id = id + self._label = label + + def _applied_over(self, active: _ActiveEventGroups) -> _ActiveEventGroups: + return _ActiveEventGroups( + implicit=active.implicit, + explicit=_with_group(active.explicit, self), + ) + + def _to_proto(self) -> temporalio.api.sdk.v1.EventGroupMarker: + # Deliberately the SDK's default converter rather than the worker's own: the UI and CLI + # rely on the label being a json/plain string, which a user-provided converter could + # break. + return temporalio.api.sdk.v1.EventGroupMarker( + label=temporalio.api.sdk.v1.EventGroupMarker.Label( + id=self._id, + label=temporalio.converter.PayloadConverter.default.to_payload( + self._label + ), + ) + ) + + +class _ImplicitEventGroup(EventGroup): + """An Event Group created by the SDK around an inbound signal or update. + + The workflow's main function deliberately gets no such group, so commands it + produces outside any explicit scope carry no markers at all. + """ + + def __init__(self, marker: temporalio.api.sdk.v1.EventGroupMarker) -> None: + self._marker = marker + + def _applied_over(self, active: _ActiveEventGroups) -> _ActiveEventGroups: + # Implicit groups intentionally do not inherit the enclosing scope: a + # handler registered inside an explicit scope must not attribute its + # commands to that scope. + return _ActiveEventGroups(implicit=self) + + def _to_proto(self) -> temporalio.api.sdk.v1.EventGroupMarker: + return self._marker + + +@dataclass(frozen=True) +class _ActiveEventGroups: + implicit: EventGroup | None = None + explicit: tuple[_LabelEventGroup, ...] = () + + +_active_event_groups: contextvars.ContextVar[_ActiveEventGroups] = ( + contextvars.ContextVar( + "__temporal_active_event_groups", default=_ActiveEventGroups() + ) +) + + +def create_event_group(label: str, *, id: str | None = None) -> EventGroup: + """Create an Event Group that can be attached to commands produced by this + workflow. + + Args: + label: User-visible label for the group, surfaced in the UI and CLI. + The label is converted to a payload using the SDK's default payload + converter, not the one configured on the worker, then encoded using + the worker's configured payload codecs. + + Note that when no ``id`` is given, the id is derived from the label + using a hash function. Given short and predictable labels, + brute-forcing the hashed value may be computationally feasible, + thereby recovering the label. Avoid putting sensitive information + in labels, or provide an explicit ``id``. + id: Opaque identifier determining whether two Event Groups are the + same. Events are grouped together if and only if their groups have + the same id, without regard to their labels; only the first label + seen for a given id is used. Defaults to a deterministic, + replay-stable value derived from the label. The id is not encoded + using payload codecs. + + Returns: + The new Event Group. + + .. warning:: + Event Groups is an experimental API and may change without notice. + """ + info = _Runtime.current().workflow_info() + if not label: + raise ValueError("Event group label cannot be empty") + if id is None: + # Salted with the run id so that the label cannot be recovered from the + # id using precomputed hashes. This is the run id of the + # WorkflowExecutionStarted event, which is preserved across resets, so + # ids remain stable on replay and after a reset. + id = hashlib.sha1( + f"{info.original_execution_run_id}{label}".encode() + ).hexdigest() + elif not id: + raise ValueError("Event group id cannot be empty") + return _LabelEventGroup(id, label) + + +def _inbound_event_group(event_id: int) -> EventGroup: + """Create the implicit Event Group for an inbound signal's history event.""" + if event_id <= 0: + raise ValueError(f"Invalid inbound event id: {event_id}") + return _ImplicitEventGroup( + temporalio.api.sdk.v1.EventGroupMarker( + inbound_event=temporalio.api.sdk.v1.EventGroupMarker.InboundEvent( + inbound_event_id=event_id + ) + ) + ) + + +def _inbound_update_event_group(update_id: str) -> EventGroup: + """Create the implicit Event Group for an inbound update.""" + return _ImplicitEventGroup( + temporalio.api.sdk.v1.EventGroupMarker( + inbound_update=temporalio.api.sdk.v1.EventGroupMarker.InboundUpdate( + inbound_update_id=update_id + ) + ) + ) + + +def _event_group_markers_to_proto( + event_groups: Sequence[EventGroup] | None, +) -> list[temporalio.api.sdk.v1.EventGroupMarker]: + """Merge the given Event Groups with those active in the current scope and + serialize them as the markers attached to a workflow command. + + Must be called from the context the command was requested in, which is not + necessarily the one it is ultimately built in. + """ + active = _active_event_groups.get() + explicit = active.explicit + for group in event_groups or (): + if not isinstance(group, _LabelEventGroup): + raise TypeError( + "Event groups must be created with workflow.create_event_group()" + ) + explicit = _with_group(explicit, group) + groups: list[EventGroup] = list(explicit) + if active.implicit: + groups.insert(0, active.implicit) + return [group._to_proto() for group in groups] + + +def _with_group( + groups: tuple[_LabelEventGroup, ...], group: _LabelEventGroup +) -> tuple[_LabelEventGroup, ...]: + """Add a group to a set of groups, deduplicating by id.""" + if any(existing._id == group._id for existing in groups): + return tuple( + group if existing._id == group._id else existing for existing in groups + ) + return (*groups, group) diff --git a/temporalio/workflow/_nexus.py b/temporalio/workflow/_nexus.py index 29bd10715..d8ad124cc 100644 --- a/temporalio/workflow/_nexus.py +++ b/temporalio/workflow/_nexus.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Awaitable, Callable, Generator, Mapping +from collections.abc import Awaitable, Callable, Generator, Mapping, Sequence from datetime import timedelta from enum import IntEnum from typing import Any, Generic, overload @@ -15,6 +15,7 @@ from temporalio.types import NexusServiceType from ._context import _Runtime +from ._event_groups import EventGroup __all__ = [ "NexusClient", @@ -112,6 +113,7 @@ async def start_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NexusOperationHandle[OutputT]: ... # Overload for string operation name @@ -129,6 +131,7 @@ async def start_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NexusOperationHandle[OutputT]: ... # Overload for workflow_run_operation methods @@ -149,6 +152,7 @@ async def start_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NexusOperationHandle[OutputT]: ... # Overload for sync_operation methods (async def) @@ -169,6 +173,7 @@ async def start_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NexusOperationHandle[OutputT]: ... # Overload for sync_operation methods (def) @@ -189,6 +194,7 @@ async def start_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NexusOperationHandle[OutputT]: ... # Overload for operation_handler @@ -208,6 +214,7 @@ async def start_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NexusOperationHandle[OutputT]: ... # Overload for temporal_operation methods @@ -233,6 +240,7 @@ async def start_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NexusOperationHandle[OutputT]: ... @abstractmethod @@ -248,6 +256,7 @@ async def start_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> Any: """Start a Nexus operation and return its handle. @@ -283,6 +292,7 @@ async def execute_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> OutputT: ... # Overload for string operation name @@ -300,6 +310,7 @@ async def execute_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> OutputT: ... # Overload for workflow_run_operation methods @@ -320,6 +331,7 @@ async def execute_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> OutputT: ... # Overload for sync_operation methods (async def) @@ -340,6 +352,7 @@ async def execute_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> OutputT: ... # Overload for sync_operation methods (def) @@ -360,6 +373,7 @@ async def execute_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> OutputT: ... # Overload for operation_handler @@ -380,6 +394,7 @@ async def execute_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> OutputT: ... # Overload for temporal_operation methods @@ -405,6 +420,7 @@ async def execute_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> OutputT: ... @abstractmethod @@ -420,6 +436,7 @@ async def execute_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> Any: """Execute a Nexus operation and return its result. @@ -477,6 +494,7 @@ async def start_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> Any: return await _Runtime.current().workflow_start_nexus_operation( endpoint=self.endpoint, @@ -490,6 +508,7 @@ async def start_operation( cancellation_type=cancellation_type, headers=headers, summary=summary, + event_groups=event_groups, ) async def execute_operation( @@ -504,6 +523,7 @@ async def execute_operation( cancellation_type: NexusOperationCancellationType = NexusOperationCancellationType.WAIT_COMPLETED, headers: Mapping[str, str] | None = None, summary: str | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> Any: handle = await self.start_operation( operation, @@ -515,6 +535,7 @@ async def execute_operation( cancellation_type=cancellation_type, headers=headers, summary=summary, + event_groups=event_groups, ) return await handle diff --git a/temporalio/workflow/_workflow_ops.py b/temporalio/workflow/_workflow_ops.py index f80ca1bdb..5e0dfc14b 100644 --- a/temporalio/workflow/_workflow_ops.py +++ b/temporalio/workflow/_workflow_ops.py @@ -20,6 +20,7 @@ ) from ._activities import _AsyncioTask from ._context import _Runtime, uuid4 +from ._event_groups import EventGroup from ._exceptions import ContinueAsNewVersioningBehavior, VersioningIntent __all__ = [ @@ -172,6 +173,7 @@ class ChildWorkflowConfig(TypedDict, total=False): versioning_intent: VersioningIntent | None static_summary: str | None static_details: str | None + event_groups: Sequence[EventGroup] | None priority: temporalio.common.Priority @@ -198,6 +200,7 @@ async def start_child_workflow( versioning_intent: VersioningIntent | None = None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ChildWorkflowHandle[SelfType, ReturnType]: ... @@ -226,6 +229,7 @@ async def start_child_workflow( versioning_intent: VersioningIntent | None = None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ChildWorkflowHandle[SelfType, ReturnType]: ... @@ -254,6 +258,7 @@ async def start_child_workflow( versioning_intent: VersioningIntent | None = None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ChildWorkflowHandle[SelfType, ReturnType]: ... @@ -284,6 +289,7 @@ async def start_child_workflow( versioning_intent: VersioningIntent | None = None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ChildWorkflowHandle[Any, Any]: ... @@ -312,6 +318,7 @@ async def start_child_workflow( versioning_intent: VersioningIntent | None = None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ChildWorkflowHandle[Any, Any]: """Start a child workflow and return its handle. @@ -374,6 +381,7 @@ async def start_child_workflow( versioning_intent=versioning_intent, static_summary=static_summary, static_details=static_details, + event_groups=event_groups, priority=priority, ) @@ -401,6 +409,7 @@ async def execute_child_workflow( versioning_intent: VersioningIntent | None = None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -429,6 +438,7 @@ async def execute_child_workflow( versioning_intent: VersioningIntent | None = None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -457,6 +467,7 @@ async def execute_child_workflow( versioning_intent: VersioningIntent | None = None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> ReturnType: ... @@ -487,6 +498,7 @@ async def execute_child_workflow( versioning_intent: VersioningIntent | None = None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> Any: ... @@ -515,6 +527,7 @@ async def execute_child_workflow( versioning_intent: VersioningIntent | None = None, static_summary: str | None = None, static_details: str | None = None, + event_groups: Sequence[EventGroup] | None = None, priority: temporalio.common.Priority = temporalio.common.Priority.default, ) -> Any: """Start a child workflow and wait for completion. @@ -543,6 +556,7 @@ async def execute_child_workflow( versioning_intent=versioning_intent, static_summary=static_summary, static_details=static_details, + event_groups=event_groups, priority=priority, ) return await handle @@ -692,6 +706,7 @@ def continue_as_new( ) = None, versioning_intent: VersioningIntent | None = None, initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NoReturn: ... @@ -712,6 +727,7 @@ def continue_as_new( ) = None, versioning_intent: VersioningIntent | None = None, initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NoReturn: ... @@ -733,6 +749,7 @@ def continue_as_new( ) = None, versioning_intent: VersioningIntent | None = None, initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NoReturn: ... @@ -754,6 +771,7 @@ def continue_as_new( ) = None, versioning_intent: VersioningIntent | None = None, initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NoReturn: ... @@ -775,6 +793,7 @@ def continue_as_new( ) = None, versioning_intent: VersioningIntent | None = None, initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NoReturn: ... @@ -795,6 +814,7 @@ def continue_as_new( ) = None, versioning_intent: VersioningIntent | None = None, initial_versioning_behavior: ContinueAsNewVersioningBehavior | None = None, + event_groups: Sequence[EventGroup] | None = None, ) -> NoReturn: """Stop the workflow immediately and continue as new. @@ -840,6 +860,7 @@ def continue_as_new( search_attributes=search_attributes, versioning_intent=versioning_intent, initial_versioning_behavior=initial_versioning_behavior, + event_groups=event_groups, ) diff --git a/tests/test_workflow_exports.py b/tests/test_workflow_exports.py index 8788addc5..400ebdc27 100644 --- a/tests/test_workflow_exports.py +++ b/tests/test_workflow_exports.py @@ -21,6 +21,7 @@ "ContinueAsNewError", "ContinueAsNewVersioningBehavior", "DynamicWorkflowConfig", + "EventGroup", "ExternalWorkflowHandle", "HandlerUnfinishedPolicy", "Info", @@ -64,7 +65,10 @@ "_bind_method", "_build_log_context", "_current_update_info", + "_event_group_markers_to_proto", "_imports_passed_through", + "_inbound_event_group", + "_inbound_update_event_group", "_in_sandbox", "_is_unbound_method_on_cls", "_parameters_identical_up_to_naming", @@ -78,6 +82,7 @@ "annotations", "as_completed", "continue_as_new", + "create_event_group", "create_nexus_client", "current_update_info", "defn", diff --git a/tests/worker/test_event_groups.py b/tests/worker/test_event_groups.py new file mode 100644 index 000000000..181a71359 --- /dev/null +++ b/tests/worker/test_event_groups.py @@ -0,0 +1,1932 @@ +"""Event Groups tests, following Workspace/test-plan.md. + +The existing ``test_event_groups.py`` is the pre-plan smoke suite and is left +in place until this file replaces it. Case IDs in comments are the plan's; +TypeScript's suite is a style reference only and may still change. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import uuid +from collections.abc import Sequence +from datetime import timedelta + +import nexusrpc +import pytest + +from temporalio import activity, workflow +from temporalio.api.common.v1 import Payload, WorkflowExecution +from temporalio.api.enums.v1 import EventType +from temporalio.api.history.v1 import HistoryEvent +from temporalio.api.sdk.v1 import EventGroupMarker +from temporalio.api.workflowservice.v1 import ResetWorkflowExecutionRequest +from temporalio.client import Client, WorkflowHandle +from temporalio.common import RawValue, RetryPolicy, SearchAttributeKey +from temporalio.converter import ( + CompositePayloadConverter, + DataConverter, + DefaultPayloadConverter, + EncodingPayloadConverter, + PayloadCodec, + PayloadConverter, +) +from temporalio.exceptions import ( + ActivityError, + ApplicationError, + ChildWorkflowError, + NexusOperationError, + TemporalError, +) +from temporalio.testing import WorkflowEnvironment +from temporalio.worker import Worker +from tests.helpers import assert_eventually, ensure_search_attributes_present, new_worker +from tests.helpers.nexus import make_nexus_endpoint_name + +# These tests need a server that transcribes Event Group markers onto history. +# Time-skipping (the Java test server) does not. + +_ACT_TIMEOUT = timedelta(seconds=10) + + +def _require_event_groups_server(env: WorkflowEnvironment) -> None: + if env.supports_time_skipping: + pytest.skip("Event Groups require a server that transcribes markers") + + +#################################################################################################### +# 1. Explicit Event Groups Marker Label IDs (`EG-LABEL-ID`) +#################################################################################################### + + +@workflow.defn +class DerivedIdsWorkflow: + @workflow.run + async def run(self) -> None: + a = workflow.create_event_group("aaa") + b1 = workflow.create_event_group("bbb") + b2 = workflow.create_event_group("bbb") + await _activity("activity-a", [a]) + await _activity("activity-b1", [b1]) + await _activity("activity-b2", [b2]) + + +async def test_derived_label_ids(client: Client, env: WorkflowEnvironment): + _require_event_groups_server(env) + + async with new_worker( + client, DerivedIdsWorkflow, activities=[noop_activity] + ) as worker: + handle1 = await client.start_workflow( + DerivedIdsWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + handle2 = await client.start_workflow( + DerivedIdsWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle1.result() + await handle2.result() + events1 = await _fetch_events(handle1) + events2 = await _fetch_events(handle2) + run_id1 = _run_id(handle1) + + assert ( + len(_events_of_type(events1, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)) + == 3 + ) + assert ( + len(_events_of_type(events2, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)) + == 3 + ) + + # EG-LABEL-ID-00: Derived IDs match SHA1(original_execution_run_id + label) + _assert_marker_ids( + _activity_event(events1, "activity-a"), + _label_marker_id(_default_marker_id(run_id1, "aaa")), + ) + + # EG-LABEL-ID-01: same label + no user-provided ID => same group + assert _marker_ids(_activity_event(events1, "activity-b1")) == _marker_ids( + _activity_event(events1, "activity-b2") + ) + + # EG-LABEL-ID-02: different labels + no user-provided ID => distinct groups + assert _marker_ids(_activity_event(events1, "activity-a")) != _marker_ids( + _activity_event(events1, "activity-b1") + ) + + # EG-LABEL-ID-03: same labels + different workflow execs => distinct groups + assert _marker_ids(_activity_event(events1, "activity-a")) != _marker_ids( + _activity_event(events2, "activity-a") + ) + + +async def test_derived_label_ids_stable_across_reset( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, DerivedIdsWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + DerivedIdsWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + original_run_id = _run_id(handle) + + first_wft_started = next( + e.event_id + for e in events + if e.event_type == EventType.EVENT_TYPE_WORKFLOW_TASK_STARTED + ) + reset = await client.workflow_service.reset_workflow_execution( + ResetWorkflowExecutionRequest( + namespace=client.namespace, + workflow_execution=WorkflowExecution( + workflow_id=handle.id, run_id=original_run_id + ), + reason="test event group id stability across reset", + request_id=str(uuid.uuid4()), + workflow_task_finish_event_id=first_wft_started, + ) + ) + assert reset.run_id != original_run_id + reset_handle = client.get_workflow_handle(handle.id, run_id=reset.run_id) + await reset_handle.result() + reset_events = await _fetch_events(reset_handle) + + assert ( + len(_events_of_type(events, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)) + == 3 + ) + assert ( + len( + _events_of_type( + reset_events, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED + ) + ) + == 3 + ) + + # Control: reset re-executed the first workflow task + assert ( + _events_of_type(events, EventType.EVENT_TYPE_WORKFLOW_TASK_COMPLETED)[ + 0 + ].event_time + != _events_of_type( + reset_events, EventType.EVENT_TYPE_WORKFLOW_TASK_COMPLETED + )[0].event_time + ) + + # EG-LABEL-ID-04: derived IDs are based on the original execution run id + _assert_marker_ids( + _activity_event(reset_events, "activity-a"), + _label_marker_id(_default_marker_id(original_run_id, "aaa")), + ) + assert _marker_ids(_activity_event(events, "activity-b1")) == _marker_ids( + _activity_event(reset_events, "activity-b1") + ) + + +@workflow.defn +class UserProvidedIdsWorkflow: + @workflow.run + async def run(self) -> None: + c = workflow.create_event_group("ccc", id="c-id") + d1 = workflow.create_event_group("ddd1", id="d-id") + d2 = workflow.create_event_group("ddd2", id="d-id") + await _activity("activity-c", [c]) + await _activity("activity-d1", [d1]) + await _activity("activity-d2", [d2]) + + +async def test_user_provided_label_ids(client: Client, env: WorkflowEnvironment): + _require_event_groups_server(env) + + async with new_worker( + client, UserProvidedIdsWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + UserProvidedIdsWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + assert ( + len(_events_of_type(events, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)) + == 3 + ) + + # EG-LABEL-ID-20: user-provided IDs are used verbatim + _assert_marker_ids( + _activity_event(events, "activity-c"), _label_marker_id("c-id") + ) + + # EG-LABEL-ID-21: different labels + same user-provided ID => same group + assert _marker_ids(_activity_event(events, "activity-d1")) == _marker_ids( + _activity_event(events, "activity-d2") + ) + + +#################################################################################################### +# 2. Explicit Event Groups Marker Label Payload (`EG-LABEL-PAYLOAD`) +#################################################################################################### + + +@workflow.defn +class LabelPayloadWorkflow: + @workflow.run + async def run(self) -> None: + a = workflow.create_event_group("aaa") + b = workflow.create_event_group("bbb", id="b-id") + # Control: activity arguments go through the worker's payload converter, so this is how the + # custom-converter test proves that converter is actually installed. + await workflow.execute_activity( + control_activity, + "control", + start_to_close_timeout=_ACT_TIMEOUT, + activity_id="control", + ) + await _activity("activity-a", [a]) + await _activity("activity-b", [b]) + + +class _CustomStringConverter(EncodingPayloadConverter): + @property + def encoding(self) -> str: + return "custom" + + def to_payload(self, value: object) -> Payload | None: + if isinstance(value, str): + return Payload( + metadata={"encoding": b"custom"}, + data=f"custom-converter-{value}".encode(), + ) + return None + + def from_payload(self, payload: Payload, type_hint: type | None = None) -> str: + text = payload.data.decode() + prefix = "custom-converter-" + return text[len(prefix) :] if text.startswith(prefix) else text + + +class _CustomPayloadConverter(CompositePayloadConverter): + def __init__(self) -> None: + super().__init__( + _CustomStringConverter(), + *DefaultPayloadConverter.default_encoding_payload_converters, + ) + + +class _WrappingPayloadCodec(PayloadCodec): + async def encode(self, payloads: Sequence[Payload]) -> list[Payload]: + return [ + Payload( + metadata={"encoding": b"binary/wrapped"}, data=p.SerializeToString() + ) + for p in payloads + ] + + async def decode(self, payloads: Sequence[Payload]) -> list[Payload]: + decoded: list[Payload] = [] + for payload in payloads: + inner = Payload() + inner.ParseFromString(payload.data) + decoded.append(inner) + return decoded + + +async def test_label_payload_is_json_plain(client: Client, env: WorkflowEnvironment): + _require_event_groups_server(env) + + async with new_worker( + client, + LabelPayloadWorkflow, + activities=[noop_activity, control_activity], + ) as worker: + handle = await client.start_workflow( + LabelPayloadWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + a_id = _default_marker_id(run_id, "aaa") + + activity_a = _activity_event(events, "activity-a") + activity_b = _activity_event(events, "activity-b") + _assert_markers(activity_a, _label_marker(a_id, "aaa")) + _assert_markers(activity_b, _label_marker("b-id", "bbb")) + + # EG-LABEL-PAYLOAD-00: label payload is a json/plain JSON string + assert _label_payload_of(activity_a, a_id) == ("json/plain", '"aaa"') + assert _label_payload_of(activity_b, "b-id") == ("json/plain", '"bbb"') + + +async def test_label_payload_uses_default_converter_not_worker_converter( + env: WorkflowEnvironment, +): + _require_event_groups_server(env) + + custom_client = await env.connect_client( + data_converter=DataConverter(payload_converter_class=_CustomPayloadConverter) + ) + async with new_worker( + custom_client, + LabelPayloadWorkflow, + activities=[noop_activity, control_activity], + ) as worker: + handle = await custom_client.start_workflow( + LabelPayloadWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + a_id = _default_marker_id(run_id, "aaa") + + control = _activity_event(events, "control") + control_payload = ( + control.activity_task_scheduled_event_attributes.input.payloads[0] + ) + assert control_payload.metadata["encoding"] == b"custom" + assert control_payload.data == b"custom-converter-control" + + activity_a = _activity_event(events, "activity-a") + activity_b = _activity_event(events, "activity-b") + + # EG-LABEL-PAYLOAD-01: labels still go through the SDK default converter + assert _label_payload_of(activity_a, a_id) == ("json/plain", '"aaa"') + assert _label_payload_of(activity_b, "b-id") == ("json/plain", '"bbb"') + + +async def test_label_payload_is_codec_encoded_but_ids_are_not( + env: WorkflowEnvironment, +): + _require_event_groups_server(env) + + codec = _WrappingPayloadCodec() + codec_client = await env.connect_client( + data_converter=DataConverter(payload_codec=codec) + ) + async with new_worker( + codec_client, + LabelPayloadWorkflow, + activities=[noop_activity, control_activity], + ) as worker: + handle = await codec_client.start_workflow( + LabelPayloadWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + a_id = _default_marker_id(run_id, "aaa") + + activity_a = _activity_event(events, "activity-a") + activity_b = _activity_event(events, "activity-b") + + # EG-LABEL-PAYLOAD-21: IDs are not codec-encoded + _assert_marker_ids(activity_a, _label_marker_id(a_id)) + _assert_marker_ids(activity_b, _label_marker_id("b-id")) + + # EG-LABEL-PAYLOAD-20: label payloads are processed by payload codecs + assert _label_payload_of(activity_a, a_id)[0] == "binary/wrapped" + assert _label_payload_of(activity_b, "b-id")[0] == "binary/wrapped" + decoded_a = (await codec.decode([_raw_label_payload(activity_a, a_id)]))[0] + decoded_b = (await codec.decode([_raw_label_payload(activity_b, "b-id")]))[0] + assert PayloadConverter.default.from_payload(decoded_a) == "aaa" + assert PayloadConverter.default.from_payload(decoded_b) == "bbb" + + +#################################################################################################### +# 3. Explicit Event Group Scopes (`EG-SCOPE`) +#################################################################################################### + + +@workflow.defn +class ScopeBaselineWorkflow: + @workflow.run + async def run(self) -> None: + a = workflow.create_event_group("aaa") + with a.scope(): + await _activity("activity") + await workflow.sleep(0.001) + await workflow.start_child_workflow( + NoopChildWorkflow.run, + id=f"{workflow.info().workflow_id}_child", + ) + + +async def test_commands_in_a_scope_carry_its_marker( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, + ScopeBaselineWorkflow, + NoopChildWorkflow, + activities=[noop_activity], + ) as worker: + handle = await client.start_workflow( + ScopeBaselineWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + a = _label_marker(_default_marker_id(_run_id(handle), "aaa"), "aaa") + + # EG-SCOPE-00: baseline only; per-command coverage lives in EG-COMMANDS + _assert_markers(_activity_event(events, "activity"), a) + _assert_markers(_single_event(events, EventType.EVENT_TYPE_TIMER_STARTED), a) + _assert_markers( + _single_event( + events, EventType.EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED + ), + a, + ) + + +@workflow.defn +class NestedScopesWorkflow: + @workflow.run + async def run(self) -> None: + a = workflow.create_event_group("aaa") + b = workflow.create_event_group("bbb") + with a.scope(): + await _activity("a-before") + with b.scope(): + await _activity("a-b") + await _activity("a-after") + await _activity("outside") + + +async def test_nesting_scopes_composes(client: Client, env: WorkflowEnvironment): + _require_event_groups_server(env) + + async with new_worker( + client, NestedScopesWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + NestedScopesWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + a = _label_marker(_default_marker_id(run_id, "aaa"), "aaa") + b = _label_marker(_default_marker_id(run_id, "bbb"), "bbb") + + assert ( + len(_events_of_type(events, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)) + == 4 + ) + + # EG-SCOPE-01 + _assert_markers(_activity_event(events, "a-before"), a) + _assert_markers(_activity_event(events, "a-b"), a, b) + _assert_markers(_activity_event(events, "a-after"), a) + _assert_markers(_activity_event(events, "outside")) + + +@workflow.defn +class ReenteredScopeWorkflow: + @workflow.run + async def run(self) -> None: + a = workflow.create_event_group("aaa") + with a.scope(): + await _activity("a-before") + with a.scope(): + await _activity("a-inner") + await _activity("a-after") + await _activity("outside") + + +async def test_reentering_a_group_nests_correctly( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, ReenteredScopeWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + ReenteredScopeWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + a = _label_marker(_default_marker_id(_run_id(handle), "aaa"), "aaa") + + assert ( + len(_events_of_type(events, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)) + == 4 + ) + + # EG-SCOPE-02: inner re-entry still serializes the marker once + _assert_markers(_activity_event(events, "a-before"), a) + _assert_markers(_activity_event(events, "a-inner"), a) + _assert_markers(_activity_event(events, "a-after"), a) + _assert_markers(_activity_event(events, "outside")) + + +@workflow.defn +class ConcurrentScopesWorkflow: + @workflow.run + async def run(self) -> None: + a = workflow.create_event_group("aaa") + b = workflow.create_event_group("bbb") + c = workflow.create_event_group("ccc") + d = workflow.create_event_group("ddd") + e = workflow.create_event_group("eee") + + async def left() -> None: + with b.scope(): + with a.scope(): + with c.scope(): + await _activity("b-a-c") + await _activity("b-a") + await _activity("b-after-a") + + async def right() -> None: + with d.scope(): + with a.scope(): + with e.scope(): + await _activity("d-a-e") + await _activity("d-a") + await _activity("d-after-a") + + await asyncio.gather(left(), right()) + await _activity("outside") + + +async def test_a_group_can_be_scoped_from_two_concurrent_branches( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, ConcurrentScopesWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + ConcurrentScopesWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + a = _label_marker(_default_marker_id(run_id, "aaa"), "aaa") + b = _label_marker(_default_marker_id(run_id, "bbb"), "bbb") + c = _label_marker(_default_marker_id(run_id, "ccc"), "ccc") + d = _label_marker(_default_marker_id(run_id, "ddd"), "ddd") + e = _label_marker(_default_marker_id(run_id, "eee"), "eee") + + assert ( + len(_events_of_type(events, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)) + == 7 + ) + + # EG-SCOPE-03: a mutable "currently active groups" stack would cross-contaminate here + _assert_markers(_activity_event(events, "b-a-c"), b, a, c) + _assert_markers(_activity_event(events, "b-a"), b, a) + _assert_markers(_activity_event(events, "b-after-a"), b) + _assert_markers(_activity_event(events, "d-a-e"), d, a, e) + _assert_markers(_activity_event(events, "d-a"), d, a) + _assert_markers(_activity_event(events, "d-after-a"), d) + _assert_markers(_activity_event(events, "outside")) + + +@workflow.defn +class DetachedTaskScopeWorkflow: + @workflow.run + async def run(self) -> None: + a = workflow.create_event_group("aaa") + released = False + started = False + + async def task() -> None: + nonlocal started + await _activity("inside-before") + started = True + await workflow.wait_condition(lambda: released) + await _activity("inside-after") + + with a.scope(): + running = asyncio.create_task(task()) + await workflow.wait_condition(lambda: started) + released = True + await running + await _activity("outside") + + +async def test_a_task_started_inside_a_scope_keeps_it_after_exit( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, DetachedTaskScopeWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + DetachedTaskScopeWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + a = _label_marker(_default_marker_id(_run_id(handle), "aaa"), "aaa") + + assert ( + len(_events_of_type(events, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)) + == 3 + ) + + # EG-SCOPE-04: membership is captured when the task is started + _assert_markers(_activity_event(events, "inside-before"), a) + _assert_markers(_activity_event(events, "inside-after"), a) + _assert_markers(_activity_event(events, "outside")) + + +@workflow.defn +class OutsiderTaskScopeWorkflow: + @workflow.run + async def run(self) -> None: + a = workflow.create_event_group("aaa") + release = False + + async def outsider() -> None: + await workflow.wait_condition(lambda: release) + await _activity("outside-task") + + running = asyncio.create_task(outsider()) + with a.scope(): + await _activity("in-a") + release = True + await running + + +async def test_a_task_created_outside_a_scope_does_not_inherit_it( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, OutsiderTaskScopeWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + OutsiderTaskScopeWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + a = _label_marker(_default_marker_id(_run_id(handle), "aaa"), "aaa") + + assert ( + len(_events_of_type(events, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)) + == 2 + ) + + # EG-SCOPE-05: membership follows the context the code was started in + _assert_markers(_activity_event(events, "in-a"), a) + _assert_markers(_activity_event(events, "outside-task")) + + +@workflow.defn +class ThrowingScopeWorkflow: + @workflow.run + async def run(self) -> None: + a = workflow.create_event_group("aaa") + b = workflow.create_event_group("bbb") + with a.scope(): + try: + with b.scope(): + await _activity("a-b") + raise RuntimeError("boom") + except RuntimeError: + pass + await _activity("a-after") + + +async def test_a_scope_unwinds_cleanly_when_its_body_throws( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, ThrowingScopeWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + ThrowingScopeWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + a = _label_marker(_default_marker_id(run_id, "aaa"), "aaa") + b = _label_marker(_default_marker_id(run_id, "bbb"), "bbb") + + assert ( + len(_events_of_type(events, EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)) + == 2 + ) + + # EG-SCOPE-06 + _assert_markers(_activity_event(events, "a-b"), a, b) + _assert_markers(_activity_event(events, "a-after"), a) + + +#################################################################################################### +# 4. Implicit Event Groups (`EG-IMPLICIT`) +#################################################################################################### + + +@workflow.defn +class StaticSignalHandlerWorkflow: + def __init__(self) -> None: + self._done = False + + @workflow.run + async def run(self) -> None: + await _activity("from-main-before-signal") + await workflow.wait_condition(lambda: self._done) + await _activity("from-main-after-signal") + + @workflow.signal + async def my_signal(self) -> None: + await _activity("from-static-signal") + a = workflow.create_event_group("aaa") + with a.scope(): + await _activity("from-static-signal-scoped") + self._done = True + + +async def test_static_signal_handler_implicit_group( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, StaticSignalHandlerWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + StaticSignalHandlerWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.signal(StaticSignalHandlerWorkflow.my_signal) + await handle.result() + events = await _fetch_events(handle) + signal = _event_marker(_signaled_event_ids(events)[0]) + a = _label_marker(_default_marker_id(_run_id(handle), "aaa"), "aaa") + + # EG-IMPLICIT-00 + _assert_markers(_activity_event(events, "from-static-signal"), signal) + # EG-IMPLICIT-01 + _assert_markers(_activity_event(events, "from-static-signal-scoped"), signal, a) + # EG-IMPLICIT-30 + _assert_markers(_activity_event(events, "from-main-before-signal")) + _assert_markers(_activity_event(events, "from-main-after-signal")) + + +@workflow.defn +class RuntimeSignalHandlerWorkflow: + def __init__(self) -> None: + self._done = False + + @workflow.run + async def run(self) -> None: + outside = workflow.create_event_group("outside") + inside = workflow.create_event_group("inside") + + async def on_signal() -> None: + await _activity("from-runtime-signal") + with inside.scope(): + await _activity("from-runtime-signal-scoped") + self._done = True + + with outside.scope(): + workflow.set_signal_handler("mySignal", on_signal) + await _activity("in-outside") + + await _activity("from-main-before-signal") + await workflow.wait_condition(lambda: self._done) + await _activity("from-main-after-signal") + + +async def test_runtime_signal_handler_implicit_group( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, RuntimeSignalHandlerWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + RuntimeSignalHandlerWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.signal("mySignal") + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + signal_ids = _signaled_event_ids(events) + assert len(signal_ids) == 1 + signal = _event_marker(signal_ids[0]) + outside = _label_marker(_default_marker_id(run_id, "outside"), "outside") + inside = _label_marker(_default_marker_id(run_id, "inside"), "inside") + + # EG-IMPLICIT-10: handler carries the signaled event, not the registration scope + _assert_markers(_activity_event(events, "from-runtime-signal"), signal) + _assert_markers(_activity_event(events, "in-outside"), outside) + # EG-IMPLICIT-11 + _assert_markers( + _activity_event(events, "from-runtime-signal-scoped"), signal, inside + ) + # EG-IMPLICIT-30 + _assert_markers(_activity_event(events, "from-main-before-signal")) + _assert_markers(_activity_event(events, "from-main-after-signal")) + + +@workflow.defn +class BufferedSignalWorkflow: + def __init__(self) -> None: + self._unblocked = False + self._handled = False + + @workflow.run + async def run(self) -> None: + workflow.set_signal_handler("unblock", self._unblock) + await workflow.wait_condition(lambda: self._unblocked) + + async def on_signal() -> None: + await _activity("from-runtime-signal") + self._handled = True + + workflow.set_signal_handler("mySignal", on_signal) + await workflow.wait_condition(lambda: self._handled) + + def _unblock(self) -> None: + self._unblocked = True + + +async def test_buffered_signal_keeps_its_original_implicit_marker( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, BufferedSignalWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + BufferedSignalWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.signal("mySignal") + await handle.signal("unblock") + await handle.result() + events = await _fetch_events(handle) + signal_ids = _signaled_event_ids(events) + assert len(signal_ids) == 2 + # mySignal is sent first, so it is the first signaled event + signal = _event_marker(signal_ids[0]) + + # EG-IMPLICIT-12 + _assert_markers(_activity_event(events, "from-runtime-signal"), signal) + + +@workflow.defn +class CatchAllSignalWorkflow: + def __init__(self) -> None: + self._done = False + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self._done) + + @workflow.signal(dynamic=True) + async def on_any_signal(self, _name: str, _args: Sequence[RawValue]) -> None: + await _activity("from-catch-all-signal") + self._done = True + + +async def test_catch_all_signal_handler_implicit_group( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, CatchAllSignalWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + CatchAllSignalWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.signal("non-existent-signal") + await handle.result() + events = await _fetch_events(handle) + signal = _event_marker(_signaled_event_ids(events)[0]) + + # EG-IMPLICIT-20 + _assert_markers(_activity_event(events, "from-catch-all-signal"), signal) + + +@workflow.defn +class StaticUpdateHandlerWorkflow: + def __init__(self) -> None: + self._done = False + + @workflow.run + async def run(self) -> None: + await _activity("from-main-before-update") + await workflow.wait_condition(lambda: self._done) + await _activity("from-main-after-update") + + @workflow.update + async def my_update(self) -> None: + await _activity("from-static-update") + inside = workflow.create_event_group("inside") + with inside.scope(): + await _activity("from-static-update-scoped") + self._done = True + + +async def test_static_update_handler_implicit_group( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + update_id = "static-update-1" + async with new_worker( + client, StaticUpdateHandlerWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + StaticUpdateHandlerWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.execute_update(StaticUpdateHandlerWorkflow.my_update, id=update_id) + await handle.result() + events = await _fetch_events(handle) + update = _update_marker(update_id) + inside = _label_marker(_default_marker_id(_run_id(handle), "inside"), "inside") + + # EG-IMPLICIT-50 + _assert_markers(_activity_event(events, "from-static-update"), update) + # EG-IMPLICIT-51 + _assert_markers( + _activity_event(events, "from-static-update-scoped"), update, inside + ) + # EG-IMPLICIT-80 + _assert_markers(_activity_event(events, "from-main-before-update")) + _assert_markers(_activity_event(events, "from-main-after-update")) + + +@workflow.defn +class RuntimeUpdateHandlerWorkflow: + def __init__(self) -> None: + self._done = False + + @workflow.run + async def run(self) -> None: + outside = workflow.create_event_group("outside") + inside = workflow.create_event_group("inside") + + async def on_update() -> None: + await _activity("from-runtime-update") + with inside.scope(): + await _activity("from-runtime-update-scoped") + self._done = True + + with outside.scope(): + workflow.set_update_handler("myUpdate", on_update) + await _activity("in-outside") + + await _activity("from-main-before-update") + await workflow.wait_condition(lambda: self._done) + await _activity("from-main-after-update") + + +async def test_runtime_update_handler_implicit_group( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + update_id = "runtime-update-1" + async with new_worker( + client, RuntimeUpdateHandlerWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + RuntimeUpdateHandlerWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Updates that arrive before registration are rejected, not buffered. + async def handler_registered() -> None: + events = await _fetch_events(handle) + assert any( + e.event_type == EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED + and e.activity_task_scheduled_event_attributes.activity_id + == "in-outside" + for e in events + ) + + await assert_eventually(handler_registered) + await handle.execute_update("myUpdate", id=update_id) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + update = _update_marker(update_id) + outside = _label_marker(_default_marker_id(run_id, "outside"), "outside") + inside = _label_marker(_default_marker_id(run_id, "inside"), "inside") + + # EG-IMPLICIT-60 + _assert_markers(_activity_event(events, "from-runtime-update"), update) + _assert_markers(_activity_event(events, "in-outside"), outside) + # EG-IMPLICIT-61 + _assert_markers( + _activity_event(events, "from-runtime-update-scoped"), update, inside + ) + # EG-IMPLICIT-80 + _assert_markers(_activity_event(events, "from-main-before-update")) + _assert_markers(_activity_event(events, "from-main-after-update")) + + +@workflow.defn +class CatchAllUpdateWorkflow: + def __init__(self) -> None: + self._done = False + + @workflow.run + async def run(self) -> None: + await workflow.wait_condition(lambda: self._done) + + @workflow.update(dynamic=True) + async def on_any_update(self, _name: str, _args: Sequence[RawValue]) -> None: + await _activity("from-catch-all-update") + self._done = True + + +async def test_catch_all_update_handler_implicit_group( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + update_id = "catch-all-update-1" + async with new_worker( + client, CatchAllUpdateWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + CatchAllUpdateWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.execute_update("non-existent-update", id=update_id) + await handle.result() + events = await _fetch_events(handle) + + # EG-IMPLICIT-70 + _assert_markers( + _activity_event(events, "from-catch-all-update"), _update_marker(update_id) + ) + + +#################################################################################################### +# 5. Event Group Marker Aggregation (`EG-AGGREGATION`) +#################################################################################################### + + +@workflow.defn +class AggregationWorkflow: + @workflow.run + async def run(self) -> None: + a1 = workflow.create_event_group("aaa") + a2 = workflow.create_event_group("aaa") + b1 = workflow.create_event_group("bbb1", id="b-id") + b2 = workflow.create_event_group("bbb2", id="b-id") + + await _activity("direct-duplicates", [a2, b1, a1, b1, a2, a1]) + + with a1.scope(): + with a2.scope(): + with b1.scope(): + await _activity("nested-scopes") + + with a1.scope(): + with b1.scope(): + await _activity("scope-and-direct-b", [b1]) + await _activity("scope-and-direct-a-b", [b1, a1]) + + await _activity("same-instance-twice", [a1, a1]) + await _activity("same-id-direct", [b1, b2]) + with b1.scope(): + await _activity("same-id-scope-and-direct", [b2]) + + +async def test_markers_dedupe_by_id(client: Client, env: WorkflowEnvironment): + _require_event_groups_server(env) + + async with new_worker( + client, AggregationWorkflow, activities=[noop_activity] + ) as worker: + handle = await client.start_workflow( + AggregationWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + a = _label_marker(_default_marker_id(_run_id(handle), "aaa"), "aaa") + b = _label_marker("b-id", "bbb1") + both = (a, b) + + # EG-AGGREGATION-00 + _assert_markers(_activity_event(events, "direct-duplicates"), *both) + # EG-AGGREGATION-01 + _assert_markers(_activity_event(events, "nested-scopes"), *both) + # EG-AGGREGATION-02 + _assert_markers(_activity_event(events, "scope-and-direct-b"), *both) + _assert_markers(_activity_event(events, "scope-and-direct-a-b"), *both) + # EG-AGGREGATION-03 + _assert_markers(_activity_event(events, "same-instance-twice"), a) + # EG-AGGREGATION-04: compare IDs only; which label is emitted is unspecified + _assert_marker_ids( + _activity_event(events, "same-id-direct"), _label_marker_id("b-id") + ) + _assert_marker_ids( + _activity_event(events, "same-id-scope-and-direct"), + _label_marker_id("b-id"), + ) + + +#################################################################################################### +# 6. Command Type Coverage (`EG-COMMANDS`) +# +# EG-COMMANDS-23 and EG-COMMANDS-24 do not apply: Core-based SDKs have no version/sideEffect API. +# Python continue_as_new always takes options, so there is no short-form counterpart of EG-COMMANDS-40. +# ExternalWorkflowHandle.signal/cancel do not take event_groups; EG-COMMANDS-06/07 assert ambient only. +#################################################################################################### + + +@workflow.defn +class TimerCommandsWorkflow: + @workflow.run + async def run(self) -> None: + direct = workflow.create_event_group("direct") + scope = workflow.create_event_group("scope") + with scope.scope(): + await workflow.sleep(0.001, event_groups=[direct]) + try: + await workflow.wait_condition( + lambda: False, timeout=0.001, event_groups=[direct] + ) + except asyncio.TimeoutError: + pass + # A 1ms sleep is the timeout; cancelling the 60s task is the cancel + # command. Avoid asyncio.wait_for so the timeout timer is a normal + # sleep and only carries the ambient scope. + long = asyncio.create_task(workflow.sleep(60, event_groups=[direct])) + await workflow.sleep(0.001) + long.cancel() + await _swallow(long) + + +async def test_timer_commands_carry_markers(client: Client, env: WorkflowEnvironment): + _require_event_groups_server(env) + + async with new_worker(client, TimerCommandsWorkflow) as worker: + handle = await client.start_workflow( + TimerCommandsWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + both = ( + _label_marker(_default_marker_id(run_id, "direct"), "direct"), + _label_marker(_default_marker_id(run_id, "scope"), "scope"), + ) + ambient = (_label_marker(_default_marker_id(run_id, "scope"), "scope"),) + + timers = _events_of_type(events, EventType.EVENT_TYPE_TIMER_STARTED) + # sleep, wait_condition timeout, wait_for's 1ms timeout, the cancelled 60s sleep + assert len(timers) == 4 + cancels = _events_of_type(events, EventType.EVENT_TYPE_TIMER_CANCELED) + assert len(cancels) == 1 + + # EG-COMMANDS-00 and EG-COMMANDS-01 run sequentially, so they are the first two timers + _assert_markers(timers[0], *both) + _assert_markers(timers[1], *both) + # EG-COMMANDS-00-CANCEL: wait_for starts both remaining timers in one task, so select by set + rest = timers[2:] + ambient_timers = [t for t in rest if _markers(t) == sorted(ambient)] + both_timers = [t for t in rest if _markers(t) == sorted(both)] + assert len(ambient_timers) == 1 + assert len(both_timers) == 1 + _assert_markers(cancels[0], *both) + + +@workflow.defn +class ActivityCommandsWorkflow: + @workflow.run + async def run(self) -> None: + direct = workflow.create_event_group("direct") + scope = workflow.create_event_group("scope") + with scope.scope(): + await workflow.execute_activity( + noop_activity, + start_to_close_timeout=_ACT_TIMEOUT, + schedule_to_start_timeout=timedelta(seconds=10), + event_groups=[direct], + activity_id="activity", + ) + await _swallow( + asyncio.wait_for( + workflow.execute_activity( + sleep_activity, + start_to_close_timeout=_ACT_TIMEOUT, + schedule_to_start_timeout=timedelta(seconds=10), + cancellation_type=workflow.ActivityCancellationType.TRY_CANCEL, + event_groups=[direct], + activity_id="activity-cancelled-sleep-5s", + ), + timeout=0.001, + ) + ) + + +async def test_activity_commands_carry_markers( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, + ActivityCommandsWorkflow, + activities=[noop_activity, sleep_activity], + ) as worker: + handle = await client.start_workflow( + ActivityCommandsWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + both = ( + _label_marker(_default_marker_id(run_id, "direct"), "direct"), + _label_marker(_default_marker_id(run_id, "scope"), "scope"), + ) + + # EG-COMMANDS-02 + _assert_markers(_activity_event(events, "activity"), *both) + # EG-COMMANDS-02-CANCEL + _assert_markers(_activity_event(events, "activity-cancelled-sleep-5s"), *both) + _assert_markers( + _single_event(events, EventType.EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED), + *both, + ) + + +@workflow.defn +class LocalActivityCommandsWorkflow: + @workflow.run + async def run(self) -> None: + direct = workflow.create_event_group("direct") + scope = workflow.create_event_group("scope") + with scope.scope(): + await workflow.execute_local_activity( + noop_activity, + start_to_close_timeout=_ACT_TIMEOUT, + event_groups=[direct], + activity_id="local-activity", + ) + + cancel_trigger = workflow.create_event_group("cancel-trigger") + cancelled_la = workflow.create_event_group("cancelled-la") + sleeping = asyncio.create_task( + workflow.execute_local_activity( + sleep_activity, + start_to_close_timeout=_ACT_TIMEOUT, + cancellation_type=workflow.ActivityCancellationType.TRY_CANCEL, + event_groups=[direct, cancelled_la], + activity_id="cancelled-local-activity-sleep-5s", + ) + ) + + async def trigger() -> None: + await workflow.execute_local_activity( + noop_activity, + start_to_close_timeout=_ACT_TIMEOUT, + event_groups=[direct, cancel_trigger], + activity_id="cancel-trigger", + ) + sleeping.cancel() + + await asyncio.gather(trigger(), _swallow(sleeping)) + + await workflow.execute_local_activity( + fail_first_activity, + start_to_close_timeout=_ACT_TIMEOUT, + local_retry_threshold=timedelta(milliseconds=1), + retry_policy=RetryPolicy( + initial_interval=timedelta(seconds=1), + backoff_coefficient=1, + maximum_attempts=2, + ), + event_groups=[direct], + activity_id="backoff-local-activity-fail-first-attempt", + ) + + +async def test_local_activity_commands_carry_markers( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, + LocalActivityCommandsWorkflow, + activities=[noop_activity, sleep_activity, fail_first_activity], + ) as worker: + handle = await client.start_workflow( + LocalActivityCommandsWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + task_timeout=timedelta(seconds=5), + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + both = ( + _label_marker(_default_marker_id(run_id, "direct"), "direct"), + _label_marker(_default_marker_id(run_id, "scope"), "scope"), + ) + cancel_trigger = ( + *both, + _label_marker( + _default_marker_id(run_id, "cancel-trigger"), "cancel-trigger" + ), + ) + cancelled_la = ( + *both, + _label_marker(_default_marker_id(run_id, "cancelled-la"), "cancelled-la"), + ) + + local_acts = _markers_named(events, "core_local_activity") + # plain complete, cancel-trigger complete, cancelled LA, backoff fail, backoff success + assert len(local_acts) == 5 + + # EG-COMMANDS-03 + _assert_markers(local_acts[0], *both) + # EG-COMMANDS-03-CANCEL + _assert_markers(local_acts[1], *cancel_trigger) + _assert_markers(local_acts[2], *cancelled_la) + # EG-COMMANDS-03-BACKOFF: plan's 10s interval vs 5s WFT timeout is the same Core branch; + # local_retry_threshold of 1ms reaches it without waiting 10s. + backoff_timer = _events_of_type(events, EventType.EVENT_TYPE_TIMER_STARTED) + assert len(backoff_timer) == 1 + _assert_markers(backoff_timer[0], *both) + _assert_markers(local_acts[3], *both) + _assert_markers(local_acts[4], *both) + + +@workflow.defn +class ChildWorkflowCommandsWorkflow: + @workflow.run + async def run(self) -> None: + direct = workflow.create_event_group("direct") + scope = workflow.create_event_group("scope") + with scope.scope(): + await workflow.start_child_workflow( + NoopChildWorkflow.run, + id=f"{workflow.info().workflow_id}_child", + event_groups=[direct], + ) + await _swallow( + asyncio.wait_for( + workflow.execute_child_workflow( + SleepChildWorkflow.run, + id=f"{workflow.info().workflow_id}_child_cancel", + cancellation_type=workflow.ChildWorkflowCancellationType.WAIT_CANCELLATION_REQUESTED, + event_groups=[direct], + ), + timeout=0.001, + ) + ) + + +async def test_child_workflow_commands_carry_markers( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker( + client, + ChildWorkflowCommandsWorkflow, + NoopChildWorkflow, + SleepChildWorkflow, + ) as worker: + handle = await client.start_workflow( + ChildWorkflowCommandsWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + both = ( + _label_marker(_default_marker_id(run_id, "direct"), "direct"), + _label_marker(_default_marker_id(run_id, "scope"), "scope"), + ) + + initiated = _events_of_type( + events, EventType.EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED + ) + assert len(initiated) == 2 + # EG-COMMANDS-04 + _assert_markers(initiated[0], *both) + # EG-COMMANDS-04-CANCEL + _assert_markers(initiated[1], *both) + _assert_markers( + _single_event( + events, + EventType.EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED, + ), + *both, + ) + + +@nexusrpc.handler.service_handler +class EventGroupsNexusService: + @nexusrpc.handler.sync_operation + async def nexus_operation( + self, _ctx: nexusrpc.handler.StartOperationContext, _input: None + ) -> None: + return None + + @nexusrpc.handler.sync_operation + async def nexus_operation_sleep_5s( + self, _ctx: nexusrpc.handler.StartOperationContext, _input: None + ) -> None: + await asyncio.sleep(5) + + +@workflow.defn +class NexusCommandsWorkflow: + @workflow.run + async def run(self, endpoint: str) -> None: + direct = workflow.create_event_group("direct") + scope = workflow.create_event_group("scope") + nexus_client = workflow.create_nexus_client( + service=EventGroupsNexusService, endpoint=endpoint + ) + with scope.scope(): + await nexus_client.execute_operation( + EventGroupsNexusService.nexus_operation, + None, + event_groups=[direct], + ) + # Don't await start/execute to completion: a sync sleeper would finish + # before cancel, and Core drops a cancel issued in the same WFT as + # schedule. The 1ms sleep forces a WFT boundary after schedule. + running = asyncio.create_task( + nexus_client.execute_operation( + EventGroupsNexusService.nexus_operation_sleep_5s, + None, + cancellation_type=workflow.NexusOperationCancellationType.TRY_CANCEL, + event_groups=[direct], + ) + ) + await workflow.sleep(0.001) + running.cancel() + await _swallow(running) + + +@pytest.mark.requires_local_server +async def test_nexus_operation_commands_carry_markers( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with the time-skipping server") + + task_queue = str(uuid.uuid4()) + endpoint = make_nexus_endpoint_name(task_queue) + await env.create_nexus_endpoint(endpoint, task_queue) + async with Worker( + client, + task_queue=task_queue, + workflows=[NexusCommandsWorkflow], + nexus_service_handlers=[EventGroupsNexusService()], + ): + handle = await client.start_workflow( + NexusCommandsWorkflow.run, + endpoint, + id=f"workflow-{uuid.uuid4()}", + task_queue=task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + run_id = _run_id(handle) + both = ( + _label_marker(_default_marker_id(run_id, "direct"), "direct"), + _label_marker(_default_marker_id(run_id, "scope"), "scope"), + ) + + scheduled = _events_of_type( + events, EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + ) + assert len(scheduled) == 2 + # EG-COMMANDS-05 + _assert_markers(scheduled[0], *both) + # EG-COMMANDS-05-CANCEL + _assert_markers(scheduled[1], *both) + _assert_markers( + _single_event( + events, EventType.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED + ), + *both, + ) + + +@workflow.defn +class AmbientOnlyCommandsWorkflow: + @workflow.run + async def run(self) -> None: + scope = workflow.create_event_group("scope") + with scope.scope(): + child = await workflow.start_child_workflow( + SleepChildWorkflow.run, + id=f"{workflow.info().workflow_id}_child", + ) + await child.signal("noop") + # External handle cancel of a live child: missing-workflow not-found + # fails the WFT in a way that is not cleanly catchable here. + await workflow.get_external_workflow_handle(child.id).cancel() + workflow.upsert_memo({"some-key": "some-value"}) + workflow.upsert_search_attributes( + [SearchAttributeKey.for_bool("CustomBoolField").value_set(False)] + ) + workflow.patched("my-patch-1") + workflow.deprecate_patch("my-patch-2") + + +async def test_apis_without_options_carry_ambient_markers( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + await ensure_search_attributes_present( + client, SearchAttributeKey.for_bool("CustomBoolField") + ) + + async with new_worker( + client, AmbientOnlyCommandsWorkflow, SleepChildWorkflow + ) as worker: + handle = await client.start_workflow( + AmbientOnlyCommandsWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + events = await _fetch_events(handle) + ambient = ( + _label_marker(_default_marker_id(_run_id(handle), "scope"), "scope"), + ) + + # EG-COMMANDS-06, EG-COMMANDS-07: no direct-attach option on the external handle + _assert_markers( + _single_event( + events, + EventType.EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED, + ), + *ambient, + ) + _assert_markers( + _single_event( + events, + EventType.EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED, + ), + *ambient, + ) + # EG-COMMANDS-20 + _assert_markers( + _single_event(events, EventType.EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED), + *ambient, + ) + # EG-COMMANDS-22 + patches = _markers_named(events, "core_patch") + assert len(patches) == 2 + for patch in patches: + _assert_markers(patch, *ambient) + # EG-COMMANDS-21 plus the two TemporalChangeVersion upserts beside the patches + upserts = _events_of_type( + events, EventType.EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES + ) + assert len(upserts) == 3 + for upsert in upserts: + _assert_markers(upsert, *ambient) + + +@workflow.defn +class ContinueAsNewCommandsWorkflow: + @workflow.run + async def run(self, second_run: bool = False) -> None: + if second_run: + return + direct = workflow.create_event_group("direct") + scope = workflow.create_event_group("scope") + with scope.scope(): + workflow.continue_as_new(True, event_groups=[direct]) + + +async def test_continue_as_new_carries_markers( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker(client, ContinueAsNewCommandsWorkflow) as worker: + handle = await client.start_workflow( + ContinueAsNewCommandsWorkflow.run, + False, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + await handle.result() + # ContinuedAsNew lives on the first run, which is also the run whose id the markers used + first_run = client.get_workflow_handle(handle.id, run_id=_run_id(handle)) + events = await _fetch_events(first_run) + run_id = _run_id(handle) + both = ( + _label_marker(_default_marker_id(run_id, "direct"), "direct"), + _label_marker(_default_marker_id(run_id, "scope"), "scope"), + ) + + # EG-COMMANDS-40 + _assert_markers( + _single_event( + events, EventType.EVENT_TYPE_WORKFLOW_EXECUTION_CONTINUED_AS_NEW + ), + *both, + ) + + +#################################################################################################### +# Language-specific +#################################################################################################### + + +@workflow.defn +class EmptyLabelWorkflow: + @workflow.run + async def run(self) -> str: + try: + workflow.create_event_group("") + except ValueError as err: + return str(err) + return "no error" + + +async def test_event_group_rejects_empty_label( + client: Client, env: WorkflowEnvironment +): + _require_event_groups_server(env) + + async with new_worker(client, EmptyLabelWorkflow) as worker: + assert "Event group label cannot be empty" == await client.execute_workflow( + EmptyLabelWorkflow.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + +def test_create_event_group_requires_workflow_context(): + with pytest.raises(TemporalError): + workflow.create_event_group("outside-workflow") + + +#################################################################################################### +# History helpers +# +# Markers are rendered as strings so failures print readably and collections can be compared as +# unordered sets (by sorting). ``_render_marker`` includes the label; ``_render_marker_id`` does +# not, for the cases where two groups share an id but not a label and the emitted label is +# unspecified. +#################################################################################################### + + +def _default_marker_id(original_execution_run_id: str, label: str) -> str: + return hashlib.sha1(f"{original_execution_run_id}{label}".encode()).hexdigest() + + +def _run_id(handle: WorkflowHandle) -> str: + assert handle.first_execution_run_id is not None + return handle.first_execution_run_id + + +def _events_of_type( + events: Sequence[HistoryEvent], event_type: EventType.ValueType +) -> list[HistoryEvent]: + return [e for e in events if e.event_type == event_type] + + +def _single_event( + events: Sequence[HistoryEvent], event_type: EventType.ValueType +) -> HistoryEvent: + matches = _events_of_type(events, event_type) + assert ( + len(matches) == 1 + ), f"expected 1 {EventType.Name(event_type)}, got {len(matches)}" + return matches[0] + + +def _activity_event(events: Sequence[HistoryEvent], activity_id: str) -> HistoryEvent: + matches = [ + e + for e in events + if e.event_type == EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED + and e.activity_task_scheduled_event_attributes.activity_id == activity_id + ] + assert len(matches) == 1, f"expected 1 activity {activity_id!r}, got {len(matches)}" + return matches[0] + + +def _markers_named( + events: Sequence[HistoryEvent], marker_name: str +) -> list[HistoryEvent]: + return [ + e + for e in events + if e.event_type == EventType.EVENT_TYPE_MARKER_RECORDED + and e.marker_recorded_event_attributes.marker_name == marker_name + ] + + +def _signaled_event_ids(events: Sequence[HistoryEvent]) -> list[int]: + return [ + e.event_id + for e in events + if e.event_type == EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED + ] + + +def _render_marker(marker: EventGroupMarker) -> str: + if marker.HasField("inbound_event"): + return f"event:{marker.inbound_event.inbound_event_id}" + if marker.HasField("inbound_update"): + return f"update:{marker.inbound_update.inbound_update_id}" + try: + label = PayloadConverter.default.from_payload(marker.label.label) + return f"label:{marker.label.id}:{label}" + except Exception: + return f"label:{marker.label.id}" + + +def _render_marker_id(marker: EventGroupMarker) -> str: + if marker.HasField("inbound_event"): + return f"event:{marker.inbound_event.inbound_event_id}" + if marker.HasField("inbound_update"): + return f"update:{marker.inbound_update.inbound_update_id}" + return f"label:{marker.label.id}" + + +def _markers(event: HistoryEvent) -> list[str]: + return sorted(_render_marker(m) for m in event.event_group_markers) + + +def _marker_ids(event: HistoryEvent) -> list[str]: + return sorted(_render_marker_id(m) for m in event.event_group_markers) + + +def _assert_markers(event: HistoryEvent, *expected: str) -> None: + actual = [_render_marker(m) for m in event.event_group_markers] + assert len(actual) == len( + expected + ), f"marker count {len(actual)} != {len(expected)}: {actual}" + assert sorted(actual) == sorted(expected) + + +def _assert_marker_ids(event: HistoryEvent, *expected: str) -> None: + actual = [_render_marker_id(m) for m in event.event_group_markers] + assert len(actual) == len( + expected + ), f"marker count {len(actual)} != {len(expected)}: {actual}" + assert sorted(actual) == sorted(expected) + + +def _label_marker(group_id: str, label: str) -> str: + return f"label:{group_id}:{label}" + + +def _label_marker_id(group_id: str) -> str: + return f"label:{group_id}" + + +def _event_marker(event_id: int) -> str: + return f"event:{event_id}" + + +def _update_marker(update_id: str) -> str: + return f"update:{update_id}" + + +def _label_payload_of(event: HistoryEvent, marker_id: str) -> tuple[str, str]: + for marker in event.event_group_markers: + if marker.HasField("label") and marker.label.id == marker_id: + encoding = marker.label.label.metadata["encoding"].decode() + return encoding, marker.label.label.data.decode() + raise AssertionError(f"no label marker {marker_id!r} on event") + + +def _raw_label_payload(event: HistoryEvent, marker_id: str) -> Payload: + for marker in event.event_group_markers: + if marker.HasField("label") and marker.label.id == marker_id: + return marker.label.label + raise AssertionError(f"no label marker {marker_id!r} on event") + + +async def _fetch_events(handle: WorkflowHandle) -> list[HistoryEvent]: + return list((await handle.fetch_history()).events) + + +# Module-level so every workflow can issue a uniquely keyed activity without repeating options. +async def _activity( + activity_id: str, + event_groups: Sequence[workflow.EventGroup] | None = None, +) -> None: + await workflow.execute_activity( + noop_activity, + start_to_close_timeout=_ACT_TIMEOUT, + activity_id=activity_id, + event_groups=event_groups, + ) + + +async def _swallow(aw: object) -> None: + try: + await aw # type: ignore[misc] + except ( + asyncio.CancelledError, + asyncio.TimeoutError, + ActivityError, + ChildWorkflowError, + NexusOperationError, + ): + pass + + +@activity.defn +async def noop_activity() -> None: + return None + + +@activity.defn +async def control_activity(value: str) -> str: + return value + + +@activity.defn +async def sleep_activity() -> None: + await asyncio.sleep(5) + + +@activity.defn +async def fail_first_activity() -> None: + if activity.info().attempt == 1: + raise ApplicationError("retry me") + + +@workflow.defn +class NoopChildWorkflow: + @workflow.run + async def run(self) -> None: + return None + + +@workflow.defn +class SleepChildWorkflow: + @workflow.run + async def run(self) -> None: + await workflow.sleep(5)