2626)
2727from dbos ._context import assert_current_dbos_context
2828from dbos ._dbos import WorkflowHandleAsync
29+ from dbos ._error import DBOSAwaitedWorkflowCancelledError , DBOSWorkflowCancelledError
2930from dbos ._schemas .system_database import SystemSchema
3031from dbos ._sys_db import WorkflowStatusString
3132from dbos ._utils import GlobalParams
@@ -853,9 +854,8 @@ def regular_workflow() -> None:
853854
854855 # Complete the blocked workflow
855856 blocking_event .set ()
856- with pytest .raises (Exception ) as exc_info :
857+ with pytest .raises (DBOSAwaitedWorkflowCancelledError ) :
857858 blocked_handle .get_result ()
858- assert "was cancelled" in str (exc_info .value )
859859
860860 # Verify all queue entries eventually get cleaned up.
861861 assert queue_entries_are_cleaned_up (dbos )
@@ -891,9 +891,8 @@ def normal_workflow() -> None:
891891
892892 # Verify the blocked workflows are cancelled
893893 for handle in handles :
894- with pytest .raises (Exception ) as exc_info :
894+ with pytest .raises (DBOSAwaitedWorkflowCancelledError ) :
895895 handle .get_result ()
896- assert "was cancelled" in str (exc_info .value )
897896
898897 # Verify the normal workflow succeeds
899898 normal_handle .get_result ()
@@ -911,17 +910,14 @@ def parent_workflow() -> None:
911910
912911 with SetWorkflowTimeout (1.0 ):
913912 handle = queue .enqueue (parent_workflow )
914- with pytest .raises (Exception ) as exc_info :
913+ with pytest .raises (DBOSAwaitedWorkflowCancelledError ) :
915914 handle .get_result ()
916- assert "was cancelled" in str (exc_info .value )
917915
918- with pytest .raises (Exception ) as exc_info :
916+ with pytest .raises (DBOSAwaitedWorkflowCancelledError ) :
919917 DBOS .retrieve_workflow (child_id ).get_result ()
920- assert "was cancelled" in str (exc_info .value )
921918
922919 # Verify if a parent called with a timeout enqueues a blocked child
923920 # then exits the deadline propagates and the child is cancelled.
924- child_id = str (uuid .uuid4 ())
925921 queue = Queue ("regular_queue" )
926922
927923 @DBOS .workflow ()
@@ -931,9 +927,41 @@ def exiting_parent_workflow() -> str:
931927
932928 with SetWorkflowTimeout (1.0 ):
933929 child_id = exiting_parent_workflow ()
934- with pytest .raises (Exception ) as exc_info :
930+ with pytest .raises (DBOSAwaitedWorkflowCancelledError ):
931+ DBOS .retrieve_workflow (child_id ).get_result ()
932+
933+ # Verify if a parent called with a timeout enqueues a child that
934+ # never starts because the queue is blocked, the deadline propagates
935+ # and both parent and child are cancelled.
936+ child_id = str (uuid .uuid4 ())
937+ queue = Queue ("stuck_queue" , concurrency = 1 )
938+
939+ start_event = threading .Event ()
940+ blocking_event = threading .Event ()
941+
942+ @DBOS .workflow ()
943+ def stuck_workflow () -> None :
944+ start_event .set ()
945+ blocking_event .wait ()
946+
947+ stuck_handle = queue .enqueue (stuck_workflow )
948+ start_event .wait ()
949+
950+ @DBOS .workflow ()
951+ def blocked_parent_workflow () -> None :
952+ with SetWorkflowID (child_id ):
953+ queue .enqueue (blocking_workflow )
954+ while True :
955+ DBOS .sleep (0.1 )
956+
957+ with SetWorkflowTimeout (1.0 ):
958+ handle = DBOS .start_workflow (blocked_parent_workflow )
959+ with pytest .raises (DBOSWorkflowCancelledError ):
960+ handle .get_result ()
961+ with pytest .raises (DBOSAwaitedWorkflowCancelledError ):
935962 DBOS .retrieve_workflow (child_id ).get_result ()
936- assert "was cancelled" in str (exc_info .value )
963+ blocking_event .set ()
964+ stuck_handle .get_result ()
937965
938966 # Verify all queue entries eventually get cleaned up.
939967 assert queue_entries_are_cleaned_up (dbos )
@@ -1341,3 +1369,80 @@ def test_workflow() -> str:
13411369 # Change the version, verify the other version complets
13421370 GlobalParams .app_version = other_version
13431371 assert other_version_handle .get_result ()
1372+
1373+
1374+ def test_timeout_queue_recovery (dbos : DBOS ) -> None :
1375+ queue = Queue ("test_queue" )
1376+ evt = threading .Event ()
1377+
1378+ @DBOS .workflow ()
1379+ def blocking_workflow () -> None :
1380+ evt .set ()
1381+ while True :
1382+ DBOS .sleep (0.1 )
1383+
1384+ timeout = 3.0
1385+ enqueue_time = time .time ()
1386+ with SetWorkflowTimeout (timeout ):
1387+ original_handle = queue .enqueue (blocking_workflow )
1388+
1389+ # Verify the workflow's timeout is properly configured
1390+ evt .wait ()
1391+ original_status = original_handle .get_status ()
1392+ assert original_status .workflow_timeout_ms == timeout * 1000
1393+ assert (
1394+ original_status .workflow_deadline_epoch_ms is not None
1395+ and original_status .workflow_deadline_epoch_ms > enqueue_time * 1000
1396+ )
1397+
1398+ # Recover the workflow. Verify its deadline remains the same
1399+ evt .clear ()
1400+ handles = DBOS ._recover_pending_workflows ()
1401+ assert len (handles ) == 1
1402+ evt .wait ()
1403+ recovered_handle = handles [0 ]
1404+ recovered_status = recovered_handle .get_status ()
1405+ assert recovered_status .workflow_timeout_ms == timeout * 1000
1406+ assert (
1407+ recovered_status .workflow_deadline_epoch_ms
1408+ == original_status .workflow_deadline_epoch_ms
1409+ )
1410+
1411+ with pytest .raises (DBOSAwaitedWorkflowCancelledError ):
1412+ original_handle .get_result ()
1413+
1414+ with pytest .raises (DBOSAwaitedWorkflowCancelledError ):
1415+ recovered_handle .get_result ()
1416+
1417+
1418+ def test_unsetting_timeout (dbos : DBOS ) -> None :
1419+
1420+ queue = Queue ("test_queue" )
1421+
1422+ @DBOS .workflow ()
1423+ def child () -> str :
1424+ for _ in range (5 ):
1425+ DBOS .sleep (1 )
1426+ return DBOS .workflow_id
1427+
1428+ @DBOS .workflow ()
1429+ def parent (child_one : str , child_two : str ) -> None :
1430+ with SetWorkflowID (child_two ):
1431+ with SetWorkflowTimeout (None ):
1432+ queue .enqueue (child )
1433+
1434+ with SetWorkflowID (child_one ):
1435+ queue .enqueue (child )
1436+
1437+ child_one , child_two = str (uuid .uuid4 ()), str (uuid .uuid4 ())
1438+ with SetWorkflowTimeout (1.0 ):
1439+ queue .enqueue (parent , child_one , child_two ).get_result ()
1440+
1441+ # Verify child one, which has a propagated timeout, is cancelled
1442+ handle : WorkflowHandle [str ] = DBOS .retrieve_workflow (child_one )
1443+ with pytest .raises (DBOSAwaitedWorkflowCancelledError ):
1444+ handle .get_result ()
1445+
1446+ # Verify child two, which doesn't have a timeout, succeeds
1447+ handle = DBOS .retrieve_workflow (child_two )
1448+ assert handle .get_result () == child_two
0 commit comments