11import asyncio
2- from dataclasses import dataclass
2+ from dataclasses import dataclass , field
33from datetime import timedelta
4- from typing import Optional
4+ from typing import Optional , Callable
55
66from temporalio import activity , workflow
77
8+ from resource_locking .resource_allocator import ResourceAllocator
89from resource_locking .shared import (
910 LOCK_MANAGER_WORKFLOW_ID ,
1011 AcquireRequest ,
11- AcquireResponse ,
12+ AcquireResponse , AcquiredResource ,
1213)
1314
1415@dataclass
@@ -38,7 +39,7 @@ class ResourceLockingWorkflowInput:
3839 should_continue_as_new : bool
3940
4041 # Used to transfer resource ownership between iterations during continue_as_new
41- already_assigned_resource : Optional [AcquireResponse ]
42+ already_acquired_resource : Optional [AcquiredResource ] = field ( default = None )
4243
4344
4445class FailWorkflowException (Exception ):
@@ -51,53 +52,13 @@ class FailWorkflowException(Exception):
5152
5253@workflow .defn (failure_exception_types = [FailWorkflowException ])
5354class ResourceLockingWorkflow :
54- def __init__ (self ):
55- self .assigned_resource : Optional [AcquireResponse ] = None
56-
57- @workflow .signal (name = "assign_resource" )
58- def handle_assign_resource (self , input : AcquireResponse ):
59- self .assigned_resource = input
60-
6155 @workflow .run
6256 async def run (self , input : ResourceLockingWorkflowInput ):
63- if has_timeout (workflow .info ().run_timeout ):
64- # See "locking" comment below for rationale
65- raise FailWorkflowException (
66- f"ResourceLockingWorkflow cannot have a run_timeout (found { workflow .info ().run_timeout } )"
67- )
68- if has_timeout (workflow .info ().execution_timeout ):
69- raise FailWorkflowException (
70- f"ResourceLockingWorkflow cannot have an execution_timeout (found { workflow .info ().execution_timeout } )"
71- )
72-
73- sem_handle = workflow .get_external_workflow_handle (LOCK_MANAGER_WORKFLOW_ID )
74-
75- info = workflow .info ()
76- if input .already_assigned_resource is None :
77- await sem_handle .signal ("acquire_resource" , AcquireRequest (info .workflow_id ))
78- elif info .continued_run_id :
79- self .assigned_resource = input .already_assigned_resource
80- else :
81- raise FailWorkflowException (
82- f"Only set 'already_assigned_resource' when using continue_as_new"
83- )
84-
85- await workflow .wait_condition (
86- lambda : self .assigned_resource is not None , timeout = MAX_RESOURCE_WAIT_TIME
87- )
88- if self .assigned_resource is None :
89- raise FailWorkflowException (
90- f"No resource was assigned after { MAX_RESOURCE_WAIT_TIME } "
91- )
92-
93- # From this point forward, we own the resource. Note that this is a lock, not a lease! Our finally block will
94- # release the resource if an activity fails. This is why we asserted the lack of workflow-level timeouts
95- # above - the finally block wouldn't run if there was a timeout.
96- try :
57+ async with ResourceAllocator .acquire_resource (already_acquired_resource = input .already_acquired_resource ) as resource :
9758 for iteration in ["first" , "second" , "third" ]:
9859 await workflow .execute_activity (
9960 use_resource ,
100- UseResourceActivityInput (self . assigned_resource .resource , iteration ),
61+ UseResourceActivityInput (resource .resource , iteration ),
10162 start_to_close_timeout = timedelta (seconds = 10 ),
10263 )
10364
@@ -111,17 +72,11 @@ async def run(self, input: ResourceLockingWorkflowInput):
11172 next_input = ResourceLockingWorkflowInput (
11273 iteration_to_fail_after = input .iteration_to_fail_after ,
11374 should_continue_as_new = False ,
114- already_assigned_resource = self . assigned_resource ,
75+ already_acquired_resource = resource ,
11576 )
77+
78+ # By default, ResourceAllocator will release the resource when we return. We want to hold the resource
79+ # across continue-as-new for the sake of demonstration.
80+ resource .autorelease = False
81+
11682 workflow .continue_as_new (next_input )
117- finally :
118- # Only release the resource if we didn't continue-as-new. workflow.continue_as_new raises to halt workflow
119- # execution, but this code in this finally block will still run. It wouldn't successfully send the signal...
120- # the if statement just avoids some warnings in the log.
121- if not input .should_continue_as_new :
122- await sem_handle .signal (self .assigned_resource .release_signal_name )
123-
124- def has_timeout (timeout : Optional [timedelta ]) -> bool :
125- # After continue_as_new, timeouts are 0, even if they were None before continue_as_new (and were not set in the
126- # continue_as_new call).
127- return timeout is not None and timeout > timedelta (0 )
0 commit comments