Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions temporalio/bridge/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions temporalio/bridge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions temporalio/bridge/_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down Expand Up @@ -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
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading