@@ -77,3 +77,98 @@ async fn workflow_task_before_the_deadline_completes_normally() {
7777 assert ! ( events_of( & history, "WorkflowTimedOut" ) . is_empty( ) ) ;
7878 assert_eq ! ( events_of( & history, "WorkflowCompleted" ) . len( ) , 1 ) ;
7979}
80+
81+ // Server #794: a failure or history wait reported after the run's deadline
82+ // closes the run as timed out, and the worker gets the same receipt a
83+ // completion gets (PHP SDK 3.6.1 does not recognise a timeout refusal shaped
84+ // as `waiting_for_history`). Published 2.11.8
85+ // acknowledged the wait and left the run waiting.
86+ #[ tokio:: test]
87+ async fn reports_after_a_deadline_time_out_the_run_with_the_completion_receipt ( ) {
88+ let waiting = json ! ( { "type" : "WorkflowTaskWaitingForHistory" , "message" : "history is not ready yet" } ) ;
89+ let failure = json ! ( { "type" : "RuntimeException" , "message" : "worker failed" } ) ;
90+ for kind in [ "run" , "execution" ] {
91+ let mut receipts = Vec :: new ( ) ;
92+ for report in [ "waiting" , "failure" , "completion" ] {
93+ let database = TestDatabase :: new ( ) . await ;
94+ let app = router ( database. open ( ) . await . unwrap ( ) ) ;
95+ register ( & app, "worker" , json ! ( [ "echo" ] ) , json ! ( [ ] ) ) . await ;
96+ let workflow_id = format ! ( "deadline-{kind}-{report}" ) ;
97+ let ( status, started) = request ( & app, "POST" , "/api/workflows" , json ! ( { "workflow_id" : workflow_id, "workflow_type" : "echo" ,
98+ "task_queue" : "test" , "input" : envelope( Payload :: Array ( vec![ ] ) ) , format!( "{kind}_timeout_seconds" ) : 600 } ) ) . await ;
99+ assert_eq ! ( status, StatusCode :: CREATED , "{started}" ) ;
100+ let task = poll ( & app, "worker" , "workflow" ) . await ;
101+ if kind == "run" {
102+ database. execute ( "UPDATE workflow_runs SET run_deadline_at='2000-01-01 00:00:00'" ) . await ;
103+ } else {
104+ database. execute ( "UPDATE workflow_runs SET execution_deadline_at='2000-01-01 00:00:00'" ) . await ;
105+ }
106+ let task_id = task[ "task_id" ] . as_str ( ) . unwrap ( ) ;
107+ let fail_path = format ! ( "/api/worker/workflow-tasks/{task_id}/fail" ) ;
108+ let fail_body = |failure : & Value | json ! ( { "lease_owner" : task[ "lease_owner" ] , "workflow_task_attempt" : task[ "workflow_task_attempt" ] , "failure" : failure} ) ;
109+ let history_before = run_history ( & app, & json ! ( workflow_id) , & started[ "run_id" ] ) . await ;
110+ let mut stale = fail_body ( & waiting) ;
111+ stale[ "workflow_task_attempt" ] = json ! ( task[ "workflow_task_attempt" ] . as_i64( ) . unwrap( ) + 1 ) ;
112+ let ( status, refused) = request ( & app, "POST" , & fail_path, stale) . await ;
113+ assert_eq ! ( status, StatusCode :: CONFLICT , "{kind} {report}: {refused}" ) ;
114+ assert_eq ! ( run_history( & app, & json!( workflow_id) , & started[ "run_id" ] ) . await , history_before, "a stale attempt changes nothing" ) ;
115+
116+ let ( status, mut receipt) = match report {
117+ "waiting" => request ( & app, "POST" , & fail_path, fail_body ( & waiting) ) . await ,
118+ "failure" => request ( & app, "POST" , & fail_path, fail_body ( & failure) ) . await ,
119+ _ => finish_task ( & app, & task, json ! ( [ { "type" : "complete_workflow" , "result" : envelope( Payload :: Long ( 2 ) ) } ] ) ) . await ,
120+ } ;
121+ assert_eq ! ( status, StatusCode :: CONFLICT , "{kind} {report}: {receipt}" ) ;
122+ assert_eq ! ( receipt[ "task_id" ] , task[ "task_id" ] , "{receipt}" ) ;
123+ assert_eq ! ( receipt[ "workflow_task_attempt" ] , task[ "workflow_task_attempt" ] , "{receipt}" ) ;
124+ assert_eq ! ( receipt[ "run_id" ] , started[ "run_id" ] , "{receipt}" ) ;
125+ assert_eq ! ( receipt[ "outcome" ] , "completed" , "{receipt}" ) ;
126+ assert_eq ! ( receipt[ "recorded" ] , false , "{receipt}" ) ;
127+ assert_eq ! ( receipt[ "run_status" ] , "failed" , "{receipt}" ) ;
128+ assert_eq ! ( receipt[ "reason" ] , "run_timed_out" , "{receipt}" ) ;
129+ assert_eq ! ( receipt[ "created_task_ids" ] , json!( [ ] ) , "{receipt}" ) ;
130+ assert ! ( receipt[ "protocol_version" ] . is_string( ) && receipt[ "server_capabilities" ] . is_object( ) , "{receipt}" ) ;
131+ let ( _, described) = request ( & app, "GET" , & format ! ( "/api/workflows/{workflow_id}" ) , Value :: Null ) . await ;
132+ assert_eq ! ( ( described[ "status" ] . as_str( ) , described[ "closed_reason" ] . as_str( ) ) , ( Some ( "failed" ) , Some ( "timed_out" ) ) , "{described}" ) ;
133+ let history = run_history ( & app, & json ! ( workflow_id) , & started[ "run_id" ] ) . await ;
134+ let timed_out = events_of ( & history, "WorkflowTimedOut" ) ;
135+ assert_eq ! ( timed_out. len( ) , 1 , "{history}" ) ;
136+ assert_eq ! ( timed_out[ 0 ] [ "payload" ] [ "timeout_kind" ] , format!( "{kind}_timeout" ) ) ;
137+ assert ! ( poll( & app, "worker" , "workflow" ) . await . is_null( ) , "no workflow task stays open" ) ;
138+ let ( status, replay) = request ( & app, "POST" , & fail_path, fail_body ( & waiting) ) . await ;
139+ assert_eq ! ( status, StatusCode :: CONFLICT , "{replay}" ) ;
140+ let history = run_history ( & app, & json ! ( workflow_id) , & started[ "run_id" ] ) . await ;
141+ assert_eq ! ( events_of( & history, "WorkflowTimedOut" ) . len( ) , 1 , "a replay adds no second timeout" ) ;
142+ for field in [ "task_id" , "run_id" ] {
143+ receipt[ field] = Value :: Null ;
144+ }
145+ receipts. push ( receipt) ;
146+ }
147+ assert_eq ! ( receipts[ 0 ] , receipts[ 2 ] , "{kind}: a history wait gets the completion's receipt" ) ;
148+ assert_eq ! ( receipts[ 1 ] , receipts[ 2 ] , "{kind}: a failure gets the completion's receipt" ) ;
149+ }
150+ }
151+
152+ // Before the deadline a history wait and a failure are acknowledged as usual.
153+ #[ tokio:: test]
154+ async fn reports_before_a_deadline_are_acknowledged ( ) {
155+ for ( kind, failure, outcome) in [
156+ ( "run" , json ! ( { "type" : "WorkflowTaskWaitingForHistory" , "message" : "history is not ready yet" } ) , "waiting_for_history" ) ,
157+ ( "execution" , json ! ( { "type" : "RuntimeException" , "message" : "worker failed" } ) , "failed" ) ,
158+ ] {
159+ let database = TestDatabase :: new ( ) . await ;
160+ let app = router ( database. open ( ) . await . unwrap ( ) ) ;
161+ register ( & app, "worker" , json ! ( [ "echo" ] ) , json ! ( [ ] ) ) . await ;
162+ let workflow_id = format ! ( "in-time-{kind}" ) ;
163+ let ( status, started) = request ( & app, "POST" , "/api/workflows" , json ! ( { "workflow_id" : workflow_id, "workflow_type" : "echo" ,
164+ "task_queue" : "test" , "input" : envelope( Payload :: Array ( vec![ ] ) ) , format!( "{kind}_timeout_seconds" ) : 600 } ) ) . await ;
165+ assert_eq ! ( status, StatusCode :: CREATED , "{started}" ) ;
166+ let task = poll ( & app, "worker" , "workflow" ) . await ;
167+ let ( status, answer) = request ( & app, "POST" , & format ! ( "/api/worker/workflow-tasks/{}/fail" , task[ "task_id" ] . as_str( ) . unwrap( ) ) ,
168+ json ! ( { "lease_owner" : task[ "lease_owner" ] , "workflow_task_attempt" : task[ "workflow_task_attempt" ] , "failure" : failure} ) ) . await ;
169+ assert_eq ! ( status, StatusCode :: OK , "{kind}: {answer}" ) ;
170+ assert_eq ! ( ( answer[ "outcome" ] . as_str( ) , answer[ "recorded" ] . as_bool( ) ) , ( Some ( outcome) , Some ( true ) ) , "{answer}" ) ;
171+ let history = run_history ( & app, & json ! ( workflow_id) , & started[ "run_id" ] ) . await ;
172+ assert ! ( events_of( & history, "WorkflowTimedOut" ) . is_empty( ) , "{history}" ) ;
173+ }
174+ }
0 commit comments