4949from temporalio .api .sdk .v1 import EnhancedStackTrace
5050from temporalio .api .workflowservice .v1 import (
5151 GetWorkflowExecutionHistoryRequest ,
52- PauseActivityRequest ,
5352 ResetStickyTaskQueueRequest ,
5453)
5554from temporalio .bridge .proto .workflow_activation import WorkflowActivation
5655from temporalio .bridge .proto .workflow_completion import WorkflowActivationCompletion
5756from temporalio .client import (
57+ AsyncActivityCancelledError ,
5858 Client ,
5959 RPCError ,
6060 RPCStatusCode ,
127127 new_worker ,
128128 pause_and_assert ,
129129 unpause_and_assert ,
130+ wait_for_next_heartbeat_cycle ,
130131 workflow_update_exists ,
131132)
132133from tests .helpers .external_stack_trace import (
@@ -7637,14 +7638,16 @@ async def heartbeat_activity(
76377638 while True :
76387639 try :
76397640 activity .heartbeat ()
7640- # If we are on the second attempt, we have retried due to pause/unpause.
7641- if activity .info ().attempt > 1 :
7641+ # If we have heartbeat details, we are on the second attempt, we have retried due to pause/unpause.
7642+ if activity .info ().heartbeat_details :
76427643 return activity .cancellation_details ()
76437644 await asyncio .sleep (0.1 )
76447645 except (CancelledError , asyncio .CancelledError ) as err :
76457646 if not catch_err :
76467647 raise err
76477648 return activity .cancellation_details ()
7649+ finally :
7650+ activity .heartbeat ("finally-complete" )
76487651
76497652
76507653@activity .defn
@@ -7654,14 +7657,16 @@ def sync_heartbeat_activity(
76547657 while True :
76557658 try :
76567659 activity .heartbeat ()
7657- # If we are on the second attempt, we have retried due to pause/unpause.
7658- if activity .info ().attempt > 1 :
7660+ # If we have heartbeat details, we are on the second attempt, we have retried due to pause/unpause.
7661+ if activity .info ().heartbeat_details :
76597662 return activity .cancellation_details ()
76607663 time .sleep (0.1 )
76617664 except (CancelledError , asyncio .CancelledError ) as err :
76627665 if not catch_err :
76637666 raise err
76647667 return activity .cancellation_details ()
7668+ finally :
7669+ activity .heartbeat ("finally-complete" )
76657670
76667671
76677672@workflow .defn
@@ -7806,7 +7811,10 @@ async def test_activity_pause_unpause(client: Client, env: WorkflowEnvironment):
78067811
78077812 # Wait for next heartbeat to propagate the cancellation. Unpausing before the heartbeat
78087813 # will show activity as unpaused to core. Consequently, it will *not* issue an activity cancel.
7809- time .sleep (0.3 )
7814+ await wait_for_next_heartbeat_cycle (
7815+ handle , activity_info_1 .activity_id , activity_info_1 .last_heartbeat_time
7816+ )
7817+
78107818 # Unpause activity
78117819 await unpause_and_assert (client , handle , activity_info_1 .activity_id )
78127820 # Expect second activity to have started now
@@ -7818,11 +7826,74 @@ async def test_activity_pause_unpause(client: Client, env: WorkflowEnvironment):
78187826 # Pause activity then assert it is paused
78197827 await pause_and_assert (client , handle , activity_info_2 .activity_id )
78207828 # Wait for next heartbeat to propagate the cancellation.
7821- time .sleep (0.3 )
7829+ await wait_for_next_heartbeat_cycle (
7830+ handle , activity_info_2 .activity_id , activity_info_2 .last_heartbeat_time
7831+ )
78227832 # Unpause activity
78237833 await unpause_and_assert (client , handle , activity_info_2 .activity_id )
78247834
78257835 # Check workflow complete
78267836 result = await handle .result ()
78277837 assert result [0 ] == None
78287838 assert result [1 ] == None
7839+
7840+
7841+ @activity .defn
7842+ async def external_activity_heartbeat () -> None :
7843+ activity .raise_complete_async ()
7844+
7845+
7846+ @workflow .defn
7847+ class ExternalActivityWorkflow :
7848+ @workflow .run
7849+ async def run (self , activity_id : str ) -> None :
7850+ await workflow .execute_activity (
7851+ external_activity_heartbeat ,
7852+ activity_id = activity_id ,
7853+ start_to_close_timeout = timedelta (seconds = 10 ),
7854+ heartbeat_timeout = timedelta (seconds = 1 ),
7855+ retry_policy = RetryPolicy (maximum_attempts = 2 ),
7856+ )
7857+
7858+
7859+ async def test_external_activity_cancellation_details (
7860+ client : Client , env : WorkflowEnvironment
7861+ ):
7862+ if env .supports_time_skipping :
7863+ pytest .skip ("Time-skipping server does not support pause API yet" )
7864+ async with Worker (
7865+ client ,
7866+ task_queue = str (uuid .uuid4 ()),
7867+ workflows = [ExternalActivityWorkflow ],
7868+ activities = [external_activity_heartbeat ],
7869+ ) as worker :
7870+ test_activity_id = f"heartbeat-activity-{ uuid .uuid4 ()} "
7871+
7872+ wf_handle = await client .start_workflow (
7873+ ExternalActivityWorkflow .run ,
7874+ test_activity_id ,
7875+ id = f"test-external-activity-pause-{ uuid .uuid4 ()} " ,
7876+ task_queue = worker .task_queue ,
7877+ )
7878+ wf_desc = await wf_handle .describe ()
7879+
7880+ # Wait for external activity
7881+ activity_info = await assert_pending_activity_exists_eventually (
7882+ wf_handle , test_activity_id
7883+ )
7884+ # Assert not paused
7885+ assert not activity_info .paused
7886+
7887+ external_activity_handle = client .get_async_activity_handle (
7888+ workflow_id = wf_desc .id , run_id = wf_desc .run_id , activity_id = test_activity_id
7889+ )
7890+
7891+ # Pause activity then assert it is paused
7892+ await pause_and_assert (client , wf_handle , activity_info .activity_id )
7893+
7894+ try :
7895+ await external_activity_handle .heartbeat ()
7896+ except AsyncActivityCancelledError as err :
7897+ assert err .details == temporalio .activity .ActivityCancellationDetails (
7898+ paused = True
7899+ )
0 commit comments