-
Notifications
You must be signed in to change notification settings - Fork 3
add workflow context and add history event iterator #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
shijiesheng
merged 1 commit into
cadence-workflow:main
from
shijiesheng:workflow-thread
Sep 12, 2025
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from cadence.client import Client | ||
from cadence.workflow import WorkflowContext, WorkflowInfo | ||
|
||
|
||
class Context(WorkflowContext): | ||
|
||
def __init__(self, client: Client, info: WorkflowInfo): | ||
self._client = client | ||
self._info = info | ||
|
||
def info(self) -> WorkflowInfo: | ||
return self._info | ||
|
||
def client(self) -> Client: | ||
return self._client |
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
|
||
from cadence.api.v1.service_worker_pb2 import PollForDecisionTaskResponse | ||
from cadence.api.v1.service_workflow_pb2 import GetWorkflowExecutionHistoryRequest, GetWorkflowExecutionHistoryResponse | ||
from cadence.client import Client | ||
|
||
async def iterate_history_events(decision_task: PollForDecisionTaskResponse, client: Client): | ||
PAGE_SIZE = 1000 | ||
|
||
current_page = decision_task.history.events | ||
next_page_token = decision_task.next_page_token | ||
workflow_execution = decision_task.workflow_execution | ||
|
||
while True: | ||
for event in current_page: | ||
yield event | ||
if not next_page_token: | ||
break | ||
response: GetWorkflowExecutionHistoryResponse = await client.workflow_stub.GetWorkflowExecutionHistory(GetWorkflowExecutionHistoryRequest( | ||
domain=client.domain, | ||
workflow_execution=workflow_execution, | ||
next_page_token=next_page_token, | ||
page_size=PAGE_SIZE, | ||
)) | ||
current_page = response.history.events | ||
next_page_token = response.next_page_token |
19 changes: 6 additions & 13 deletions
19
cadence/workflow/workflow_engine.py → ...nce/_internal/workflow/workflow_engine.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,21 @@ | ||
from dataclasses import dataclass | ||
from typing import Callable | ||
|
||
from cadence._internal.workflow.context import Context | ||
from cadence.api.v1.decision_pb2 import Decision | ||
from cadence.client import Client | ||
from cadence.data_converter import DataConverter | ||
from cadence.api.v1.service_worker_pb2 import PollForDecisionTaskResponse | ||
from cadence.workflow import WorkflowInfo | ||
|
||
@dataclass | ||
class WorkflowContext: | ||
domain: str | ||
workflow_id: str | ||
run_id: str | ||
client: Client | ||
workflow_func: Callable | ||
data_converter: DataConverter | ||
|
||
@dataclass | ||
class DecisionResult: | ||
decisions: list[Decision] | ||
|
||
class WorkflowEngine: | ||
def __init__(self, context: WorkflowContext): | ||
self._context = context | ||
def __init__(self, info: WorkflowInfo, client: Client): | ||
self._context = Context(client, info) | ||
|
||
# TODO: Implement this | ||
def process_decision(self, decision_task: PollForDecisionTaskResponse) -> DecisionResult: | ||
return DecisionResult(decisions=[]) | ||
with self._context._activate(): | ||
return DecisionResult(decisions=[]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from abc import ABC, abstractmethod | ||
from contextlib import contextmanager | ||
from contextvars import ContextVar | ||
from dataclasses import dataclass | ||
from typing import Iterator | ||
|
||
from cadence.client import Client | ||
|
||
@dataclass | ||
class WorkflowInfo: | ||
workflow_type: str | ||
workflow_domain: str | ||
workflow_id: str | ||
workflow_run_id: str | ||
|
||
class WorkflowContext(ABC): | ||
_var: ContextVar['WorkflowContext'] = ContextVar("workflow") | ||
|
||
@abstractmethod | ||
def info(self) -> WorkflowInfo: | ||
... | ||
|
||
@abstractmethod | ||
def client(self) -> Client: | ||
... | ||
|
||
@contextmanager | ||
def _activate(self) -> Iterator[None]: | ||
token = WorkflowContext._var.set(self) | ||
yield None | ||
WorkflowContext._var.reset(token) | ||
|
||
@staticmethod | ||
def is_set() -> bool: | ||
return WorkflowContext._var.get(None) is not None | ||
|
||
@staticmethod | ||
def get() -> 'WorkflowContext': | ||
return WorkflowContext._var.get() |
2 changes: 1 addition & 1 deletion
2
...workflow/test_deterministic_event_loop.py → ...workflow/test_deterministic_event_loop.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
178 changes: 178 additions & 0 deletions
178
tests/cadence/_internal/workflow/test_history_event_iterator.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import pytest | ||
from unittest.mock import Mock, AsyncMock | ||
|
||
from cadence.client import Client | ||
from cadence.api.v1.common_pb2 import WorkflowExecution | ||
from cadence.api.v1.history_pb2 import HistoryEvent, History | ||
from cadence.api.v1.service_worker_pb2 import PollForDecisionTaskResponse | ||
from cadence.api.v1.service_workflow_pb2 import GetWorkflowExecutionHistoryResponse | ||
from cadence._internal.workflow.history_event_iterator import iterate_history_events | ||
|
||
|
||
@pytest.fixture | ||
def mock_client(): | ||
"""Create a mock client with workflow_stub.""" | ||
client = Mock(spec=Client) | ||
client.workflow_stub = AsyncMock() | ||
client.domain = "test-domain" | ||
return client | ||
|
||
|
||
@pytest.fixture | ||
def mock_workflow_execution(): | ||
"""Create a mock workflow execution.""" | ||
return WorkflowExecution( | ||
workflow_id="test-workflow-id", | ||
run_id="test-run-id" | ||
) | ||
|
||
|
||
def create_history_event(event_id: int) -> HistoryEvent: | ||
return HistoryEvent(event_id=event_id) | ||
|
||
|
||
async def test_iterate_history_events_single_page_no_next_token(mock_client, mock_workflow_execution): | ||
"""Test iterating over a single page of events with no next page token.""" | ||
# Create test events | ||
events = [ | ||
create_history_event(1), | ||
create_history_event(2), | ||
create_history_event(3) | ||
] | ||
|
||
# Create decision task response with events but no next page token | ||
decision_task = PollForDecisionTaskResponse( | ||
history=History(events=events), | ||
next_page_token=b"", # Empty token means no more pages | ||
workflow_execution=mock_workflow_execution | ||
) | ||
|
||
# Iterate and collect events | ||
result_events = [e async for e in iterate_history_events(decision_task, mock_client)] | ||
|
||
# Verify all events were returned | ||
assert len(result_events) == 3 | ||
assert result_events[0].event_id == 1 | ||
assert result_events[1].event_id == 2 | ||
assert result_events[2].event_id == 3 | ||
|
||
# Verify no additional API calls were made | ||
mock_client.workflow_stub.GetWorkflowExecutionHistory.assert_not_called() | ||
|
||
|
||
async def test_iterate_history_events_empty_events(mock_client, mock_workflow_execution): | ||
"""Test iterating over empty events list.""" | ||
# Create decision task response with no events | ||
decision_task = PollForDecisionTaskResponse( | ||
history=History(events=[]), | ||
next_page_token=b"", | ||
workflow_execution=mock_workflow_execution | ||
) | ||
|
||
# Iterate and collect events | ||
result_events = [e async for e in iterate_history_events(decision_task, mock_client)] | ||
|
||
# Verify no events were returned | ||
assert len(result_events) == 0 | ||
|
||
# Verify no additional API calls were made | ||
mock_client.workflow_stub.GetWorkflowExecutionHistory.assert_not_called() | ||
|
||
async def test_iterate_history_events_multiple_pages(mock_client, mock_workflow_execution): | ||
"""Test iterating over multiple pages of events.""" | ||
|
||
# Create decision task response with first page and next page token | ||
decision_task = PollForDecisionTaskResponse( | ||
history=History(events=[ | ||
create_history_event(1), | ||
create_history_event(2) | ||
]), | ||
next_page_token=b"page2_token", | ||
workflow_execution=mock_workflow_execution | ||
) | ||
|
||
# Mock the subsequent API calls | ||
second_response = GetWorkflowExecutionHistoryResponse( | ||
history=History(events=[ | ||
create_history_event(3), | ||
create_history_event(4) | ||
]), | ||
next_page_token=b"page3_token" | ||
) | ||
|
||
third_response = GetWorkflowExecutionHistoryResponse( | ||
history=History(events=[ | ||
create_history_event(5) | ||
]), | ||
next_page_token=b"" # No more pages | ||
) | ||
|
||
# Configure mock to return responses in sequence | ||
mock_client.workflow_stub.GetWorkflowExecutionHistory.side_effect = [ | ||
second_response, | ||
third_response | ||
] | ||
|
||
# Iterate and collect events | ||
result_events = [e async for e in iterate_history_events(decision_task, mock_client)] | ||
|
||
# Verify all events from all pages were returned | ||
assert len(result_events) == 5 | ||
assert result_events[0].event_id == 1 | ||
assert result_events[1].event_id == 2 | ||
assert result_events[2].event_id == 3 | ||
assert result_events[3].event_id == 4 | ||
assert result_events[4].event_id == 5 | ||
|
||
# Verify correct API calls were made | ||
assert mock_client.workflow_stub.GetWorkflowExecutionHistory.call_count == 2 | ||
|
||
# Verify first API call | ||
first_call = mock_client.workflow_stub.GetWorkflowExecutionHistory.call_args_list[0] | ||
first_request = first_call[0][0] | ||
assert first_request.domain == "test-domain" | ||
assert first_request.workflow_execution == mock_workflow_execution | ||
assert first_request.next_page_token == b"page2_token" | ||
assert first_request.page_size == 1000 | ||
|
||
# Verify second API call | ||
second_call = mock_client.workflow_stub.GetWorkflowExecutionHistory.call_args_list[1] | ||
second_request = second_call[0][0] | ||
assert second_request.domain == "test-domain" | ||
assert second_request.workflow_execution == mock_workflow_execution | ||
assert second_request.next_page_token == b"page3_token" | ||
assert second_request.page_size == 1000 | ||
|
||
async def test_iterate_history_events_single_page_with_next_token_then_empty(mock_client, mock_workflow_execution): | ||
"""Test case where first page has next token but second page is empty.""" | ||
# Create first page of events | ||
first_page_events = [ | ||
create_history_event(1), | ||
create_history_event(2) | ||
] | ||
|
||
# Create decision task response with first page and next page token | ||
decision_task = PollForDecisionTaskResponse( | ||
history=History(events=first_page_events), | ||
next_page_token=b"page2_token", | ||
workflow_execution=mock_workflow_execution | ||
) | ||
|
||
# Mock the second API call to return empty page | ||
second_response = GetWorkflowExecutionHistoryResponse( | ||
history=History(events=[]), | ||
next_page_token=b"" # No more pages | ||
) | ||
|
||
mock_client.workflow_stub.GetWorkflowExecutionHistory.return_value = second_response | ||
|
||
# Iterate and collect events | ||
result_events = [e async for e in iterate_history_events(decision_task, mock_client)] | ||
|
||
# Verify only first page events were returned | ||
assert len(result_events) == 2 | ||
assert result_events[0].event_id == 1 | ||
assert result_events[1].event_id == 2 | ||
|
||
# Verify one API call was made | ||
assert mock_client.workflow_stub.GetWorkflowExecutionHistory.call_count == 1 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: importing this context
from cadence._internal.workflow.context import Context
will confuse people withfrom cadence.workflow import WorkflowContext
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think that's a misunderstanding. This is just an internal implementation that user will never get exposed to.