temporalio 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +180 -7
  3. data/bridge/Cargo.lock +208 -76
  4. data/bridge/Cargo.toml +5 -2
  5. data/bridge/sdk-core/Cargo.toml +1 -1
  6. data/bridge/sdk-core/README.md +20 -10
  7. data/bridge/sdk-core/client/Cargo.toml +1 -1
  8. data/bridge/sdk-core/client/src/lib.rs +227 -59
  9. data/bridge/sdk-core/client/src/metrics.rs +17 -8
  10. data/bridge/sdk-core/client/src/raw.rs +13 -12
  11. data/bridge/sdk-core/client/src/retry.rs +132 -43
  12. data/bridge/sdk-core/core/Cargo.toml +28 -15
  13. data/bridge/sdk-core/core/benches/workflow_replay.rs +13 -10
  14. data/bridge/sdk-core/core/src/abstractions.rs +225 -36
  15. data/bridge/sdk-core/core/src/core_tests/activity_tasks.rs +217 -79
  16. data/bridge/sdk-core/core/src/core_tests/determinism.rs +165 -2
  17. data/bridge/sdk-core/core/src/core_tests/local_activities.rs +565 -34
  18. data/bridge/sdk-core/core/src/core_tests/queries.rs +247 -90
  19. data/bridge/sdk-core/core/src/core_tests/workers.rs +3 -5
  20. data/bridge/sdk-core/core/src/core_tests/workflow_cancels.rs +1 -1
  21. data/bridge/sdk-core/core/src/core_tests/workflow_tasks.rs +430 -67
  22. data/bridge/sdk-core/core/src/ephemeral_server/mod.rs +106 -12
  23. data/bridge/sdk-core/core/src/internal_flags.rs +136 -0
  24. data/bridge/sdk-core/core/src/lib.rs +148 -34
  25. data/bridge/sdk-core/core/src/protosext/mod.rs +1 -1
  26. data/bridge/sdk-core/core/src/replay/mod.rs +185 -41
  27. data/bridge/sdk-core/core/src/telemetry/log_export.rs +190 -0
  28. data/bridge/sdk-core/core/src/telemetry/metrics.rs +219 -140
  29. data/bridge/sdk-core/core/src/telemetry/mod.rs +326 -315
  30. data/bridge/sdk-core/core/src/telemetry/prometheus_server.rs +20 -14
  31. data/bridge/sdk-core/core/src/test_help/mod.rs +85 -21
  32. data/bridge/sdk-core/core/src/worker/activities/activity_heartbeat_manager.rs +112 -156
  33. data/bridge/sdk-core/core/src/worker/activities/activity_task_poller_stream.rs +89 -0
  34. data/bridge/sdk-core/core/src/worker/activities/local_activities.rs +364 -128
  35. data/bridge/sdk-core/core/src/worker/activities.rs +263 -170
  36. data/bridge/sdk-core/core/src/worker/client/mocks.rs +23 -3
  37. data/bridge/sdk-core/core/src/worker/client.rs +48 -6
  38. data/bridge/sdk-core/core/src/worker/mod.rs +186 -75
  39. data/bridge/sdk-core/core/src/worker/workflow/bridge.rs +1 -3
  40. data/bridge/sdk-core/core/src/worker/workflow/driven_workflow.rs +13 -24
  41. data/bridge/sdk-core/core/src/worker/workflow/history_update.rs +879 -226
  42. data/bridge/sdk-core/core/src/worker/workflow/machines/activity_state_machine.rs +101 -48
  43. data/bridge/sdk-core/core/src/worker/workflow/machines/cancel_external_state_machine.rs +8 -12
  44. data/bridge/sdk-core/core/src/worker/workflow/machines/cancel_workflow_state_machine.rs +6 -9
  45. data/bridge/sdk-core/core/src/worker/workflow/machines/child_workflow_state_machine.rs +90 -32
  46. data/bridge/sdk-core/core/src/worker/workflow/machines/complete_workflow_state_machine.rs +6 -9
  47. data/bridge/sdk-core/core/src/worker/workflow/machines/continue_as_new_workflow_state_machine.rs +7 -10
  48. data/bridge/sdk-core/core/src/worker/workflow/machines/fail_workflow_state_machine.rs +6 -9
  49. data/bridge/sdk-core/core/src/worker/workflow/machines/local_activity_state_machine.rs +160 -83
  50. data/bridge/sdk-core/core/src/worker/workflow/machines/mod.rs +36 -54
  51. data/bridge/sdk-core/core/src/worker/workflow/machines/modify_workflow_properties_state_machine.rs +179 -0
  52. data/bridge/sdk-core/core/src/worker/workflow/machines/patch_state_machine.rs +104 -157
  53. data/bridge/sdk-core/core/src/worker/workflow/machines/signal_external_state_machine.rs +8 -12
  54. data/bridge/sdk-core/core/src/worker/workflow/machines/timer_state_machine.rs +9 -13
  55. data/bridge/sdk-core/core/src/worker/workflow/machines/transition_coverage.rs +10 -4
  56. data/bridge/sdk-core/core/src/worker/workflow/machines/upsert_search_attributes_state_machine.rs +14 -11
  57. data/bridge/sdk-core/core/src/worker/workflow/machines/workflow_machines/local_acts.rs +6 -17
  58. data/bridge/sdk-core/core/src/worker/workflow/machines/workflow_machines.rs +395 -299
  59. data/bridge/sdk-core/core/src/worker/workflow/machines/workflow_task_state_machine.rs +12 -20
  60. data/bridge/sdk-core/core/src/worker/workflow/managed_run/managed_wf_test.rs +33 -18
  61. data/bridge/sdk-core/core/src/worker/workflow/managed_run.rs +1032 -374
  62. data/bridge/sdk-core/core/src/worker/workflow/mod.rs +525 -392
  63. data/bridge/sdk-core/core/src/worker/workflow/run_cache.rs +40 -57
  64. data/bridge/sdk-core/core/src/worker/workflow/wft_extraction.rs +125 -0
  65. data/bridge/sdk-core/core/src/worker/workflow/wft_poller.rs +3 -6
  66. data/bridge/sdk-core/core/src/worker/workflow/workflow_stream/saved_wf_inputs.rs +117 -0
  67. data/bridge/sdk-core/core/src/worker/workflow/workflow_stream/tonic_status_serde.rs +24 -0
  68. data/bridge/sdk-core/core/src/worker/workflow/workflow_stream.rs +456 -681
  69. data/bridge/sdk-core/core-api/Cargo.toml +6 -4
  70. data/bridge/sdk-core/core-api/src/errors.rs +1 -34
  71. data/bridge/sdk-core/core-api/src/lib.rs +7 -45
  72. data/bridge/sdk-core/core-api/src/telemetry.rs +141 -0
  73. data/bridge/sdk-core/core-api/src/worker.rs +27 -1
  74. data/bridge/sdk-core/etc/deps.svg +115 -140
  75. data/bridge/sdk-core/etc/regen-depgraph.sh +5 -0
  76. data/bridge/sdk-core/fsm/rustfsm_procmacro/src/lib.rs +18 -15
  77. data/bridge/sdk-core/fsm/rustfsm_procmacro/tests/trybuild/no_handle_conversions_require_into_fail.stderr +1 -1
  78. data/bridge/sdk-core/fsm/rustfsm_trait/src/lib.rs +8 -3
  79. data/bridge/sdk-core/histories/evict_while_la_running_no_interference-16_history.bin +0 -0
  80. data/bridge/sdk-core/histories/evict_while_la_running_no_interference-23_history.bin +0 -0
  81. data/bridge/sdk-core/histories/evict_while_la_running_no_interference-85_history.bin +0 -0
  82. data/bridge/sdk-core/protos/api_upstream/buf.yaml +0 -3
  83. data/bridge/sdk-core/protos/api_upstream/build/go.mod +7 -0
  84. data/bridge/sdk-core/protos/api_upstream/build/go.sum +5 -0
  85. data/bridge/sdk-core/protos/api_upstream/{temporal/api/enums/v1/cluster.proto → build/tools.go} +7 -18
  86. data/bridge/sdk-core/protos/api_upstream/go.mod +6 -0
  87. data/bridge/sdk-core/protos/api_upstream/temporal/api/batch/v1/message.proto +12 -9
  88. data/bridge/sdk-core/protos/api_upstream/temporal/api/command/v1/message.proto +15 -26
  89. data/bridge/sdk-core/protos/api_upstream/temporal/api/common/v1/message.proto +13 -2
  90. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/batch_operation.proto +3 -2
  91. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/command_type.proto +4 -9
  92. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/common.proto +3 -2
  93. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/event_type.proto +10 -8
  94. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/failed_cause.proto +28 -2
  95. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/namespace.proto +2 -2
  96. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/query.proto +2 -2
  97. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/reset.proto +2 -2
  98. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/schedule.proto +2 -2
  99. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/task_queue.proto +2 -2
  100. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/update.proto +24 -19
  101. data/bridge/sdk-core/protos/api_upstream/temporal/api/enums/v1/workflow.proto +2 -2
  102. data/bridge/sdk-core/protos/api_upstream/temporal/api/errordetails/v1/message.proto +2 -2
  103. data/bridge/sdk-core/protos/api_upstream/temporal/api/failure/v1/message.proto +2 -2
  104. data/bridge/sdk-core/protos/api_upstream/temporal/api/filter/v1/message.proto +2 -2
  105. data/bridge/sdk-core/protos/api_upstream/temporal/api/history/v1/message.proto +62 -26
  106. data/bridge/sdk-core/protos/api_upstream/temporal/api/namespace/v1/message.proto +4 -2
  107. data/bridge/sdk-core/protos/api_upstream/temporal/api/operatorservice/v1/request_response.proto +24 -61
  108. data/bridge/sdk-core/protos/api_upstream/temporal/api/operatorservice/v1/service.proto +2 -21
  109. data/bridge/sdk-core/protos/api_upstream/temporal/api/protocol/v1/message.proto +57 -0
  110. data/bridge/sdk-core/protos/api_upstream/temporal/api/query/v1/message.proto +2 -2
  111. data/bridge/sdk-core/protos/api_upstream/temporal/api/replication/v1/message.proto +2 -2
  112. data/bridge/sdk-core/protos/api_upstream/temporal/api/schedule/v1/message.proto +110 -31
  113. data/bridge/sdk-core/protos/api_upstream/temporal/api/sdk/v1/task_complete_metadata.proto +63 -0
  114. data/bridge/sdk-core/protos/api_upstream/temporal/api/taskqueue/v1/message.proto +4 -4
  115. data/bridge/sdk-core/protos/api_upstream/temporal/api/update/v1/message.proto +71 -6
  116. data/bridge/sdk-core/protos/api_upstream/temporal/api/version/v1/message.proto +2 -2
  117. data/bridge/sdk-core/protos/api_upstream/temporal/api/workflow/v1/message.proto +3 -2
  118. data/bridge/sdk-core/protos/api_upstream/temporal/api/workflowservice/v1/request_response.proto +111 -36
  119. data/bridge/sdk-core/protos/api_upstream/temporal/api/workflowservice/v1/service.proto +19 -5
  120. data/bridge/sdk-core/protos/local/temporal/sdk/core/activity_result/activity_result.proto +1 -0
  121. data/bridge/sdk-core/protos/local/temporal/sdk/core/activity_task/activity_task.proto +1 -0
  122. data/bridge/sdk-core/protos/local/temporal/sdk/core/child_workflow/child_workflow.proto +1 -0
  123. data/bridge/sdk-core/protos/local/temporal/sdk/core/common/common.proto +1 -0
  124. data/bridge/sdk-core/protos/local/temporal/sdk/core/core_interface.proto +1 -0
  125. data/bridge/sdk-core/protos/local/temporal/sdk/core/external_data/external_data.proto +1 -0
  126. data/bridge/sdk-core/protos/local/temporal/sdk/core/workflow_activation/workflow_activation.proto +9 -0
  127. data/bridge/sdk-core/protos/local/temporal/sdk/core/workflow_commands/workflow_commands.proto +9 -1
  128. data/bridge/sdk-core/protos/local/temporal/sdk/core/workflow_completion/workflow_completion.proto +6 -0
  129. data/bridge/sdk-core/protos/testsrv_upstream/temporal/api/testservice/v1/request_response.proto +2 -2
  130. data/bridge/sdk-core/protos/testsrv_upstream/temporal/api/testservice/v1/service.proto +2 -2
  131. data/bridge/sdk-core/sdk/Cargo.toml +4 -3
  132. data/bridge/sdk-core/sdk/src/interceptors.rs +36 -3
  133. data/bridge/sdk-core/sdk/src/lib.rs +94 -25
  134. data/bridge/sdk-core/sdk/src/workflow_context.rs +13 -2
  135. data/bridge/sdk-core/sdk/src/workflow_future.rs +10 -13
  136. data/bridge/sdk-core/sdk-core-protos/Cargo.toml +5 -2
  137. data/bridge/sdk-core/sdk-core-protos/build.rs +36 -2
  138. data/bridge/sdk-core/sdk-core-protos/src/history_builder.rs +164 -104
  139. data/bridge/sdk-core/sdk-core-protos/src/history_info.rs +27 -23
  140. data/bridge/sdk-core/sdk-core-protos/src/lib.rs +252 -74
  141. data/bridge/sdk-core/sdk-core-protos/src/task_token.rs +12 -2
  142. data/bridge/sdk-core/test-utils/Cargo.toml +4 -1
  143. data/bridge/sdk-core/test-utils/src/canned_histories.rs +106 -296
  144. data/bridge/sdk-core/test-utils/src/histfetch.rs +1 -1
  145. data/bridge/sdk-core/test-utils/src/lib.rs +161 -50
  146. data/bridge/sdk-core/test-utils/src/wf_input_saver.rs +50 -0
  147. data/bridge/sdk-core/test-utils/src/workflows.rs +29 -0
  148. data/bridge/sdk-core/tests/fuzzy_workflow.rs +130 -0
  149. data/bridge/sdk-core/tests/{load_tests.rs → heavy_tests.rs} +125 -51
  150. data/bridge/sdk-core/tests/integ_tests/ephemeral_server_tests.rs +25 -3
  151. data/bridge/sdk-core/tests/integ_tests/heartbeat_tests.rs +10 -5
  152. data/bridge/sdk-core/tests/integ_tests/metrics_tests.rs +239 -0
  153. data/bridge/sdk-core/tests/integ_tests/polling_tests.rs +4 -60
  154. data/bridge/sdk-core/tests/integ_tests/queries_tests.rs +5 -128
  155. data/bridge/sdk-core/tests/integ_tests/visibility_tests.rs +83 -25
  156. data/bridge/sdk-core/tests/integ_tests/workflow_tests/activities.rs +93 -69
  157. data/bridge/sdk-core/tests/integ_tests/workflow_tests/cancel_external.rs +1 -0
  158. data/bridge/sdk-core/tests/integ_tests/workflow_tests/cancel_wf.rs +6 -13
  159. data/bridge/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs +1 -0
  160. data/bridge/sdk-core/tests/integ_tests/workflow_tests/continue_as_new.rs +6 -2
  161. data/bridge/sdk-core/tests/integ_tests/workflow_tests/determinism.rs +3 -10
  162. data/bridge/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +151 -116
  163. data/bridge/sdk-core/tests/integ_tests/workflow_tests/modify_wf_properties.rs +54 -0
  164. data/bridge/sdk-core/tests/integ_tests/workflow_tests/patches.rs +7 -28
  165. data/bridge/sdk-core/tests/integ_tests/workflow_tests/replay.rs +115 -24
  166. data/bridge/sdk-core/tests/integ_tests/workflow_tests/resets.rs +1 -0
  167. data/bridge/sdk-core/tests/integ_tests/workflow_tests/signals.rs +18 -14
  168. data/bridge/sdk-core/tests/integ_tests/workflow_tests/stickyness.rs +6 -20
  169. data/bridge/sdk-core/tests/integ_tests/workflow_tests/timers.rs +10 -21
  170. data/bridge/sdk-core/tests/integ_tests/workflow_tests/upsert_search_attrs.rs +6 -4
  171. data/bridge/sdk-core/tests/integ_tests/workflow_tests.rs +27 -18
  172. data/bridge/sdk-core/tests/main.rs +8 -16
  173. data/bridge/sdk-core/tests/runner.rs +75 -36
  174. data/bridge/sdk-core/tests/wf_input_replay.rs +32 -0
  175. data/bridge/src/connection.rs +117 -82
  176. data/bridge/src/lib.rs +356 -42
  177. data/bridge/src/runtime.rs +10 -3
  178. data/bridge/src/test_server.rs +153 -0
  179. data/bridge/src/worker.rs +133 -9
  180. data/lib/gen/temporal/api/batch/v1/message_pb.rb +8 -6
  181. data/lib/gen/temporal/api/command/v1/message_pb.rb +10 -16
  182. data/lib/gen/temporal/api/common/v1/message_pb.rb +5 -1
  183. data/lib/gen/temporal/api/enums/v1/batch_operation_pb.rb +2 -1
  184. data/lib/gen/temporal/api/enums/v1/command_type_pb.rb +3 -3
  185. data/lib/gen/temporal/api/enums/v1/common_pb.rb +2 -1
  186. data/lib/gen/temporal/api/enums/v1/event_type_pb.rb +5 -4
  187. data/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb +9 -1
  188. data/lib/gen/temporal/api/enums/v1/namespace_pb.rb +1 -1
  189. data/lib/gen/temporal/api/enums/v1/query_pb.rb +1 -1
  190. data/lib/gen/temporal/api/enums/v1/reset_pb.rb +1 -1
  191. data/lib/gen/temporal/api/enums/v1/schedule_pb.rb +1 -1
  192. data/lib/gen/temporal/api/enums/v1/task_queue_pb.rb +1 -1
  193. data/lib/gen/temporal/api/enums/v1/update_pb.rb +7 -10
  194. data/lib/gen/temporal/api/enums/v1/workflow_pb.rb +1 -1
  195. data/lib/gen/temporal/api/errordetails/v1/message_pb.rb +1 -1
  196. data/lib/gen/temporal/api/failure/v1/message_pb.rb +1 -1
  197. data/lib/gen/temporal/api/filter/v1/message_pb.rb +1 -1
  198. data/lib/gen/temporal/api/history/v1/message_pb.rb +34 -25
  199. data/lib/gen/temporal/api/namespace/v1/message_pb.rb +2 -1
  200. data/lib/gen/temporal/api/operatorservice/v1/request_response_pb.rb +14 -51
  201. data/lib/gen/temporal/api/operatorservice/v1/service_pb.rb +1 -1
  202. data/lib/gen/temporal/api/protocol/v1/message_pb.rb +30 -0
  203. data/lib/gen/temporal/api/query/v1/message_pb.rb +1 -1
  204. data/lib/gen/temporal/api/replication/v1/message_pb.rb +1 -1
  205. data/lib/gen/temporal/api/schedule/v1/message_pb.rb +22 -1
  206. data/lib/gen/temporal/api/sdk/v1/task_complete_metadata_pb.rb +23 -0
  207. data/lib/gen/temporal/api/taskqueue/v1/message_pb.rb +2 -2
  208. data/lib/gen/temporal/api/testservice/v1/request_response_pb.rb +49 -0
  209. data/lib/gen/temporal/api/testservice/v1/service_pb.rb +21 -0
  210. data/lib/gen/temporal/api/update/v1/message_pb.rb +49 -3
  211. data/lib/gen/temporal/api/version/v1/message_pb.rb +1 -1
  212. data/lib/gen/temporal/api/workflow/v1/message_pb.rb +2 -1
  213. data/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb +47 -20
  214. data/lib/gen/temporal/api/workflowservice/v1/service_pb.rb +1 -1
  215. data/lib/gen/temporal/sdk/core/activity_result/activity_result_pb.rb +13 -9
  216. data/lib/gen/temporal/sdk/core/activity_task/activity_task_pb.rb +10 -6
  217. data/lib/gen/temporal/sdk/core/child_workflow/child_workflow_pb.rb +13 -9
  218. data/lib/gen/temporal/sdk/core/common/common_pb.rb +7 -3
  219. data/lib/gen/temporal/sdk/core/core_interface_pb.rb +9 -3
  220. data/lib/gen/temporal/sdk/core/external_data/external_data_pb.rb +7 -3
  221. data/lib/gen/temporal/sdk/core/workflow_activation/workflow_activation_pb.rb +28 -21
  222. data/lib/gen/temporal/sdk/core/workflow_commands/workflow_commands_pb.rb +32 -24
  223. data/lib/gen/temporal/sdk/core/workflow_completion/workflow_completion_pb.rb +12 -5
  224. data/lib/temporalio/activity/context.rb +102 -0
  225. data/lib/temporalio/activity/info.rb +67 -0
  226. data/lib/temporalio/activity.rb +85 -0
  227. data/lib/temporalio/bridge/connect_options.rb +15 -0
  228. data/lib/temporalio/bridge/error.rb +8 -0
  229. data/lib/temporalio/bridge/retry_config.rb +24 -0
  230. data/lib/temporalio/bridge/tls_options.rb +19 -0
  231. data/lib/temporalio/bridge.rb +14 -0
  232. data/lib/{temporal → temporalio}/client/implementation.rb +57 -56
  233. data/lib/{temporal → temporalio}/client/workflow_handle.rb +35 -35
  234. data/lib/{temporal → temporalio}/client.rb +19 -32
  235. data/lib/temporalio/connection/retry_config.rb +44 -0
  236. data/lib/temporalio/connection/service.rb +20 -0
  237. data/lib/temporalio/connection/test_service.rb +92 -0
  238. data/lib/temporalio/connection/tls_options.rb +51 -0
  239. data/lib/temporalio/connection/workflow_service.rb +731 -0
  240. data/lib/temporalio/connection.rb +86 -0
  241. data/lib/{temporal → temporalio}/data_converter.rb +76 -35
  242. data/lib/{temporal → temporalio}/error/failure.rb +6 -6
  243. data/lib/{temporal → temporalio}/error/workflow_failure.rb +4 -2
  244. data/lib/{temporal → temporalio}/errors.rb +19 -1
  245. data/lib/{temporal → temporalio}/failure_converter/base.rb +5 -5
  246. data/lib/{temporal → temporalio}/failure_converter/basic.rb +58 -52
  247. data/lib/temporalio/failure_converter.rb +7 -0
  248. data/lib/temporalio/interceptor/activity_inbound.rb +22 -0
  249. data/lib/temporalio/interceptor/activity_outbound.rb +24 -0
  250. data/lib/{temporal → temporalio}/interceptor/chain.rb +7 -6
  251. data/lib/{temporal → temporalio}/interceptor/client.rb +27 -2
  252. data/lib/temporalio/interceptor.rb +22 -0
  253. data/lib/{temporal → temporalio}/payload_codec/base.rb +5 -5
  254. data/lib/{temporal → temporalio}/payload_converter/base.rb +3 -3
  255. data/lib/{temporal → temporalio}/payload_converter/bytes.rb +4 -3
  256. data/lib/{temporal → temporalio}/payload_converter/composite.rb +7 -5
  257. data/lib/{temporal → temporalio}/payload_converter/encoding_base.rb +4 -4
  258. data/lib/{temporal → temporalio}/payload_converter/json.rb +4 -3
  259. data/lib/{temporal → temporalio}/payload_converter/nil.rb +4 -3
  260. data/lib/temporalio/payload_converter.rb +14 -0
  261. data/lib/{temporal → temporalio}/retry_policy.rb +17 -7
  262. data/lib/{temporal → temporalio}/retry_state.rb +1 -1
  263. data/lib/temporalio/runtime.rb +25 -0
  264. data/lib/temporalio/testing/time_skipping_handle.rb +32 -0
  265. data/lib/temporalio/testing/time_skipping_interceptor.rb +23 -0
  266. data/lib/temporalio/testing/workflow_environment.rb +112 -0
  267. data/lib/temporalio/testing.rb +175 -0
  268. data/lib/{temporal → temporalio}/timeout_type.rb +2 -2
  269. data/lib/temporalio/version.rb +3 -0
  270. data/lib/temporalio/worker/activity_runner.rb +114 -0
  271. data/lib/temporalio/worker/activity_worker.rb +164 -0
  272. data/lib/temporalio/worker/reactor.rb +46 -0
  273. data/lib/temporalio/worker/runner.rb +63 -0
  274. data/lib/temporalio/worker/sync_worker.rb +124 -0
  275. data/lib/temporalio/worker/thread_pool_executor.rb +51 -0
  276. data/lib/temporalio/worker.rb +204 -0
  277. data/lib/temporalio/workflow/async.rb +46 -0
  278. data/lib/{temporal → temporalio}/workflow/execution_info.rb +4 -4
  279. data/lib/{temporal → temporalio}/workflow/execution_status.rb +1 -1
  280. data/lib/temporalio/workflow/future.rb +138 -0
  281. data/lib/{temporal → temporalio}/workflow/id_reuse_policy.rb +6 -6
  282. data/lib/temporalio/workflow/info.rb +76 -0
  283. data/lib/{temporal → temporalio}/workflow/query_reject_condition.rb +5 -5
  284. data/lib/temporalio.rb +12 -3
  285. data/temporalio.gemspec +11 -6
  286. metadata +137 -64
  287. data/bridge/sdk-core/Cargo.lock +0 -2606
  288. data/bridge/sdk-core/bridge-ffi/Cargo.toml +0 -24
  289. data/bridge/sdk-core/bridge-ffi/LICENSE.txt +0 -23
  290. data/bridge/sdk-core/bridge-ffi/build.rs +0 -25
  291. data/bridge/sdk-core/bridge-ffi/include/sdk-core-bridge.h +0 -249
  292. data/bridge/sdk-core/bridge-ffi/src/lib.rs +0 -825
  293. data/bridge/sdk-core/bridge-ffi/src/wrappers.rs +0 -211
  294. data/bridge/sdk-core/core/src/log_export.rs +0 -62
  295. data/bridge/sdk-core/core/src/worker/workflow/machines/mutable_side_effect_state_machine.rs +0 -127
  296. data/bridge/sdk-core/core/src/worker/workflow/machines/side_effect_state_machine.rs +0 -71
  297. data/bridge/sdk-core/protos/api_upstream/temporal/api/cluster/v1/message.proto +0 -83
  298. data/bridge/sdk-core/protos/local/temporal/sdk/core/bridge/bridge.proto +0 -210
  299. data/bridge/sdk-core/sdk/src/conversions.rs +0 -8
  300. data/lib/bridge.so +0 -0
  301. data/lib/gen/temporal/api/cluster/v1/message_pb.rb +0 -67
  302. data/lib/gen/temporal/api/enums/v1/cluster_pb.rb +0 -26
  303. data/lib/gen/temporal/sdk/core/bridge/bridge_pb.rb +0 -222
  304. data/lib/temporal/bridge.rb +0 -14
  305. data/lib/temporal/connection.rb +0 -736
  306. data/lib/temporal/failure_converter.rb +0 -8
  307. data/lib/temporal/payload_converter.rb +0 -14
  308. data/lib/temporal/runtime.rb +0 -22
  309. data/lib/temporal/version.rb +0 -3
  310. data/lib/temporal.rb +0 -8
@@ -1,188 +1,217 @@
1
1
  #[cfg(test)]
2
2
  mod managed_wf_test;
3
3
 
4
+ #[cfg(test)]
5
+ pub(crate) use managed_wf_test::ManagedWFFunc;
6
+
4
7
  use crate::{
8
+ abstractions::dbg_panic,
9
+ protosext::WorkflowActivationExt,
5
10
  worker::{
6
11
  workflow::{
7
- machines::WorkflowMachines, ActivationAction, ActivationCompleteOutcome, HistoryUpdate,
8
- LocalResolution, NewIncomingWFT, OutgoingServerCommands, RequestEvictMsg, RunActions,
9
- RunActivationCompletion, RunUpdateResponse, ServerCommandsWithWorkflowInfo, WFCommand,
10
- WorkflowBridge,
12
+ history_update::HistoryPaginator, machines::WorkflowMachines, ActivationAction,
13
+ ActivationCompleteOutcome, ActivationCompleteResult, ActivationOrAuto,
14
+ EvictionRequestResult, FailedActivationWFTReport, HeartbeatTimeoutMsg, HistoryUpdate,
15
+ LocalActivityRequestSink, LocalResolution, NextPageReq, OutgoingServerCommands,
16
+ OutstandingActivation, OutstandingTask, PermittedWFT, RequestEvictMsg, RunBasics,
17
+ ServerCommandsWithWorkflowInfo, WFCommand, WFMachinesError, WFTReportStatus,
18
+ WorkflowBridge, WorkflowTaskInfo, WFT_HEARTBEAT_TIMEOUT_FRACTION,
11
19
  },
12
- LocalActRequest,
20
+ LocalActRequest, LEGACY_QUERY_ID,
13
21
  },
14
22
  MetricsContext,
15
23
  };
16
- use futures::{stream, StreamExt};
24
+ use futures_util::future::AbortHandle;
17
25
  use std::{
26
+ collections::HashSet,
18
27
  ops::Add,
28
+ rc::Rc,
19
29
  sync::mpsc::Sender,
20
30
  time::{Duration, Instant},
21
31
  };
22
- use temporal_sdk_core_api::errors::WFMachinesError;
23
- use temporal_sdk_core_protos::coresdk::{
24
- workflow_activation::{RemoveFromCache, WorkflowActivation},
25
- workflow_commands::QueryResult,
26
- };
27
- use tokio::{
28
- sync::{
29
- mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
30
- oneshot,
32
+ use temporal_sdk_core_protos::{
33
+ coresdk::{
34
+ workflow_activation::{
35
+ create_evict_activation, query_to_job, remove_from_cache::EvictionReason,
36
+ workflow_activation_job, RemoveFromCache, WorkflowActivation,
37
+ },
38
+ workflow_commands::QueryResult,
39
+ workflow_completion,
31
40
  },
32
- task,
33
- task::JoinHandle,
41
+ temporal::api::{enums::v1::WorkflowTaskFailedCause, failure::v1::Failure},
42
+ TaskToken,
34
43
  };
35
- use tokio_stream::wrappers::UnboundedReceiverStream;
44
+ use tokio::sync::oneshot;
36
45
  use tracing::Span;
37
- use tracing_futures::Instrument;
38
-
39
- use crate::worker::workflow::{
40
- ActivationCompleteResult, ActivationOrAuto, FailRunUpdate, FulfillableActivationComplete,
41
- GoodRunUpdate, LocalActivityRequestSink, RunAction, RunUpdateResponseKind,
42
- };
43
- use temporal_sdk_core_protos::TaskToken;
44
-
45
- use crate::abstractions::dbg_panic;
46
- #[cfg(test)]
47
- pub(crate) use managed_wf_test::ManagedWFFunc;
48
46
 
49
47
  type Result<T, E = WFMachinesError> = std::result::Result<T, E>;
50
- /// What percentage of a WFT timeout we are willing to wait before sending a WFT heartbeat when
51
- /// necessary.
52
- const WFT_HEARTBEAT_TIMEOUT_FRACTION: f32 = 0.8;
48
+ pub(super) type RunUpdateAct = Option<ActivationOrAuto>;
53
49
 
50
+ /// Manages access to a specific workflow run. Everything inside is entirely synchronous and should
51
+ /// remain that way.
52
+ #[derive(derive_more::DebugCustom)]
53
+ #[debug(
54
+ fmt = "ManagedRun {{ wft: {:?}, activation: {:?}, buffered_resp: {:?} \
55
+ trying_to_evict: {} }}",
56
+ wft,
57
+ activation,
58
+ buffered_resp,
59
+ "trying_to_evict.is_some()"
60
+ )]
54
61
  pub(super) struct ManagedRun {
55
62
  wfm: WorkflowManager,
56
- update_tx: UnboundedSender<RunUpdateResponse>,
57
- local_activity_request_sink: LocalActivityRequestSink,
63
+ /// Called when the machines need to produce local activity requests. This can't be lifted up
64
+ /// easily as return values, because sometimes local activity requests trigger immediate
65
+ /// resolutions (ex: too many attempts). Thus lifting it up creates a lot of unneeded complexity
66
+ /// pushing things out and then directly back in. The downside is this is the only "impure" part
67
+ /// of the in/out nature of workflow state management. If there's ever a sensible way to lift it
68
+ /// up, that'd be nice.
69
+ local_activity_request_sink: Rc<dyn LocalActivityRequestSink>,
70
+ /// Set if the run is currently waiting on the execution of some local activities.
58
71
  waiting_on_la: Option<WaitingOnLAs>,
59
- // Is set to true if the machines encounter an error and the only subsequent thing we should
60
- // do is be evicted.
72
+ /// Is set to true if the machines encounter an error and the only subsequent thing we should
73
+ /// do is be evicted.
61
74
  am_broken: bool,
62
- }
75
+ /// If set, the WFT this run is currently/will be processing.
76
+ wft: Option<OutstandingTask>,
77
+ /// An outstanding activation to lang
78
+ activation: Option<OutstandingActivation>,
79
+ /// If set, it indicates there is a buffered poll response from the server that applies to this
80
+ /// run. This can happen when lang takes too long to complete a task and the task times out, for
81
+ /// example. Upon next completion, the buffered response will be removed and can be made ready
82
+ /// to be returned from polling
83
+ buffered_resp: Option<PermittedWFT>,
84
+ /// Is set if an eviction has been requested for this run
85
+ trying_to_evict: Option<RequestEvictMsg>,
63
86
 
64
- /// If an activation completion needed to wait on LA completions (or heartbeat timeout) we use
65
- /// this struct to store the data we need to finish the completion once that has happened
66
- struct WaitingOnLAs {
67
- wft_timeout: Duration,
68
- /// If set, we are waiting for LAs to complete as part of a just-finished workflow activation.
69
- /// If unset, we already had a heartbeat timeout and got a new WFT without any new work while
70
- /// there are still incomplete LAs.
71
- completion_dat: Option<(
72
- CompletionDataForWFT,
73
- oneshot::Sender<ActivationCompleteResult>,
74
- )>,
75
- hb_chan: UnboundedSender<Span>,
76
- heartbeat_timeout_task: JoinHandle<()>,
87
+ /// We track if we have recorded useful debugging values onto a certain span yet, to overcome
88
+ /// duplicating field values. Remove this once https://github.com/tokio-rs/tracing/issues/2334
89
+ /// is fixed.
90
+ recorded_span_ids: HashSet<tracing::Id>,
91
+ metrics: MetricsContext,
92
+ /// We store the paginator used for our own run's history fetching
93
+ paginator: Option<HistoryPaginator>,
94
+ completion_waiting_on_page_fetch: Option<RunActivationCompletion>,
77
95
  }
78
-
79
- #[derive(Debug)]
80
- struct CompletionDataForWFT {
81
- task_token: TaskToken,
82
- query_responses: Vec<QueryResult>,
83
- has_pending_query: bool,
84
- activation_was_only_eviction: bool,
85
- }
86
-
87
96
  impl ManagedRun {
88
97
  pub(super) fn new(
89
- wfm: WorkflowManager,
90
- update_tx: UnboundedSender<RunUpdateResponse>,
91
- local_activity_request_sink: LocalActivityRequestSink,
98
+ basics: RunBasics,
99
+ local_activity_request_sink: Rc<dyn LocalActivityRequestSink>,
92
100
  ) -> Self {
101
+ let metrics = basics.metrics.clone();
102
+ let wfm = WorkflowManager::new(basics);
93
103
  Self {
94
104
  wfm,
95
- update_tx,
96
105
  local_activity_request_sink,
97
106
  waiting_on_la: None,
98
107
  am_broken: false,
108
+ wft: None,
109
+ activation: None,
110
+ buffered_resp: None,
111
+ trying_to_evict: None,
112
+ recorded_span_ids: Default::default(),
113
+ metrics,
114
+ paginator: None,
115
+ completion_waiting_on_page_fetch: None,
99
116
  }
100
117
  }
101
118
 
102
- pub(super) async fn run(self, run_actions_rx: UnboundedReceiver<RunAction>) {
103
- let (heartbeat_tx, heartbeat_rx) = unbounded_channel();
104
- stream::select(
105
- UnboundedReceiverStream::new(run_actions_rx),
106
- UnboundedReceiverStream::new(heartbeat_rx).map(|trace_span| RunAction {
107
- action: RunActions::HeartbeatTimeout,
108
- trace_span,
109
- }),
110
- )
111
- .fold((self, heartbeat_tx), |(mut me, heartbeat_tx), action| {
112
- let span = action.trace_span;
113
- let action = action.action;
114
- let mut no_wft = false;
115
- async move {
116
- let res = match action {
117
- RunActions::NewIncomingWFT(wft) => me
118
- .incoming_wft(wft)
119
- .await
120
- .map(RunActionOutcome::AfterNewWFT),
121
- RunActions::ActivationCompletion(completion) => me
122
- .completion(completion, &heartbeat_tx)
123
- .await
124
- .map(RunActionOutcome::AfterCompletion),
125
- RunActions::CheckMoreWork {
126
- want_to_evict,
127
- has_pending_queries,
128
- has_wft,
129
- } => {
130
- if !has_wft {
131
- no_wft = true;
132
- }
133
- me.check_more_work(want_to_evict, has_pending_queries, has_wft)
134
- .await
135
- .map(RunActionOutcome::AfterCheckWork)
136
- }
137
- RunActions::LocalResolution(r) => me
138
- .local_resolution(r)
139
- .await
140
- .map(RunActionOutcome::AfterLocalResolution),
141
- RunActions::HeartbeatTimeout => {
142
- let maybe_act = if me.heartbeat_timeout() {
143
- Some(ActivationOrAuto::Autocomplete {
144
- run_id: me.wfm.machines.run_id.clone(),
145
- })
146
- } else {
147
- None
148
- };
149
- Ok(RunActionOutcome::AfterHeartbeatTimeout(maybe_act))
150
- }
151
- };
152
- match res {
153
- Ok(outcome) => {
154
- me.send_update_response(outcome, no_wft);
155
- }
156
- Err(e) => {
157
- error!(error=?e, "Error in run machines");
158
- me.am_broken = true;
159
- me.update_tx
160
- .send(RunUpdateResponse {
161
- kind: RunUpdateResponseKind::Fail(FailRunUpdate {
162
- run_id: me.wfm.machines.run_id.clone(),
163
- err: e.source,
164
- completion_resp: e.complete_resp_chan,
165
- }),
166
- span: Span::current(),
167
- })
168
- .expect("Machine can send update");
169
- }
170
- }
171
- (me, heartbeat_tx)
172
- }
173
- .instrument(span)
174
- })
175
- .await;
119
+ /// Returns true if there are pending jobs that need to be sent to lang.
120
+ pub(super) fn more_pending_work(&self) -> bool {
121
+ // We don't want to consider there to be more local-only work to be done if there is
122
+ // no workflow task associated with the run right now. This can happen if, ex, we
123
+ // complete a local activity while waiting for server to send us the next WFT.
124
+ // Activating lang would be harmful at this stage, as there might be work returned
125
+ // in that next WFT which should be part of the next activation.
126
+ self.wft.is_some() && self.wfm.machines.has_pending_jobs()
127
+ }
128
+
129
+ pub(super) fn have_seen_terminal_event(&self) -> bool {
130
+ self.wfm.machines.have_seen_terminal_event
131
+ }
132
+
133
+ /// Returns a ref to info about the currently tracked workflow task, if any.
134
+ pub(super) fn wft(&self) -> Option<&OutstandingTask> {
135
+ self.wft.as_ref()
136
+ }
137
+
138
+ /// Returns a ref to info about the currently tracked workflow activation, if any.
139
+ pub(super) fn activation(&self) -> Option<&OutstandingActivation> {
140
+ self.activation.as_ref()
141
+ }
142
+
143
+ /// Returns true if this run has already been told it will be evicted.
144
+ pub(super) fn is_trying_to_evict(&self) -> bool {
145
+ self.trying_to_evict.is_some()
146
+ }
147
+
148
+ /// Called whenever a new workflow task is obtained for this run
149
+ pub(super) fn incoming_wft(&mut self, pwft: PermittedWFT) -> RunUpdateAct {
150
+ let res = self._incoming_wft(pwft);
151
+ self.update_to_acts(res.map(Into::into), true)
176
152
  }
177
153
 
178
- async fn incoming_wft(
154
+ fn _incoming_wft(
179
155
  &mut self,
180
- wft: NewIncomingWFT,
156
+ pwft: PermittedWFT,
181
157
  ) -> Result<Option<ActivationOrAuto>, RunUpdateErr> {
182
- let activation = if let Some(h) = wft.history_update {
183
- self.wfm.feed_history_from_server(h).await?
158
+ if self.wft.is_some() {
159
+ dbg_panic!("Trying to send a new WFT for a run which already has one!");
160
+ }
161
+ let start_time = Instant::now();
162
+
163
+ let work = pwft.work;
164
+ let did_miss_cache = !work.is_incremental() || !work.update.is_real();
165
+ debug!(
166
+ run_id = %work.execution.run_id,
167
+ task_token = %&work.task_token,
168
+ update = ?work.update,
169
+ has_legacy_query = %work.legacy_query.is_some(),
170
+ attempt = %work.attempt,
171
+ "Applying new workflow task from server"
172
+ );
173
+ let wft_info = WorkflowTaskInfo {
174
+ attempt: work.attempt,
175
+ task_token: work.task_token,
176
+ wf_id: work.execution.workflow_id.clone(),
177
+ };
178
+
179
+ let legacy_query_from_poll = work
180
+ .legacy_query
181
+ .map(|q| query_to_job(LEGACY_QUERY_ID.to_string(), q));
182
+
183
+ let mut pending_queries = work.query_requests;
184
+ if !pending_queries.is_empty() && legacy_query_from_poll.is_some() {
185
+ error!(
186
+ "Server issued both normal and legacy queries. This should not happen. Please \
187
+ file a bug report."
188
+ );
189
+ return Err(RunUpdateErr {
190
+ source: WFMachinesError::Fatal(
191
+ "Server issued both normal and legacy query".to_string(),
192
+ ),
193
+ complete_resp_chan: None,
194
+ });
195
+ }
196
+ if let Some(lq) = legacy_query_from_poll {
197
+ pending_queries.push(lq);
198
+ }
199
+
200
+ self.paginator = Some(pwft.paginator);
201
+ self.wft = Some(OutstandingTask {
202
+ info: wft_info,
203
+ hit_cache: !did_miss_cache,
204
+ pending_queries,
205
+ start_time,
206
+ permit: pwft.permit,
207
+ });
208
+
209
+ // The update field is only populated in the event we hit the cache
210
+ let activation = if work.update.is_real() {
211
+ self.metrics.sticky_cache_hit();
212
+ self.wfm.feed_history_from_server(work.update)?
184
213
  } else {
185
- let r = self.wfm.get_next_activation().await?;
214
+ let r = self.wfm.get_next_activation()?;
186
215
  if r.jobs.is_empty() {
187
216
  return Err(RunUpdateErr {
188
217
  source: WFMachinesError::Fatal(format!(
@@ -197,16 +226,17 @@ impl ManagedRun {
197
226
 
198
227
  if activation.jobs.is_empty() {
199
228
  if self.wfm.machines.outstanding_local_activity_count() > 0 {
200
- // If the activation has no jobs but there are outstanding LAs, we need to restart the
201
- // WFT heartbeat.
229
+ // If the activation has no jobs but there are outstanding LAs, we need to restart
230
+ // the WFT heartbeat.
202
231
  if let Some(ref mut lawait) = self.waiting_on_la {
203
232
  if lawait.completion_dat.is_some() {
204
233
  panic!("Should not have completion dat when getting new wft & empty jobs")
205
234
  }
206
- lawait.heartbeat_timeout_task.abort();
207
- lawait.heartbeat_timeout_task = start_heartbeat_timeout_task(
208
- lawait.hb_chan.clone(),
209
- wft.start_time,
235
+ lawait.hb_timeout_handle.abort();
236
+ lawait.hb_timeout_handle = sink_heartbeat_timeout_start(
237
+ self.wfm.machines.run_id.clone(),
238
+ self.local_activity_request_sink.as_ref(),
239
+ start_time,
210
240
  lawait.wft_timeout,
211
241
  );
212
242
  // No activation needs to be sent to lang. We just need to wait for another
@@ -228,109 +258,70 @@ impl ManagedRun {
228
258
  Ok(Some(ActivationOrAuto::LangActivation(activation)))
229
259
  }
230
260
 
231
- async fn completion(
261
+ /// Deletes the currently tracked WFT & records latency metrics. Should be called after it has
262
+ /// been responded to (server has been told). Returns the WFT if there was one.
263
+ pub(super) fn mark_wft_complete(
232
264
  &mut self,
233
- mut completion: RunActivationCompletion,
234
- heartbeat_tx: &UnboundedSender<Span>,
235
- ) -> Result<Option<FulfillableActivationComplete>, RunUpdateErr> {
236
- let resp_chan = completion
237
- .resp_chan
238
- .take()
239
- .expect("Completion response channel must be populated");
240
-
241
- let outcome = async move {
242
- // Send commands from lang into the machines then check if the workflow run
243
- // needs another activation and mark it if so
244
- self.wfm.push_commands(completion.commands).await?;
245
- // Don't bother applying the next task if we're evicting at the end of
246
- // this activation
247
- if !completion.activation_was_eviction {
248
- self.wfm.apply_next_task_if_ready().await?;
249
- }
250
- let new_local_acts = self.wfm.drain_queued_local_activities();
265
+ report_status: WFTReportStatus,
266
+ ) -> Option<OutstandingTask> {
267
+ debug!("Marking WFT completed");
268
+ let retme = self.wft.take();
251
269
 
252
- let immediate_resolutions = (self.local_activity_request_sink)(new_local_acts);
253
- for resolution in immediate_resolutions {
254
- self.wfm
255
- .notify_of_local_result(LocalResolution::LocalActivity(resolution))?;
256
- }
257
-
258
- let data = CompletionDataForWFT {
259
- task_token: completion.task_token,
260
- query_responses: completion.query_responses,
261
- has_pending_query: completion.has_pending_query,
262
- activation_was_only_eviction: completion.activation_was_only_eviction,
263
- };
264
- if self.wfm.machines.outstanding_local_activity_count() == 0 {
265
- Ok((None, data, self))
266
- } else {
267
- let wft_timeout: Duration = self
268
- .wfm
269
- .machines
270
- .get_started_info()
271
- .and_then(|attrs| attrs.workflow_task_timeout)
272
- .ok_or_else(|| {
273
- WFMachinesError::Fatal(
274
- "Workflow's start attribs were missing a well formed task timeout"
275
- .to_string(),
276
- )
277
- })?;
278
- let heartbeat_tx = heartbeat_tx.clone();
279
- Ok((
280
- Some((heartbeat_tx, completion.start_time, wft_timeout)),
281
- data,
282
- self,
283
- ))
270
+ // Only record latency metrics if we genuinely reported to server
271
+ if matches!(report_status, WFTReportStatus::Reported) {
272
+ if let Some(ot) = &retme {
273
+ self.metrics.wf_task_latency(ot.start_time.elapsed());
284
274
  }
275
+ // Tell the LA manager that we're done with the WFT
276
+ self.local_activity_request_sink.sink_reqs(vec![
277
+ LocalActRequest::IndicateWorkflowTaskCompleted(self.wfm.machines.run_id.clone()),
278
+ ]);
285
279
  }
286
- .await;
287
280
 
288
- match outcome {
289
- Ok((None, data, me)) => Ok(Some(me.prepare_complete_resp(resp_chan, data, false))),
290
- Ok((Some((chan, start_t, wft_timeout)), data, me)) => {
291
- if let Some(wola) = me.waiting_on_la.as_mut() {
292
- wola.heartbeat_timeout_task.abort();
293
- }
294
- me.waiting_on_la = Some(WaitingOnLAs {
295
- wft_timeout,
296
- completion_dat: Some((data, resp_chan)),
297
- hb_chan: chan.clone(),
298
- heartbeat_timeout_task: start_heartbeat_timeout_task(
299
- chan,
300
- start_t,
301
- wft_timeout,
302
- ),
303
- });
304
- Ok(None)
305
- }
306
- Err(e) => Err(RunUpdateErr {
307
- source: e,
308
- complete_resp_chan: Some(resp_chan),
309
- }),
310
- }
281
+ retme
311
282
  }
312
283
 
313
- async fn check_more_work(
314
- &mut self,
315
- want_to_evict: Option<RequestEvictMsg>,
316
- has_pending_queries: bool,
317
- has_wft: bool,
318
- ) -> Result<Option<ActivationOrAuto>, RunUpdateErr> {
319
- if !has_wft {
320
- // It doesn't make sense to do work unless we have a WFT
284
+ /// Checks if any further activations need to go out for this run and produces them if so.
285
+ pub(super) fn check_more_activations(&mut self) -> RunUpdateAct {
286
+ let res = self._check_more_activations();
287
+ self.update_to_acts(res.map(Into::into), false)
288
+ }
289
+
290
+ fn _check_more_activations(&mut self) -> Result<Option<ActivationOrAuto>, RunUpdateErr> {
291
+ // No point in checking for more activations if there's already an outstanding activation.
292
+ if self.activation.is_some() {
321
293
  return Ok(None);
322
294
  }
295
+ // In the event it's time to evict this run, cancel any outstanding LAs
296
+ if self.trying_to_evict.is_some() {
297
+ self.sink_la_requests(vec![LocalActRequest::CancelAllInRun(
298
+ self.wfm.machines.run_id.clone(),
299
+ )])?;
300
+ }
301
+
302
+ if self.wft.is_none() {
303
+ // It doesn't make sense to do workflow work unless we have a WFT
304
+ return Ok(None);
305
+ }
306
+
323
307
  if self.wfm.machines.has_pending_jobs() && !self.am_broken {
324
308
  Ok(Some(ActivationOrAuto::LangActivation(
325
- self.wfm.get_next_activation().await?,
309
+ self.wfm.get_next_activation()?,
326
310
  )))
327
311
  } else {
328
- if has_pending_queries && !self.am_broken {
329
- return Ok(Some(ActivationOrAuto::ReadyForQueries(
330
- self.wfm.machines.get_wf_activation(),
331
- )));
312
+ if !self.am_broken {
313
+ let has_pending_queries = self
314
+ .wft
315
+ .as_ref()
316
+ .map(|wft| !wft.pending_queries.is_empty())
317
+ .unwrap_or_default();
318
+ if has_pending_queries {
319
+ return Ok(Some(ActivationOrAuto::ReadyForQueries(
320
+ self.wfm.machines.get_wf_activation(),
321
+ )));
322
+ }
332
323
  }
333
- if let Some(wte) = want_to_evict {
324
+ if let Some(wte) = self.trying_to_evict.clone() {
334
325
  let mut act = self.wfm.machines.get_wf_activation();
335
326
  // No other jobs make any sense to send if we encountered an error.
336
327
  if self.am_broken {
@@ -347,53 +338,306 @@ impl ManagedRun {
347
338
  }
348
339
  }
349
340
 
350
- fn prepare_complete_resp(
341
+ /// Called whenever lang successfully completes a workflow activation. Commands produced by the
342
+ /// activation are passed in. `resp_chan` will be used to unblock the completion call when
343
+ /// everything we need to do to fulfill it has happened.
344
+ ///
345
+ /// Can return an error in the event that another page of history needs to be fetched before
346
+ /// the completion can proceed.
347
+ pub(super) fn successful_completion(
351
348
  &mut self,
352
- resp_chan: oneshot::Sender<ActivationCompleteResult>,
353
- data: CompletionDataForWFT,
354
- due_to_heartbeat_timeout: bool,
355
- ) -> FulfillableActivationComplete {
356
- let outgoing_cmds = self.wfm.get_server_commands();
357
- let query_responses = data.query_responses;
358
- let has_query_responses = !query_responses.is_empty();
359
- let is_query_playback = data.has_pending_query && !has_query_responses;
349
+ mut commands: Vec<WFCommand>,
350
+ used_flags: Vec<u32>,
351
+ resp_chan: Option<oneshot::Sender<ActivationCompleteResult>>,
352
+ ) -> Result<RunUpdateAct, NextPageReq> {
353
+ let activation_was_only_eviction = self.activation_has_only_eviction();
354
+ let (task_token, has_pending_query, start_time) = if let Some(entry) = self.wft.as_ref() {
355
+ (
356
+ entry.info.task_token.clone(),
357
+ !entry.pending_queries.is_empty(),
358
+ entry.start_time,
359
+ )
360
+ } else {
361
+ if !activation_was_only_eviction {
362
+ // Not an error if this was an eviction, since it's normal to issue eviction
363
+ // activations without an associated workflow task in that case.
364
+ dbg_panic!(
365
+ "Attempted to complete activation for run {} without associated workflow task",
366
+ self.run_id()
367
+ );
368
+ }
369
+ self.reply_to_complete(ActivationCompleteOutcome::DoNothing, resp_chan);
370
+ return Ok(None);
371
+ };
360
372
 
361
- // We only actually want to send commands back to the server if there are no more
362
- // pending activations and we are caught up on replay. We don't want to complete a wft
363
- // if we already saw the final event in the workflow, or if we are playing back for the
364
- // express purpose of fulfilling a query. If the activation we sent was *only* an
365
- // eviction, and there were no commands produced during iteration, don't send that
366
- // either.
367
- let no_commands_and_evicting =
368
- outgoing_cmds.commands.is_empty() && data.activation_was_only_eviction;
369
- let to_be_sent = ServerCommandsWithWorkflowInfo {
370
- task_token: data.task_token,
371
- action: ActivationAction::WftComplete {
372
- force_new_wft: due_to_heartbeat_timeout,
373
- commands: outgoing_cmds.commands,
373
+ // If the only command from the activation is a legacy query response, that means we need
374
+ // to respond differently than a typical activation.
375
+ if matches!(&commands.as_slice(),
376
+ &[WFCommand::QueryResponse(qr)] if qr.query_id == LEGACY_QUERY_ID)
377
+ {
378
+ let qr = match commands.remove(0) {
379
+ WFCommand::QueryResponse(qr) => qr,
380
+ _ => unreachable!("We just verified this is the only command"),
381
+ };
382
+ self.reply_to_complete(
383
+ ActivationCompleteOutcome::ReportWFTSuccess(ServerCommandsWithWorkflowInfo {
384
+ task_token,
385
+ action: ActivationAction::RespondLegacyQuery {
386
+ result: Box::new(qr),
387
+ },
388
+ }),
389
+ resp_chan,
390
+ );
391
+ Ok(None)
392
+ } else {
393
+ // First strip out query responses from other commands that actually affect machines
394
+ // Would be prettier with `drain_filter`
395
+ let mut i = 0;
396
+ let mut query_responses = vec![];
397
+ while i < commands.len() {
398
+ if matches!(commands[i], WFCommand::QueryResponse(_)) {
399
+ if let WFCommand::QueryResponse(qr) = commands.remove(i) {
400
+ query_responses.push(qr);
401
+ }
402
+ } else {
403
+ i += 1;
404
+ }
405
+ }
406
+
407
+ if activation_was_only_eviction && !commands.is_empty() {
408
+ dbg_panic!("Reply to an eviction only containing an eviction included commands");
409
+ }
410
+
411
+ let rac = RunActivationCompletion {
412
+ task_token,
413
+ start_time,
414
+ commands,
415
+ activation_was_eviction: self.activation_has_eviction(),
416
+ activation_was_only_eviction,
417
+ has_pending_query,
374
418
  query_responses,
375
- },
419
+ used_flags,
420
+ resp_chan,
421
+ };
422
+
423
+ // Verify we can actually apply the next workflow task, which will happen as part of
424
+ // applying the completion to machines. If we can't, return early indicating we need
425
+ // to fetch a page.
426
+ if !self.wfm.ready_to_apply_next_wft() {
427
+ return if let Some(paginator) = self.paginator.take() {
428
+ debug!("Need to fetch a history page before next WFT can be applied");
429
+ self.completion_waiting_on_page_fetch = Some(rac);
430
+ Err(NextPageReq {
431
+ paginator,
432
+ span: Span::current(),
433
+ })
434
+ } else {
435
+ Ok(self.update_to_acts(
436
+ Err(RunUpdateErr {
437
+ source: WFMachinesError::Fatal(
438
+ "Run's paginator was absent when attempting to fetch next history \
439
+ page. This is a Core SDK bug."
440
+ .to_string(),
441
+ ),
442
+ complete_resp_chan: rac.resp_chan,
443
+ }),
444
+ false,
445
+ ))
446
+ };
447
+ }
448
+
449
+ Ok(self.process_completion(rac))
450
+ }
451
+ }
452
+
453
+ /// Called after the higher-up machinery has fetched more pages of event history needed to apply
454
+ /// the next workflow task. The history update and paginator used to perform the fetch are
455
+ /// passed in, with the update being used to apply the task, and the paginator stored to be
456
+ /// attached with another fetch request if needed.
457
+ pub(super) fn fetched_page_completion(
458
+ &mut self,
459
+ update: HistoryUpdate,
460
+ paginator: HistoryPaginator,
461
+ ) -> RunUpdateAct {
462
+ let res = self._fetched_page_completion(update, paginator);
463
+ self.update_to_acts(res.map(Into::into), false)
464
+ }
465
+ fn _fetched_page_completion(
466
+ &mut self,
467
+ update: HistoryUpdate,
468
+ paginator: HistoryPaginator,
469
+ ) -> Result<Option<FulfillableActivationComplete>, RunUpdateErr> {
470
+ self.paginator = Some(paginator);
471
+ if let Some(d) = self.completion_waiting_on_page_fetch.take() {
472
+ self._process_completion(d, Some(update))
473
+ } else {
474
+ dbg_panic!(
475
+ "Shouldn't be possible to be applying a next-page-fetch update when \
476
+ doing anything other than completing an activation."
477
+ );
478
+ Err(RunUpdateErr::from(WFMachinesError::Fatal(
479
+ "Tried to apply next-page-fetch update to a run that wasn't handling a completion"
480
+ .to_string(),
481
+ )))
482
+ }
483
+ }
484
+
485
+ /// Called whenever either core lang cannot complete a workflow activation. EX: Nondeterminism
486
+ /// or user code threw/panicked, respectively. The `cause` and `reason` fields are determined
487
+ /// inside core always. The `failure` field may come from lang. `resp_chan` will be used to
488
+ /// unblock the completion call when everything we need to do to fulfill it has happened.
489
+ pub(super) fn failed_completion(
490
+ &mut self,
491
+ cause: WorkflowTaskFailedCause,
492
+ reason: EvictionReason,
493
+ failure: workflow_completion::Failure,
494
+ resp_chan: Option<oneshot::Sender<ActivationCompleteResult>>,
495
+ ) -> RunUpdateAct {
496
+ let tt = if let Some(tt) = self.wft.as_ref().map(|t| t.info.task_token.clone()) {
497
+ tt
498
+ } else {
499
+ dbg_panic!(
500
+ "No workflow task for run id {} found when trying to fail activation",
501
+ self.run_id()
502
+ );
503
+ self.reply_to_complete(ActivationCompleteOutcome::DoNothing, resp_chan);
504
+ return None;
376
505
  };
377
506
 
378
- let should_respond = !(self.wfm.machines.has_pending_jobs()
379
- || outgoing_cmds.replaying
380
- || is_query_playback
381
- || no_commands_and_evicting);
382
- let outcome = if should_respond || has_query_responses {
383
- ActivationCompleteOutcome::ReportWFTSuccess(to_be_sent)
507
+ self.metrics.wf_task_failed();
508
+ let message = format!("Workflow activation completion failed: {:?}", &failure);
509
+ // Blow up any cached data associated with the workflow
510
+ let evict_req_outcome = self.request_eviction(RequestEvictMsg {
511
+ run_id: self.run_id().to_string(),
512
+ message,
513
+ reason,
514
+ });
515
+ let should_report = match &evict_req_outcome {
516
+ EvictionRequestResult::EvictionRequested(Some(attempt), _)
517
+ | EvictionRequestResult::EvictionAlreadyRequested(Some(attempt)) => *attempt <= 1,
518
+ _ => false,
519
+ };
520
+ let rur = evict_req_outcome.into_run_update_resp();
521
+ // If the outstanding WFT is a legacy query task, report that we need to fail it
522
+ let outcome = if self.pending_work_is_legacy_query() {
523
+ ActivationCompleteOutcome::ReportWFTFail(
524
+ FailedActivationWFTReport::ReportLegacyQueryFailure(tt, failure),
525
+ )
526
+ } else if should_report {
527
+ ActivationCompleteOutcome::ReportWFTFail(FailedActivationWFTReport::Report(
528
+ tt, cause, failure,
529
+ ))
384
530
  } else {
385
- ActivationCompleteOutcome::DoNothing
531
+ ActivationCompleteOutcome::WFTFailedDontReport
386
532
  };
387
- FulfillableActivationComplete {
388
- result: ActivationCompleteResult {
389
- most_recently_processed_event: self.wfm.machines.last_processed_event as usize,
390
- outcome,
391
- },
392
- resp_chan,
533
+ self.reply_to_complete(outcome, resp_chan);
534
+ rur
535
+ }
536
+
537
+ /// Delete the currently tracked workflow activation and return it, if any. Should be called
538
+ /// after the processing of the activation completion, and WFT reporting.
539
+ pub(super) fn delete_activation(&mut self) -> Option<OutstandingActivation> {
540
+ self.activation.take()
541
+ }
542
+
543
+ /// Called when local activities resolve
544
+ pub(super) fn local_resolution(&mut self, res: LocalResolution) -> RunUpdateAct {
545
+ let res = self._local_resolution(res);
546
+ self.update_to_acts(res.map(Into::into), false)
547
+ }
548
+
549
+ fn process_completion(&mut self, completion: RunActivationCompletion) -> RunUpdateAct {
550
+ let res = self._process_completion(completion, None);
551
+ self.update_to_acts(res.map(Into::into), false)
552
+ }
553
+
554
+ fn _process_completion(
555
+ &mut self,
556
+ completion: RunActivationCompletion,
557
+ new_update: Option<HistoryUpdate>,
558
+ ) -> Result<Option<FulfillableActivationComplete>, RunUpdateErr> {
559
+ let data = CompletionDataForWFT {
560
+ task_token: completion.task_token,
561
+ query_responses: completion.query_responses,
562
+ has_pending_query: completion.has_pending_query,
563
+ activation_was_only_eviction: completion.activation_was_only_eviction,
564
+ };
565
+
566
+ self.wfm.machines.add_lang_used_flags(completion.used_flags);
567
+
568
+ // If this is just bookkeeping after a reply to an only-eviction activation, we can bypass
569
+ // everything, since there is no reason to continue trying to update machines.
570
+ if completion.activation_was_only_eviction {
571
+ return Ok(Some(self.prepare_complete_resp(
572
+ completion.resp_chan,
573
+ data,
574
+ false,
575
+ )));
576
+ }
577
+
578
+ let outcome = (|| {
579
+ // Send commands from lang into the machines then check if the workflow run needs
580
+ // another activation and mark it if so
581
+ self.wfm.push_commands_and_iterate(completion.commands)?;
582
+ // If there was a new update included as part of the completion, apply it.
583
+ if let Some(update) = new_update {
584
+ self.wfm.feed_history_from_new_page(update)?;
585
+ }
586
+ // Don't bother applying the next task if we're evicting at the end of this activation
587
+ if !completion.activation_was_eviction {
588
+ self.wfm.apply_next_task_if_ready()?;
589
+ }
590
+ let new_local_acts = self.wfm.drain_queued_local_activities();
591
+ self.sink_la_requests(new_local_acts)?;
592
+
593
+ if self.wfm.machines.outstanding_local_activity_count() == 0 {
594
+ Ok(None)
595
+ } else {
596
+ let wft_timeout: Duration = self
597
+ .wfm
598
+ .machines
599
+ .get_started_info()
600
+ .and_then(|attrs| attrs.workflow_task_timeout)
601
+ .ok_or_else(|| {
602
+ WFMachinesError::Fatal(
603
+ "Workflow's start attribs were missing a well formed task timeout"
604
+ .to_string(),
605
+ )
606
+ })?;
607
+ Ok(Some((completion.start_time, wft_timeout)))
608
+ }
609
+ })();
610
+
611
+ match outcome {
612
+ Ok(None) => Ok(Some(self.prepare_complete_resp(
613
+ completion.resp_chan,
614
+ data,
615
+ false,
616
+ ))),
617
+ Ok(Some((start_t, wft_timeout))) => {
618
+ if let Some(wola) = self.waiting_on_la.as_mut() {
619
+ wola.hb_timeout_handle.abort();
620
+ }
621
+ self.waiting_on_la = Some(WaitingOnLAs {
622
+ wft_timeout,
623
+ completion_dat: Some((data, completion.resp_chan)),
624
+ hb_timeout_handle: sink_heartbeat_timeout_start(
625
+ self.run_id().to_string(),
626
+ self.local_activity_request_sink.as_ref(),
627
+ start_t,
628
+ wft_timeout,
629
+ ),
630
+ });
631
+ Ok(None)
632
+ }
633
+ Err(e) => Err(RunUpdateErr {
634
+ source: e,
635
+ complete_resp_chan: completion.resp_chan,
636
+ }),
393
637
  }
394
638
  }
395
639
 
396
- async fn local_resolution(
640
+ fn _local_resolution(
397
641
  &mut self,
398
642
  res: LocalResolution,
399
643
  ) -> Result<Option<FulfillableActivationComplete>, RunUpdateErr> {
@@ -402,7 +646,7 @@ impl ManagedRun {
402
646
  if self.wfm.machines.outstanding_local_activity_count() == 0 {
403
647
  if let Some(mut wait_dat) = self.waiting_on_la.take() {
404
648
  // Cancel the heartbeat timeout
405
- wait_dat.heartbeat_timeout_task.abort();
649
+ wait_dat.hb_timeout_handle.abort();
406
650
  if let Some((completion_dat, resp_chan)) = wait_dat.completion_dat.take() {
407
651
  return Ok(Some(self.prepare_complete_resp(
408
652
  resp_chan,
@@ -415,13 +659,23 @@ impl ManagedRun {
415
659
  Ok(None)
416
660
  }
417
661
 
662
+ pub(super) fn heartbeat_timeout(&mut self) -> RunUpdateAct {
663
+ let maybe_act = if self._heartbeat_timeout() {
664
+ Some(ActivationOrAuto::Autocomplete {
665
+ run_id: self.wfm.machines.run_id.clone(),
666
+ })
667
+ } else {
668
+ None
669
+ };
670
+ self.update_to_acts(Ok(maybe_act).map(Into::into), false)
671
+ }
418
672
  /// Returns `true` if autocompletion should be issued, which will actually cause us to end up
419
673
  /// in [completion] again, at which point we'll start a new heartbeat timeout, which will
420
674
  /// immediately trigger and thus finish the completion, forcing a new task as it should.
421
- fn heartbeat_timeout(&mut self) -> bool {
675
+ fn _heartbeat_timeout(&mut self) -> bool {
422
676
  if let Some(ref mut wait_dat) = self.waiting_on_la {
423
677
  // Cancel the heartbeat timeout
424
- wait_dat.heartbeat_timeout_task.abort();
678
+ wait_dat.hb_timeout_handle.abort();
425
679
  if let Some((completion_dat, resp_chan)) = wait_dat.completion_dat.take() {
426
680
  let compl = self.prepare_complete_resp(resp_chan, completion_dat, true);
427
681
  // Immediately fulfill the completion since the run update will already have
@@ -431,93 +685,454 @@ impl ManagedRun {
431
685
  // Auto-reply WFT complete
432
686
  return true;
433
687
  }
434
- } else {
435
- // If a heartbeat timeout happened, we should always have been waiting on LAs
436
- dbg_panic!("WFT heartbeat timeout fired but we were not waiting on any LAs");
437
688
  }
438
689
  false
439
690
  }
440
691
 
441
- fn send_update_response(&self, outcome: RunActionOutcome, no_wft: bool) {
442
- let mut in_response_to_wft = false;
443
- let (outgoing_activation, fulfillable_complete) = match outcome {
444
- RunActionOutcome::AfterNewWFT(a) => {
445
- in_response_to_wft = true;
446
- (a, None)
692
+ /// Returns true if the managed run has any form of pending work
693
+ /// If `ignore_evicts` is true, pending evictions do not count as pending work.
694
+ /// If `ignore_buffered` is true, buffered workflow tasks do not count as pending work.
695
+ pub(super) fn has_any_pending_work(&self, ignore_evicts: bool, ignore_buffered: bool) -> bool {
696
+ let evict_work = if ignore_evicts {
697
+ false
698
+ } else {
699
+ self.trying_to_evict.is_some()
700
+ };
701
+ let act_work = if ignore_evicts {
702
+ if let Some(ref act) = self.activation {
703
+ !act.has_only_eviction()
704
+ } else {
705
+ false
447
706
  }
448
- RunActionOutcome::AfterCheckWork(a) => (a, None),
449
- RunActionOutcome::AfterLocalResolution(f) => (None, f),
450
- RunActionOutcome::AfterCompletion(f) => (None, f),
451
- RunActionOutcome::AfterHeartbeatTimeout(a) => (a, None),
707
+ } else {
708
+ self.activation.is_some()
452
709
  };
453
- let mut more_pending_work = self.wfm.machines.has_pending_jobs();
454
- // We don't want to consider there to be more local-only work to be done if there is no
455
- // workflow task associated with the run right now. This can happen if, ex, we complete
456
- // a local activity while waiting for server to send us the next WFT. Activating lang would
457
- // be harmful at this stage, as there might be work returned in that next WFT which should
458
- // be part of the next activation.
459
- if no_wft {
460
- more_pending_work = false;
461
- }
462
- self.update_tx
463
- .send(RunUpdateResponse {
464
- kind: RunUpdateResponseKind::Good(GoodRunUpdate {
465
- run_id: self.wfm.machines.run_id.clone(),
466
- outgoing_activation,
467
- fulfillable_complete,
468
- have_seen_terminal_event: self.wfm.machines.have_seen_terminal_event,
469
- more_pending_work,
470
- most_recently_processed_event_number: self.wfm.machines.last_processed_event
471
- as usize,
472
- in_response_to_wft,
473
- }),
474
- span: Span::current(),
710
+ let buffered = if ignore_buffered {
711
+ false
712
+ } else {
713
+ self.buffered_resp.is_some()
714
+ };
715
+ trace!(wft=self.wft.is_some(), buffered=?buffered, more_work=?self.more_pending_work(),
716
+ act_work, evict_work, "Does run have pending work?");
717
+ self.wft.is_some() || buffered || self.more_pending_work() || act_work || evict_work
718
+ }
719
+
720
+ /// Stores some work if there is any outstanding WFT or activation for the run. If there was
721
+ /// not, returns the work back out inside the option.
722
+ pub(super) fn buffer_wft_if_outstanding_work(
723
+ &mut self,
724
+ work: PermittedWFT,
725
+ ) -> Option<PermittedWFT> {
726
+ let about_to_issue_evict = self.trying_to_evict.is_some();
727
+ let has_wft = self.wft().is_some();
728
+ let has_activation = self.activation().is_some();
729
+ if has_wft || has_activation || about_to_issue_evict || self.more_pending_work() {
730
+ debug!(run_id = %self.run_id(),
731
+ "Got new WFT for a run with outstanding work, buffering it");
732
+ self.buffered_resp = Some(work);
733
+ None
734
+ } else {
735
+ Some(work)
736
+ }
737
+ }
738
+
739
+ /// Returns true if there is a buffered workflow task for this run.
740
+ pub(super) fn has_buffered_wft(&self) -> bool {
741
+ self.buffered_resp.is_some()
742
+ }
743
+
744
+ /// Removes and returns the buffered workflow task, if any.
745
+ pub(super) fn take_buffered_wft(&mut self) -> Option<PermittedWFT> {
746
+ self.buffered_resp.take()
747
+ }
748
+
749
+ pub(super) fn request_eviction(&mut self, info: RequestEvictMsg) -> EvictionRequestResult {
750
+ let attempts = self.wft.as_ref().map(|wt| wt.info.attempt);
751
+
752
+ // If we were waiting on a page fetch and we're getting evicted because fetching failed,
753
+ // then make sure we allow the completion to proceed, otherwise we're stuck waiting forever.
754
+ if self.completion_waiting_on_page_fetch.is_some()
755
+ && matches!(info.reason, EvictionReason::PaginationOrHistoryFetch)
756
+ {
757
+ // We just checked it is some, unwrap OK.
758
+ let c = self.completion_waiting_on_page_fetch.take().unwrap();
759
+ let run_upd = self.failed_completion(
760
+ WorkflowTaskFailedCause::Unspecified,
761
+ info.reason,
762
+ Failure::application_failure(info.message, false).into(),
763
+ c.resp_chan,
764
+ );
765
+ return EvictionRequestResult::EvictionRequested(attempts, run_upd);
766
+ }
767
+
768
+ if !self.activation_has_eviction() && self.trying_to_evict.is_none() {
769
+ debug!(run_id=%info.run_id, reason=%info.message, "Eviction requested");
770
+ self.trying_to_evict = Some(info);
771
+ EvictionRequestResult::EvictionRequested(attempts, self.check_more_activations())
772
+ } else {
773
+ EvictionRequestResult::EvictionAlreadyRequested(attempts)
774
+ }
775
+ }
776
+
777
+ pub(super) fn record_span_fields(&mut self, span: &Span) {
778
+ if let Some(spid) = span.id() {
779
+ if self.recorded_span_ids.contains(&spid) {
780
+ return;
781
+ }
782
+ self.recorded_span_ids.insert(spid);
783
+
784
+ if let Some(wid) = self.wft().map(|wft| &wft.info.wf_id) {
785
+ span.record("workflow_id", wid.as_str());
786
+ }
787
+ }
788
+ }
789
+
790
+ /// Take the result of some update to ourselves and turn it into a return value of zero or more
791
+ /// actions
792
+ fn update_to_acts(
793
+ &mut self,
794
+ outcome: Result<ActOrFulfill, RunUpdateErr>,
795
+ in_response_to_wft: bool,
796
+ ) -> RunUpdateAct {
797
+ match outcome {
798
+ Ok(act_or_fulfill) => {
799
+ let (mut maybe_act, maybe_fulfill) = match act_or_fulfill {
800
+ ActOrFulfill::OutgoingAct(a) => (a, None),
801
+ ActOrFulfill::FulfillableComplete(c) => (None, c),
802
+ };
803
+ // If there's no activation but is pending work, check and possibly generate one
804
+ if self.more_pending_work() && maybe_act.is_none() {
805
+ match self._check_more_activations() {
806
+ Ok(oa) => maybe_act = oa,
807
+ Err(e) => {
808
+ return self.update_to_acts(Err(e), in_response_to_wft);
809
+ }
810
+ }
811
+ }
812
+ let r = match maybe_act {
813
+ Some(ActivationOrAuto::LangActivation(mut activation)) => {
814
+ if in_response_to_wft {
815
+ let wft = self
816
+ .wft
817
+ .as_mut()
818
+ .expect("WFT must exist for run just updated with one");
819
+ // If there are in-poll queries, insert jobs for those queries into the
820
+ // activation, but only if we hit the cache. If we didn't, those queries
821
+ // will need to be dealt with once replay is over
822
+ if wft.hit_cache {
823
+ put_queries_in_act(&mut activation, wft);
824
+ }
825
+ }
826
+
827
+ if activation.jobs.is_empty() {
828
+ dbg_panic!("Should not send lang activation with no jobs");
829
+ }
830
+ Some(ActivationOrAuto::LangActivation(activation))
831
+ }
832
+ Some(ActivationOrAuto::ReadyForQueries(mut act)) => {
833
+ if let Some(wft) = self.wft.as_mut() {
834
+ put_queries_in_act(&mut act, wft);
835
+ Some(ActivationOrAuto::LangActivation(act))
836
+ } else {
837
+ dbg_panic!("Ready for queries but no WFT!");
838
+ None
839
+ }
840
+ }
841
+ a @ Some(
842
+ ActivationOrAuto::Autocomplete { .. } | ActivationOrAuto::AutoFail { .. },
843
+ ) => a,
844
+ None => {
845
+ if let Some(reason) = self.trying_to_evict.as_ref() {
846
+ // If we had nothing to do, but we're trying to evict, just do that now
847
+ // as long as there's no other outstanding work.
848
+ if self.activation.is_none() && !self.more_pending_work() {
849
+ let mut evict_act = create_evict_activation(
850
+ self.run_id().to_string(),
851
+ reason.message.clone(),
852
+ reason.reason,
853
+ );
854
+ evict_act.history_length =
855
+ self.most_recently_processed_event_number() as u32;
856
+ Some(ActivationOrAuto::LangActivation(evict_act))
857
+ } else {
858
+ None
859
+ }
860
+ } else {
861
+ None
862
+ }
863
+ }
864
+ };
865
+ if let Some(f) = maybe_fulfill {
866
+ f.fulfill();
867
+ }
868
+
869
+ match r {
870
+ // After each run update, check if it's ready to handle any buffered poll
871
+ None | Some(ActivationOrAuto::Autocomplete { .. })
872
+ if !self.has_any_pending_work(false, true) =>
873
+ {
874
+ if let Some(bufft) = self.buffered_resp.take() {
875
+ self.incoming_wft(bufft)
876
+ } else {
877
+ None
878
+ }
879
+ }
880
+ Some(r) => {
881
+ self.insert_outstanding_activation(&r);
882
+ Some(r)
883
+ }
884
+ None => None,
885
+ }
886
+ }
887
+ Err(fail) => {
888
+ self.am_broken = true;
889
+ let rur = if let Some(resp_chan) = fail.complete_resp_chan {
890
+ // Automatically fail the workflow task in the event we couldn't update machines
891
+ let fail_cause = if matches!(&fail.source, WFMachinesError::Nondeterminism(_)) {
892
+ WorkflowTaskFailedCause::NonDeterministicError
893
+ } else {
894
+ WorkflowTaskFailedCause::Unspecified
895
+ };
896
+ let wft_fail_str = format!("{:?}", fail.source);
897
+ self.failed_completion(
898
+ fail_cause,
899
+ fail.source.evict_reason(),
900
+ Failure::application_failure(wft_fail_str, false).into(),
901
+ Some(resp_chan),
902
+ )
903
+ } else {
904
+ warn!(error=?fail.source, "Error while updating workflow");
905
+ Some(ActivationOrAuto::AutoFail {
906
+ run_id: self.run_id().to_owned(),
907
+ machines_err: fail.source,
908
+ })
909
+ };
910
+ rur
911
+ }
912
+ }
913
+ }
914
+
915
+ fn insert_outstanding_activation(&mut self, act: &ActivationOrAuto) {
916
+ let act_type = match &act {
917
+ ActivationOrAuto::LangActivation(act) | ActivationOrAuto::ReadyForQueries(act) => {
918
+ if act.is_legacy_query() {
919
+ OutstandingActivation::LegacyQuery
920
+ } else {
921
+ OutstandingActivation::Normal {
922
+ contains_eviction: act.eviction_index().is_some(),
923
+ num_jobs: act.jobs.len(),
924
+ }
925
+ }
926
+ }
927
+ ActivationOrAuto::Autocomplete { .. } | ActivationOrAuto::AutoFail { .. } => {
928
+ OutstandingActivation::Autocomplete
929
+ }
930
+ };
931
+ if let Some(old_act) = self.activation {
932
+ // This is a panic because we have screwed up core logic if this is violated. It must be
933
+ // upheld.
934
+ panic!(
935
+ "Attempted to insert a new outstanding activation {act:?}, but there already was \
936
+ one outstanding: {old_act:?}"
937
+ );
938
+ }
939
+ self.activation = Some(act_type);
940
+ }
941
+
942
+ fn prepare_complete_resp(
943
+ &mut self,
944
+ resp_chan: Option<oneshot::Sender<ActivationCompleteResult>>,
945
+ data: CompletionDataForWFT,
946
+ due_to_heartbeat_timeout: bool,
947
+ ) -> FulfillableActivationComplete {
948
+ let mut outgoing_cmds = self.wfm.get_server_commands();
949
+ if data.activation_was_only_eviction && !outgoing_cmds.commands.is_empty() {
950
+ if self.am_broken {
951
+ // If we broke there could be commands in the pipe that we didn't get a chance to
952
+ // handle properly during replay, just wipe them all out.
953
+ outgoing_cmds.commands = vec![];
954
+ } else {
955
+ dbg_panic!(
956
+ "There should not be any outgoing commands when preparing a completion response \
957
+ if the activation was only an eviction. This is an SDK bug."
958
+ );
959
+ }
960
+ }
961
+
962
+ let query_responses = data.query_responses;
963
+ let has_query_responses = !query_responses.is_empty();
964
+ let is_query_playback = data.has_pending_query && !has_query_responses;
965
+ let mut force_new_wft = due_to_heartbeat_timeout;
966
+
967
+ // We only actually want to send commands back to the server if there are no more pending
968
+ // activations and we are caught up on replay. We don't want to complete a wft if we already
969
+ // saw the final event in the workflow, or if we are playing back for the express purpose of
970
+ // fulfilling a query. If the activation we sent was *only* an eviction, don't send that
971
+ // either.
972
+ let should_respond = !(self.wfm.machines.has_pending_jobs()
973
+ || outgoing_cmds.replaying
974
+ || is_query_playback
975
+ || data.activation_was_only_eviction);
976
+ // If there are pending LA resolutions, and we're responding to a query here,
977
+ // we want to make sure to force a new task, as otherwise once we tell lang about
978
+ // the LA resolution there wouldn't be any task to reply to with the result of iterating
979
+ // the workflow.
980
+ if has_query_responses && self.wfm.machines.has_pending_la_resolutions() {
981
+ force_new_wft = true;
982
+ }
983
+
984
+ let outcome = if should_respond || has_query_responses {
985
+ ActivationCompleteOutcome::ReportWFTSuccess(ServerCommandsWithWorkflowInfo {
986
+ task_token: data.task_token,
987
+ action: ActivationAction::WftComplete {
988
+ force_new_wft,
989
+ commands: outgoing_cmds.commands,
990
+ query_responses,
991
+ sdk_metadata: self.wfm.machines.get_metadata_for_wft_complete(),
992
+ },
475
993
  })
476
- .expect("Machine can send update");
994
+ } else {
995
+ ActivationCompleteOutcome::DoNothing
996
+ };
997
+ FulfillableActivationComplete {
998
+ result: ActivationCompleteResult {
999
+ most_recently_processed_event: self.wfm.machines.last_processed_event as usize,
1000
+ outcome,
1001
+ },
1002
+ resp_chan,
1003
+ }
1004
+ }
1005
+
1006
+ /// Pump some local activity requests into the sink, applying any immediate results to the
1007
+ /// workflow machines.
1008
+ fn sink_la_requests(
1009
+ &mut self,
1010
+ new_local_acts: Vec<LocalActRequest>,
1011
+ ) -> Result<(), WFMachinesError> {
1012
+ let immediate_resolutions = self.local_activity_request_sink.sink_reqs(new_local_acts);
1013
+ if !immediate_resolutions.is_empty() {
1014
+ warn!("Immediate res: {:?}", &immediate_resolutions);
1015
+ }
1016
+ for resolution in immediate_resolutions {
1017
+ self.wfm
1018
+ .notify_of_local_result(LocalResolution::LocalActivity(resolution))?;
1019
+ }
1020
+ Ok(())
1021
+ }
1022
+
1023
+ fn reply_to_complete(
1024
+ &self,
1025
+ outcome: ActivationCompleteOutcome,
1026
+ chan: Option<oneshot::Sender<ActivationCompleteResult>>,
1027
+ ) {
1028
+ if let Some(chan) = chan {
1029
+ chan.send(ActivationCompleteResult {
1030
+ most_recently_processed_event: self.most_recently_processed_event_number() as usize,
1031
+ outcome,
1032
+ })
1033
+ .expect("Rcv half of activation reply not dropped");
1034
+ }
1035
+ }
1036
+
1037
+ /// Returns true if the handle is currently processing a WFT which contains a legacy query.
1038
+ fn pending_work_is_legacy_query(&self) -> bool {
1039
+ // Either we know because there is a pending legacy query, or it's already been drained and
1040
+ // sent as an activation.
1041
+ matches!(self.activation, Some(OutstandingActivation::LegacyQuery))
1042
+ || self
1043
+ .wft
1044
+ .as_ref()
1045
+ .map(|t| t.has_pending_legacy_query())
1046
+ .unwrap_or_default()
1047
+ }
1048
+
1049
+ fn most_recently_processed_event_number(&self) -> i64 {
1050
+ self.wfm.machines.last_processed_event
1051
+ }
1052
+
1053
+ fn activation_has_eviction(&mut self) -> bool {
1054
+ self.activation
1055
+ .map(OutstandingActivation::has_eviction)
1056
+ .unwrap_or_default()
1057
+ }
1058
+
1059
+ fn activation_has_only_eviction(&mut self) -> bool {
1060
+ self.activation
1061
+ .map(OutstandingActivation::has_only_eviction)
1062
+ .unwrap_or_default()
1063
+ }
1064
+
1065
+ fn run_id(&self) -> &str {
1066
+ &self.wfm.machines.run_id
477
1067
  }
478
1068
  }
479
1069
 
480
- fn start_heartbeat_timeout_task(
481
- chan: UnboundedSender<Span>,
1070
+ /// Drains pending queries from the workflow task and appends them to the activation's jobs
1071
+ fn put_queries_in_act(act: &mut WorkflowActivation, wft: &mut OutstandingTask) {
1072
+ // Nothing to do if there are no pending queries
1073
+ if wft.pending_queries.is_empty() {
1074
+ return;
1075
+ }
1076
+
1077
+ let has_legacy = wft.has_pending_legacy_query();
1078
+ // Cannot dispatch legacy query if there are any other jobs - which can happen if, ex, a local
1079
+ // activity resolves while we've gotten a legacy query after heartbeating.
1080
+ if has_legacy && !act.jobs.is_empty() {
1081
+ return;
1082
+ }
1083
+
1084
+ debug!(queries=?wft.pending_queries, "Dispatching queries");
1085
+ let query_jobs = wft
1086
+ .pending_queries
1087
+ .drain(..)
1088
+ .map(|q| workflow_activation_job::Variant::QueryWorkflow(q).into());
1089
+ act.jobs.extend(query_jobs);
1090
+ }
1091
+ fn sink_heartbeat_timeout_start(
1092
+ run_id: String,
1093
+ sink: &dyn LocalActivityRequestSink,
482
1094
  wft_start_time: Instant,
483
1095
  wft_timeout: Duration,
484
- ) -> JoinHandle<()> {
1096
+ ) -> AbortHandle {
485
1097
  // The heartbeat deadline is 80% of the WFT timeout
486
- let wft_heartbeat_deadline =
487
- wft_start_time.add(wft_timeout.mul_f32(WFT_HEARTBEAT_TIMEOUT_FRACTION));
488
- task::spawn(async move {
489
- tokio::time::sleep_until(wft_heartbeat_deadline.into()).await;
490
- let _ = chan.send(Span::current());
491
- })
492
- }
493
-
494
- enum RunActionOutcome {
495
- AfterNewWFT(Option<ActivationOrAuto>),
496
- AfterCheckWork(Option<ActivationOrAuto>),
497
- AfterLocalResolution(Option<FulfillableActivationComplete>),
498
- AfterCompletion(Option<FulfillableActivationComplete>),
499
- AfterHeartbeatTimeout(Option<ActivationOrAuto>),
1098
+ let deadline = wft_start_time.add(wft_timeout.mul_f32(WFT_HEARTBEAT_TIMEOUT_FRACTION));
1099
+ let (abort_handle, abort_reg) = AbortHandle::new_pair();
1100
+ sink.sink_reqs(vec![LocalActRequest::StartHeartbeatTimeout {
1101
+ send_on_elapse: HeartbeatTimeoutMsg {
1102
+ run_id,
1103
+ span: Span::current(),
1104
+ },
1105
+ deadline,
1106
+ abort_reg,
1107
+ }]);
1108
+ abort_handle
500
1109
  }
501
1110
 
502
- #[derive(derive_more::DebugCustom)]
503
- #[debug(fmt = "RunUpdateErr({:?})", source)]
504
- struct RunUpdateErr {
505
- source: WFMachinesError,
506
- complete_resp_chan: Option<oneshot::Sender<ActivationCompleteResult>>,
1111
+ /// If an activation completion needed to wait on LA completions (or heartbeat timeout) we use
1112
+ /// this struct to store the data we need to finish the completion once that has happened
1113
+ struct WaitingOnLAs {
1114
+ wft_timeout: Duration,
1115
+ /// If set, we are waiting for LAs to complete as part of a just-finished workflow activation.
1116
+ /// If unset, we already had a heartbeat timeout and got a new WFT without any new work while
1117
+ /// there are still incomplete LAs.
1118
+ completion_dat: Option<(
1119
+ CompletionDataForWFT,
1120
+ Option<oneshot::Sender<ActivationCompleteResult>>,
1121
+ )>,
1122
+ /// Can be used to abort heartbeat timeouts
1123
+ hb_timeout_handle: AbortHandle,
507
1124
  }
508
-
509
- impl From<WFMachinesError> for RunUpdateErr {
510
- fn from(e: WFMachinesError) -> Self {
511
- RunUpdateErr {
512
- source: e,
513
- complete_resp_chan: None,
514
- }
515
- }
1125
+ #[derive(Debug)]
1126
+ struct CompletionDataForWFT {
1127
+ task_token: TaskToken,
1128
+ query_responses: Vec<QueryResult>,
1129
+ has_pending_query: bool,
1130
+ activation_was_only_eviction: bool,
516
1131
  }
517
1132
 
518
1133
  /// Manages an instance of a [WorkflowMachines], which is not thread-safe, as well as other data
519
1134
  /// associated with that specific workflow run.
520
- pub(crate) struct WorkflowManager {
1135
+ struct WorkflowManager {
521
1136
  machines: WorkflowMachines,
522
1137
  /// Is always `Some` in normal operation. Optional to allow for unit testing with the test
523
1138
  /// workflow driver, which does not need to complete activations the normal way.
@@ -527,24 +1142,9 @@ pub(crate) struct WorkflowManager {
527
1142
  impl WorkflowManager {
528
1143
  /// Create a new workflow manager given workflow history and execution info as would be found
529
1144
  /// in [PollWorkflowTaskQueueResponse]
530
- pub fn new(
531
- history: HistoryUpdate,
532
- namespace: String,
533
- workflow_id: String,
534
- workflow_type: String,
535
- run_id: String,
536
- metrics: MetricsContext,
537
- ) -> Self {
1145
+ fn new(basics: RunBasics) -> Self {
538
1146
  let (wfb, cmd_sink) = WorkflowBridge::new();
539
- let state_machines = WorkflowMachines::new(
540
- namespace,
541
- workflow_id,
542
- workflow_type,
543
- run_id,
544
- history,
545
- Box::new(wfb).into(),
546
- metrics,
547
- );
1147
+ let state_machines = WorkflowMachines::new(basics, Box::new(wfb).into());
548
1148
  Self {
549
1149
  machines: state_machines,
550
1150
  command_sink: Some(cmd_sink),
@@ -552,7 +1152,7 @@ impl WorkflowManager {
552
1152
  }
553
1153
 
554
1154
  #[cfg(test)]
555
- pub const fn new_from_machines(workflow_machines: WorkflowMachines) -> Self {
1155
+ const fn new_from_machines(workflow_machines: WorkflowMachines) -> Self {
556
1156
  Self {
557
1157
  machines: workflow_machines,
558
1158
  command_sink: None,
@@ -563,12 +1163,15 @@ impl WorkflowManager {
563
1163
  ///
564
1164
  /// Should only be called when a workflow has caught up on replay (or is just beginning). It
565
1165
  /// will return a workflow activation if one is needed.
566
- async fn feed_history_from_server(
567
- &mut self,
568
- update: HistoryUpdate,
569
- ) -> Result<WorkflowActivation> {
570
- self.machines.new_history_from_server(update).await?;
571
- self.get_next_activation().await
1166
+ fn feed_history_from_server(&mut self, update: HistoryUpdate) -> Result<WorkflowActivation> {
1167
+ self.machines.new_history_from_server(update)?;
1168
+ self.get_next_activation()
1169
+ }
1170
+
1171
+ /// Update the machines with some events from fetching another page of history. Does *not*
1172
+ /// attempt to pull the next activation, unlike [Self::feed_history_from_server].
1173
+ fn feed_history_from_new_page(&mut self, update: HistoryUpdate) -> Result<()> {
1174
+ self.machines.new_history_from_server(update)
572
1175
  }
573
1176
 
574
1177
  /// Let this workflow know that something we've been waiting locally on has resolved, like a
@@ -585,27 +1188,33 @@ impl WorkflowManager {
585
1188
  ///
586
1189
  /// Callers may also need to call [get_server_commands] after this to issue any pending commands
587
1190
  /// to the server.
588
- async fn get_next_activation(&mut self) -> Result<WorkflowActivation> {
1191
+ fn get_next_activation(&mut self) -> Result<WorkflowActivation> {
589
1192
  // First check if there are already some pending jobs, which can be a result of replay.
590
1193
  let activation = self.machines.get_wf_activation();
591
1194
  if !activation.jobs.is_empty() {
592
1195
  return Ok(activation);
593
1196
  }
594
1197
 
595
- self.machines.apply_next_wft_from_history().await?;
1198
+ self.machines.apply_next_wft_from_history()?;
596
1199
  Ok(self.machines.get_wf_activation())
597
1200
  }
598
1201
 
1202
+ /// Returns true if machines are ready to apply the next WFT sequence, false if events will need
1203
+ /// to be fetched in order to create a complete update with the entire next WFT sequence.
1204
+ pub(crate) fn ready_to_apply_next_wft(&self) -> bool {
1205
+ self.machines.ready_to_apply_next_wft()
1206
+ }
1207
+
599
1208
  /// If there are no pending jobs for the workflow, apply the next workflow task and check
600
1209
  /// again if there are any jobs. Importantly, does not *drain* jobs.
601
1210
  ///
602
1211
  /// Returns true if there are jobs (before or after applying the next WFT).
603
- async fn apply_next_task_if_ready(&mut self) -> Result<bool> {
1212
+ fn apply_next_task_if_ready(&mut self) -> Result<bool> {
604
1213
  if self.machines.has_pending_jobs() {
605
1214
  return Ok(true);
606
1215
  }
607
1216
  loop {
608
- let consumed_events = self.machines.apply_next_wft_from_history().await?;
1217
+ let consumed_events = self.machines.apply_next_wft_from_history()?;
609
1218
 
610
1219
  if consumed_events == 0 || !self.machines.replaying || self.machines.has_pending_jobs()
611
1220
  {
@@ -635,13 +1244,62 @@ impl WorkflowManager {
635
1244
 
636
1245
  /// Feed the workflow machines new commands issued by the executing workflow code, and iterate
637
1246
  /// the machines.
638
- async fn push_commands(&mut self, cmds: Vec<WFCommand>) -> Result<()> {
1247
+ fn push_commands_and_iterate(&mut self, cmds: Vec<WFCommand>) -> Result<()> {
639
1248
  if let Some(cs) = self.command_sink.as_mut() {
640
1249
  cs.send(cmds).map_err(|_| {
641
1250
  WFMachinesError::Fatal("Internal error buffering workflow commands".to_string())
642
1251
  })?;
643
1252
  }
644
- self.machines.iterate_machines().await?;
1253
+ self.machines.iterate_machines()?;
645
1254
  Ok(())
646
1255
  }
647
1256
  }
1257
+
1258
+ #[derive(Debug)]
1259
+ struct FulfillableActivationComplete {
1260
+ result: ActivationCompleteResult,
1261
+ resp_chan: Option<oneshot::Sender<ActivationCompleteResult>>,
1262
+ }
1263
+ impl FulfillableActivationComplete {
1264
+ fn fulfill(self) {
1265
+ if let Some(resp_chan) = self.resp_chan {
1266
+ let _ = resp_chan.send(self.result);
1267
+ }
1268
+ }
1269
+ }
1270
+
1271
+ #[derive(Debug)]
1272
+ struct RunActivationCompletion {
1273
+ task_token: TaskToken,
1274
+ start_time: Instant,
1275
+ commands: Vec<WFCommand>,
1276
+ activation_was_eviction: bool,
1277
+ activation_was_only_eviction: bool,
1278
+ has_pending_query: bool,
1279
+ query_responses: Vec<QueryResult>,
1280
+ used_flags: Vec<u32>,
1281
+ /// Used to notify the worker when the completion is done processing and the completion can
1282
+ /// unblock. Must always be `Some` when initialized.
1283
+ resp_chan: Option<oneshot::Sender<ActivationCompleteResult>>,
1284
+ }
1285
+ #[derive(Debug, derive_more::From)]
1286
+ enum ActOrFulfill {
1287
+ OutgoingAct(Option<ActivationOrAuto>),
1288
+ FulfillableComplete(Option<FulfillableActivationComplete>),
1289
+ }
1290
+
1291
+ #[derive(derive_more::DebugCustom)]
1292
+ #[debug(fmt = "RunUpdateErr({source:?})")]
1293
+ struct RunUpdateErr {
1294
+ source: WFMachinesError,
1295
+ complete_resp_chan: Option<oneshot::Sender<ActivationCompleteResult>>,
1296
+ }
1297
+
1298
+ impl From<WFMachinesError> for RunUpdateErr {
1299
+ fn from(e: WFMachinesError) -> Self {
1300
+ RunUpdateErr {
1301
+ source: e,
1302
+ complete_resp_chan: None,
1303
+ }
1304
+ }
1305
+ }