temporalio 0.4.0 → 0.6.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 (146) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/Cargo.lock +735 -479
  4. data/Cargo.toml +5 -5
  5. data/README.md +188 -39
  6. data/ext/Cargo.toml +3 -3
  7. data/lib/temporalio/activity/cancellation_details.rb +58 -0
  8. data/lib/temporalio/activity/context.rb +10 -1
  9. data/lib/temporalio/activity/definition.rb +41 -3
  10. data/lib/temporalio/activity/info.rb +25 -4
  11. data/lib/temporalio/activity.rb +2 -0
  12. data/lib/temporalio/api/activity/v1/message.rb +1 -1
  13. data/lib/temporalio/api/batch/v1/message.rb +7 -2
  14. data/lib/temporalio/api/cloud/account/v1/message.rb +1 -1
  15. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +22 -2
  16. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +2 -2
  17. data/lib/temporalio/api/cloud/connectivityrule/v1/message.rb +29 -0
  18. data/lib/temporalio/api/cloud/identity/v1/message.rb +7 -2
  19. data/lib/temporalio/api/cloud/namespace/v1/message.rb +7 -2
  20. data/lib/temporalio/api/cloud/nexus/v1/message.rb +3 -2
  21. data/lib/temporalio/api/cloud/operation/v1/message.rb +2 -2
  22. data/lib/temporalio/api/cloud/region/v1/message.rb +1 -1
  23. data/lib/temporalio/api/cloud/resource/v1/message.rb +1 -1
  24. data/lib/temporalio/api/cloud/sink/v1/message.rb +1 -1
  25. data/lib/temporalio/api/cloud/usage/v1/message.rb +1 -1
  26. data/lib/temporalio/api/command/v1/message.rb +2 -2
  27. data/lib/temporalio/api/common/v1/grpc_status.rb +1 -1
  28. data/lib/temporalio/api/common/v1/message.rb +4 -2
  29. data/lib/temporalio/api/deployment/v1/message.rb +3 -2
  30. data/lib/temporalio/api/enums/v1/batch_operation.rb +2 -2
  31. data/lib/temporalio/api/enums/v1/command_type.rb +1 -1
  32. data/lib/temporalio/api/enums/v1/common.rb +5 -2
  33. data/lib/temporalio/api/enums/v1/deployment.rb +3 -2
  34. data/lib/temporalio/api/enums/v1/event_type.rb +2 -2
  35. data/lib/temporalio/api/enums/v1/failed_cause.rb +2 -2
  36. data/lib/temporalio/api/enums/v1/namespace.rb +1 -1
  37. data/lib/temporalio/api/enums/v1/nexus.rb +1 -1
  38. data/lib/temporalio/api/enums/v1/query.rb +1 -1
  39. data/lib/temporalio/api/enums/v1/reset.rb +1 -1
  40. data/lib/temporalio/api/enums/v1/schedule.rb +1 -1
  41. data/lib/temporalio/api/enums/v1/task_queue.rb +3 -2
  42. data/lib/temporalio/api/enums/v1/update.rb +1 -1
  43. data/lib/temporalio/api/enums/v1/workflow.rb +2 -2
  44. data/lib/temporalio/api/errordetails/v1/message.rb +1 -1
  45. data/lib/temporalio/api/export/v1/message.rb +1 -1
  46. data/lib/temporalio/api/failure/v1/message.rb +3 -2
  47. data/lib/temporalio/api/filter/v1/message.rb +1 -1
  48. data/lib/temporalio/api/history/v1/message.rb +4 -2
  49. data/lib/temporalio/api/namespace/v1/message.rb +1 -1
  50. data/lib/temporalio/api/nexus/v1/message.rb +2 -2
  51. data/lib/temporalio/api/operatorservice/v1/request_response.rb +1 -1
  52. data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
  53. data/lib/temporalio/api/payload_visitor.rb +106 -1
  54. data/lib/temporalio/api/protocol/v1/message.rb +1 -1
  55. data/lib/temporalio/api/query/v1/message.rb +1 -1
  56. data/lib/temporalio/api/replication/v1/message.rb +1 -1
  57. data/lib/temporalio/api/rules/v1/message.rb +27 -0
  58. data/lib/temporalio/api/schedule/v1/message.rb +2 -2
  59. data/lib/temporalio/api/sdk/v1/enhanced_stack_trace.rb +1 -1
  60. data/lib/temporalio/api/sdk/v1/task_complete_metadata.rb +1 -1
  61. data/lib/temporalio/api/sdk/v1/user_metadata.rb +1 -1
  62. data/lib/temporalio/api/sdk/v1/worker_config.rb +23 -0
  63. data/lib/temporalio/api/sdk/v1/workflow_metadata.rb +1 -1
  64. data/lib/temporalio/api/taskqueue/v1/message.rb +6 -2
  65. data/lib/temporalio/api/testservice/v1/request_response.rb +1 -1
  66. data/lib/temporalio/api/testservice/v1/service.rb +1 -1
  67. data/lib/temporalio/api/update/v1/message.rb +1 -1
  68. data/lib/temporalio/api/version/v1/message.rb +1 -1
  69. data/lib/temporalio/api/worker/v1/message.rb +31 -0
  70. data/lib/temporalio/api/workflow/v1/message.rb +14 -2
  71. data/lib/temporalio/api/workflowservice/v1/request_response.rb +28 -2
  72. data/lib/temporalio/api/workflowservice/v1/service.rb +2 -2
  73. data/lib/temporalio/cancellation.rb +16 -12
  74. data/lib/temporalio/client/async_activity_handle.rb +12 -4
  75. data/lib/temporalio/client/connection/cloud_service.rb +135 -0
  76. data/lib/temporalio/client/connection/workflow_service.rb +150 -0
  77. data/lib/temporalio/client/connection.rb +2 -1
  78. data/lib/temporalio/client/interceptor.rb +25 -7
  79. data/lib/temporalio/client/schedule.rb +10 -2
  80. data/lib/temporalio/client/with_start_workflow_operation.rb +9 -1
  81. data/lib/temporalio/client/workflow_handle.rb +50 -10
  82. data/lib/temporalio/client/workflow_update_handle.rb +9 -3
  83. data/lib/temporalio/client.rb +110 -6
  84. data/lib/temporalio/common_enums.rb +14 -0
  85. data/lib/temporalio/contrib/open_telemetry.rb +7 -7
  86. data/lib/temporalio/converters/data_converter.rb +18 -8
  87. data/lib/temporalio/converters/failure_converter.rb +6 -3
  88. data/lib/temporalio/converters/payload_converter/binary_null.rb +2 -2
  89. data/lib/temporalio/converters/payload_converter/binary_plain.rb +2 -2
  90. data/lib/temporalio/converters/payload_converter/binary_protobuf.rb +2 -2
  91. data/lib/temporalio/converters/payload_converter/composite.rb +6 -4
  92. data/lib/temporalio/converters/payload_converter/encoding.rb +4 -2
  93. data/lib/temporalio/converters/payload_converter/json_plain.rb +24 -7
  94. data/lib/temporalio/converters/payload_converter/json_protobuf.rb +2 -2
  95. data/lib/temporalio/converters/payload_converter.rb +16 -6
  96. data/lib/temporalio/error/failure.rb +19 -1
  97. data/lib/temporalio/error.rb +1 -1
  98. data/lib/temporalio/internal/bridge/api/activity_result/activity_result.rb +1 -1
  99. data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +3 -2
  100. data/lib/temporalio/internal/bridge/api/child_workflow/child_workflow.rb +1 -1
  101. data/lib/temporalio/internal/bridge/api/common/common.rb +1 -1
  102. data/lib/temporalio/internal/bridge/api/core_interface.rb +1 -1
  103. data/lib/temporalio/internal/bridge/api/external_data/external_data.rb +1 -1
  104. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +3 -2
  105. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +2 -2
  106. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +3 -2
  107. data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +1 -1
  108. data/lib/temporalio/internal/bridge/worker.rb +28 -4
  109. data/lib/temporalio/internal/bridge.rb +1 -1
  110. data/lib/temporalio/internal/client/implementation.rb +60 -52
  111. data/lib/temporalio/internal/proto_utils.rb +4 -4
  112. data/lib/temporalio/internal/worker/activity_worker.rb +93 -20
  113. data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +8 -6
  114. data/lib/temporalio/internal/worker/workflow_instance/context.rb +66 -24
  115. data/lib/temporalio/internal/worker/workflow_instance/details.rb +5 -2
  116. data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +2 -2
  117. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +2 -0
  118. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +64 -18
  119. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +28 -14
  120. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +5 -2
  121. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +10 -4
  122. data/lib/temporalio/internal/worker/workflow_instance.rb +58 -23
  123. data/lib/temporalio/internal/worker/workflow_worker.rb +16 -6
  124. data/lib/temporalio/priority.rb +100 -0
  125. data/lib/temporalio/scoped_logger.rb +1 -1
  126. data/lib/temporalio/testing/activity_environment.rb +17 -2
  127. data/lib/temporalio/testing/workflow_environment.rb +3 -3
  128. data/lib/temporalio/version.rb +1 -1
  129. data/lib/temporalio/versioning_override.rb +56 -0
  130. data/lib/temporalio/worker/deployment_options.rb +45 -0
  131. data/lib/temporalio/worker/illegal_workflow_call_validator.rb +73 -0
  132. data/lib/temporalio/worker/interceptor.rb +13 -1
  133. data/lib/temporalio/worker/poller_behavior.rb +61 -0
  134. data/lib/temporalio/worker/thread_pool.rb +1 -1
  135. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +2 -1
  136. data/lib/temporalio/worker/workflow_replayer.rb +12 -13
  137. data/lib/temporalio/worker.rb +73 -28
  138. data/lib/temporalio/worker_deployment_version.rb +67 -0
  139. data/lib/temporalio/workflow/child_workflow_handle.rb +10 -2
  140. data/lib/temporalio/workflow/definition.rb +187 -39
  141. data/lib/temporalio/workflow/external_workflow_handle.rb +3 -1
  142. data/lib/temporalio/workflow/info.rb +4 -1
  143. data/lib/temporalio/workflow.rb +134 -11
  144. data/lib/temporalio.rb +1 -0
  145. data/temporalio.gemspec +1 -0
  146. metadata +14 -3
data/Cargo.toml CHANGED
@@ -13,13 +13,13 @@ license-file = "LICENSE"
13
13
 
14
14
  [workspace.dependencies]
15
15
  derive_builder = "0.20"
16
- derive_more = { version = "1.0", features = ["constructor", "display", "from", "into", "debug"] }
16
+ derive_more = { version = "2.0", features = ["constructor", "display", "from", "into", "debug", "try_into"] }
17
17
  thiserror = "2"
18
- tonic = "0.12"
19
- tonic-build = "0.12"
20
- opentelemetry = { version = "0.26", features = ["metrics"] }
18
+ tonic = "0.13"
19
+ tonic-build = "0.13"
20
+ opentelemetry = { version = "0.30", features = ["metrics"] }
21
21
  prost = "0.13"
22
22
  prost-types = "0.13"
23
23
 
24
24
  [workspace.lints.rust]
25
- unreachable_pub = "warn"
25
+ unreachable_pub = "warn"
data/README.md CHANGED
@@ -42,7 +42,8 @@ opinions. Please communicate with us on [Slack](https://t.mp/slack) in the `#rub
42
42
  - [Cloud Client Using mTLS](#cloud-client-using-mtls)
43
43
  - [Cloud Client Using API Key](#cloud-client-using-api-key)
44
44
  - [Data Conversion](#data-conversion)
45
- - [ActiveRecord and ActiveModel](#activerecord-and-activemodel)
45
+ - [ActiveModel](#activemodel)
46
+ - [Converter Hints](#converter-hints)
46
47
  - [Workers](#workers)
47
48
  - [Workflows](#workflows)
48
49
  - [Workflow Definition](#workflow-definition)
@@ -60,6 +61,9 @@ opinions. Please communicate with us on [Slack](https://t.mp/slack) in the `#rub
60
61
  - [Manual Time Skipping](#manual-time-skipping)
61
62
  - [Mocking Activities](#mocking-activities)
62
63
  - [Workflow Replay](#workflow-replay)
64
+ - [Advanced Workflow Safety and Escaping](#advanced-workflow-safety-and-escaping)
65
+ - [Durable Fiber Scheduler](#durable-fiber-scheduler)
66
+ - [Illegal Call Tracing](#illegal-call-tracing)
63
67
  - [Activities](#activities)
64
68
  - [Activity Definition](#activity-definition)
65
69
  - [Activity Context](#activity-context)
@@ -71,8 +75,13 @@ opinions. Please communicate with us on [Slack](https://t.mp/slack) in the `#rub
71
75
  - [Metrics](#metrics)
72
76
  - [OpenTelemetry Tracing](#opentelemetry-tracing)
73
77
  - [OpenTelemetry Tracing in Workflows](#opentelemetry-tracing-in-workflows)
78
+ - [Rails](#rails)
79
+ - [ActiveRecord](#activerecord)
80
+ - [Lazy/Eager Loading](#lazyeager-loading)
81
+ - [Forking](#forking)
74
82
  - [Ractors](#ractors)
75
83
  - [Platform Support](#platform-support)
84
+ - [Migration from Coinbase Ruby SDK](#migration-from-coinbase-ruby-sdk)
76
85
  - [Development](#development)
77
86
  - [Build](#build)
78
87
  - [Build Platform-specific Gem](#build-platform-specific-gem)
@@ -295,57 +304,62 @@ will be tried in order until one accepts (default falls through to the JSON one)
295
304
  `encoding` metadata value which is used to know which converter to use on deserialize. Custom encoding converters can be
296
305
  created, or even the entire payload converter can be replaced with a different implementation.
297
306
 
298
- ##### ActiveRecord and ActiveModel
307
+ **NOTE:** For ActiveRecord, or other general/ORM models that are used for a different purpose, it is not recommended to
308
+ try to reuse them as Temporal models. Eventually model purposes diverge and models for a Temporal workflows/activities
309
+ should be specific to their use for clarity and compatibility reasons. Also many Ruby ORMs do many lazy things and
310
+ therefore provide unclear serialization semantics. Instead, consider having models specific for workflows/activities and
311
+ translate to/from existing models as needed. See the next section on how to do this with ActiveModel objects.
299
312
 
300
- By default, `ActiveRecord` and `ActiveModel` objects do not natively support the `JSON` module. A mixin can be created
301
- to add this support for `ActiveRecord`, for example:
313
+ ##### ActiveModel
314
+
315
+ By default, ActiveModel objects do not natively support the `JSON` module. A mixin can be created to add this support
316
+ for ActiveRecord, for example:
302
317
 
303
318
  ```ruby
304
- module ActiveRecordJSONSupport
319
+ module ActiveModelJSONSupport
305
320
  extend ActiveSupport::Concern
306
321
  include ActiveModel::Serializers::JSON
307
322
 
308
323
  included do
324
+ def as_json(*)
325
+ super.merge(::JSON.create_id => self.class.name)
326
+ end
327
+
309
328
  def to_json(*args)
310
- hash = as_json
311
- hash[::JSON.create_id] = self.class.name
312
- hash.to_json(*args)
329
+ as_json.to_json(*args)
313
330
  end
314
331
 
315
332
  def self.json_create(object)
333
+ object = object.dup
316
334
  object.delete(::JSON.create_id)
317
- ret = new
318
- ret.attributes = object
319
- ret
335
+ new(**object.symbolize_keys)
320
336
  end
321
337
  end
322
338
  end
323
339
  ```
324
340
 
325
- Similarly, a mixin for `ActiveModel` that adds `attributes` accessors can leverage this same mixin, for example:
341
+ Now if `include ActiveModelJSONSupport` is present on any ActiveModel class, on serialization `to_json` will be used
342
+ which will use `as_json` which calls the super `as_json` but also includes the fully qualified class name as the JSON
343
+ `create_id` key. On deserialization, Ruby JSON then uses this key to know what class to call `json_create` on.
326
344
 
327
- ```ruby
328
- module ActiveModelJSONSupport
329
- extend ActiveSupport::Concern
330
- include ActiveRecordJSONSupport
345
+ ##### Converter Hints
331
346
 
332
- included do
333
- def attributes=(hash)
334
- hash.each do |key, value|
335
- send("#{key}=", value)
336
- end
337
- end
347
+ In most places where objects are converted to payloads or vice versa, a "hint" can be provided to tell the converter
348
+ something else about the object/payload to assist conversion. The default converters ignore these hints, but custom
349
+ converters can be written to take advantage of them. For example, hints may be used to provide a custom converter the
350
+ Ruby type to deserialize a payload into.
338
351
 
339
- def attributes
340
- instance_values
341
- end
342
- end
343
- end
344
- ```
352
+ These hints manifest themselves various ways throughout the API. The most obvious way is when making definitions. An
353
+ activity can define `activity_arg_hint` (which accepts multiple) and/or `activity_result_hint` for activity-level hints.
354
+ Similarly, a workflow can define `workflow_arg_hint` and/or `workflow_result_hint` for workflow-level hints.
355
+ `workflow_signal`, `workflow_query`, and `workflow_update` all similarly accept `arg_hints` and `result_hint` (except
356
+ signal of course). These definition-level hints are passed to converters both from the caller side and the
357
+ implementation side.
345
358
 
346
- Now `include ActiveRecordJSONSupport` or `include ActiveModelJSONSupport` will make the models work with Ruby `JSON`
347
- module and therefore Temporal. Of course any other approach to make the models work with the `JSON` module will work as
348
- well.
359
+ There are some advanced payload uses in the SDK that do not currently have a way to set hints. These include
360
+ workflow/schedule memo, workflow get/upsert memo, and application error details. In some cases, users can use
361
+ `Temporalio::Converters::RawValue` and then manually convert with hints. For others, hints can be added as needed,
362
+ please open an issue or otherwise contact Temporal.
349
363
 
350
364
  ### Workers
351
365
 
@@ -418,7 +432,7 @@ class GreetingWorkflow < Temporalio::Workflow::Definition
418
432
  # on wait_condition calls, so Cancellation object doesn't need to be passed
419
433
  # explicitly.
420
434
  Temporalio::Workflow.wait_condition { @greeting_params_update || @complete }
421
-
435
+
422
436
  # If there was an update, exchange and rerun. If it's _only_ a complete, finish
423
437
  # workflow with the greeting.
424
438
  if @greeting_params_update
@@ -490,7 +504,8 @@ definition/behavior of the method:
490
504
  side effects, meaning they should never mutate state or try to wait on anything.
491
505
 
492
506
  Workflows can be inherited, but subclass workflow-level decorators override superclass ones, and the same method can't
493
- be decorated with different handler types/names in the hierarchy.
507
+ be decorated with different handler types/names in the hierarchy. Workflow handlers (execute or any marked method)
508
+ cannot accept keyword arguments.
494
509
 
495
510
  #### Running Workflows
496
511
 
@@ -556,9 +571,7 @@ Some things to note about the above code:
556
571
 
557
572
  * A timer is represented by `Temporalio::Workflow.sleep`.
558
573
  * Timers are also started on `Temporalio::Workflow.timeout`.
559
- * _Technically_ `Kernel.sleep` and `Timeout.timeout` also delegate to the above calls, but the more explicit workflow
560
- forms are encouraged because they accept more options and are not subject to Ruby standard library implementation
561
- changes.
574
+ * `Kernel.sleep` and `Timeout.timeout` are considered illegal by default.
562
575
  * Each timer accepts a `Cancellation`, but if none is given, it defaults to `Temporalio::Workflow.cancellation`.
563
576
  * `Temporalio::Workflow.wait_condition` accepts a block that waits until the evaluated block result is truthy, then
564
577
  returns the value.
@@ -571,7 +584,10 @@ Some things to note about the above code:
571
584
  #### Workflow Fiber Scheduling and Cancellation
572
585
 
573
586
  Workflows are backed by a custom, deterministic `Fiber::Scheduler`. All fiber calls inside a workflow use this scheduler
574
- to ensure coroutines run deterministically.
587
+ to ensure coroutines run deterministically. Although this means that `Kernel.sleep` and `Mutex` and such should work and
588
+ since they are Fiber-aware, Temporal intentionally disables their use by default to prevent accidental use. See
589
+ "Workflow Logic Constraints" and "Advanced Workflow Safety and Escaping" for more details, and see "Workflow Utilities"
590
+ for alternatives.
575
591
 
576
592
  Every workflow contains a `Temporalio::Cancellation` at `Temporalio::Workflow.cancellation`. This is canceled when the
577
593
  workflow is canceled. For all workflow calls that accept a cancellation token, this is the default. So if a workflow is
@@ -663,6 +679,9 @@ from workflows including:
663
679
  nil key for dynamic). `[]=` or `store` can be called on these to update the handlers, though defined handlers are
664
680
  encouraged over runtime-set ones.
665
681
 
682
+ There are also classes for `Temporalio::Workflow::Mutex`, `Temporalio::Workflow::Queue`, and
683
+ `Temporalio::Workflow::SizedQueue` that are workflow-safe wrappers around the standard library forms.
684
+
666
685
  `Temporalio::Workflow::ContinueAsNewError` can be raised to continue-as-new the workflow. It accepts positional args and
667
686
  defaults the workflow to the same as the current, though it can be changed with the `workflow` kwarg. See API
668
687
  documentation for other details.
@@ -699,11 +718,16 @@ Ruby workflows. This means there are several things workflows cannot do such as:
699
718
 
700
719
  * Perform IO (network, disk, stdio, etc)
701
720
  * Access/alter external mutable state
702
- * Do any threading
721
+ * Do any threading or blocking calls
703
722
  * Do anything using the system clock (e.g. `Time.Now`)
704
723
  * Make any random calls
705
724
  * Make any not-guaranteed-deterministic calls
706
725
 
726
+ This means you can't even use logger calls outside of `Temporalio::Workflow.logger` because they use mutexes which may
727
+ be hit during periods of high-contention, but they are not completely disabled since users may do quick debugging with
728
+ them. See the [Advanced Workflow Safety and Escaping](#advanced-workflow-safety-and-escaping) section if needing to work
729
+ around this.
730
+
707
731
  #### Workflow Testing
708
732
 
709
733
  Workflow testing can be done in an integration-test fashion against a real server. However, it is hard to simulate
@@ -906,6 +930,44 @@ end
906
930
 
907
931
  See the `WorkflowReplayer` API documentation for more details.
908
932
 
933
+ #### Advanced Workflow Safety and Escaping
934
+
935
+ Workflows use a custom fiber scheduler to make fibers durable. There is also call tracing to prevent accidentally making
936
+ illegal workflow calls. But sometimes in advanced situations, workarounds may be needed. This section describes advanced
937
+ situations working with the workflow Fiber scheduler and illegal call tracer.
938
+
939
+ ##### Durable Fiber Scheduler
940
+
941
+ By default, Temporal considers `Logger`, `sleep`, `Timeout.timeout`, `Queue`, etc illegal. However, there are cases
942
+ where it may be desired for these to work locally inside a workflow such as for logging or other side-effecting,
943
+ known-non-deterministic aspects.
944
+
945
+ Users can pass a block to `Temporalio::Workflow::Unsafe.durable_scheduler_disabled` to not use the durable scheduler.
946
+ This should be used any time the scheduler needs to be bypassed, e.g. for local stdout. Not doing this can cause
947
+ workflows to get hung in high contention situations. For instance, if there is a logger (that isn't the safe-to-use
948
+ `Temporalio::Workflow.logger`) in a workflow, _technically_ Ruby surrounds the IO writes with a mutex and in extreme
949
+ high contention that mutex may durably block and then the workflow task may complete causing hung workflows because no
950
+ event comes to wake the mutex.
951
+
952
+ Also, by default anything that relies on IO wait that is not inside `durable_scheduler_disabled` will fail. It is
953
+ recommended to put things that need this in `durable_scheduler_disabled`, but if the durable scheduler is still needed
954
+ but IO wait is also needed, then a block passed to `Temporalio::Workflow::Unsafe.io_enabled` can be used.
955
+
956
+ Note `durable_scheduler_disabled` implies `illegal_call_tracing_disabled` (see next section). Many use of
957
+ `durable_scheduler_disabled`, such as for tracing or logging, often surround themselves in a
958
+ `unless Temporalio::Workflow.replaying?` block to make sure they don't duplicate the side effects on replay.
959
+
960
+ ##### Illegal Call Tracing
961
+
962
+ Ruby workflow threads employ a `TracePoint` to catch illegal calls such as `sleep` or `Time.now` or `Thread.new`. The
963
+ set of illegal calls can be configured via the `illegal_workflow_calls` parameter when creating a worker. The default
964
+ set is at `Temporalio::Worker.default_illegal_workflow_calls`.
965
+
966
+ When an illegal call is encountered, an exception is thrown. In advanced cases there may be a need to allow an illegal
967
+ call that is known to be used deterministically. This code can be in a block passed to
968
+ `Temporalio::Workflow::Unsafe.illegal_call_tracing_disabled`. If this has side-effecting behavior that needs to use the
969
+ non-durable scheduler, use `durable_scheduler_disabled` instead (which implies this, see previous section).
970
+
909
971
  ### Activities
910
972
 
911
973
  #### Activity Definition
@@ -955,6 +1017,7 @@ Some notes about activity definition:
955
1017
  * `workflow_raw_args` can be used to have activity arguments delivered to `execute` as
956
1018
  `Temporalio::Converters::RawValue`s. These are wrappers for the raw payloads that have not been converted to types
957
1019
  (but they have been decoded by the codec if present). They can be converted with `payload_converter` on the context.
1020
+ * Activities cannot accept keyword arguments.
958
1021
 
959
1022
  #### Activity Context
960
1023
 
@@ -1154,6 +1217,53 @@ workflow and time to run the activity attempt respectively), but the other spans
1154
1217
  are created in workflows and closed immediately since long-lived spans cannot work for durable software that may resume
1155
1218
  on other machines.
1156
1219
 
1220
+ ### Rails
1221
+
1222
+ Temporal Ruby SDK is a generic Ruby library that can work in any Ruby environment. However, there are some common
1223
+ conventions for Rails users to be aware of.
1224
+
1225
+ See the [rails_app](https://github.com/temporalio/samples-ruby/tree/main/rails_app) sample for an example of using
1226
+ Temporal from Rails.
1227
+
1228
+ #### ActiveRecord
1229
+
1230
+ For ActiveRecord, or other general/ORM models that are used for a different purpose, it is not recommended to
1231
+ try to reuse them as Temporal models. Eventually model purposes diverge and models for a Temporal workflows/activities
1232
+ should be specific to their use for clarity and compatibility reasons. Also many Ruby ORMs do many lazy things and
1233
+ therefore provide unclear serialization semantics. Instead, consider having models specific for workflows/activities and
1234
+ translate to/from existing models as needed. See the [ActiveModel](#activemodel) section on how to do this with
1235
+ ActiveModel objects.
1236
+
1237
+ #### Lazy/Eager Loading
1238
+
1239
+ By default, Rails
1240
+ [eagerly loads](https://guides.rubyonrails.org/v7.2/autoloading_and_reloading_constants.html#eager-loading) all
1241
+ application code on application start in production, but lazily loads it in non-production environments. Temporal
1242
+ workflows by default disallow use of IO during the workflow run. With lazy loading enabled in dev/test environments,
1243
+ when an activity class is referenced in a workflow before it has been explicitly `require`d, it can give an error like:
1244
+
1245
+ > Cannot access File path from inside a workflow. If this is known to be safe, the code can be run in a
1246
+ > Temporalio::Workflow::Unsafe.illegal_call_tracing_disabled block.
1247
+
1248
+ This comes from `bootsnap` via `zeitwork` because it is lazily loading a class/module at workflow runtime. It is not
1249
+ good to lazily load code durnig a workflow run because it can be side effecting. Workflows and the classes they
1250
+ reference should not be eagerly loaded.
1251
+
1252
+ To resolve this, either always eagerly load (e.g. `config.eager_load = true`) or explicitly `require` what is used by a
1253
+ workflow at the top of the file.
1254
+
1255
+ Note, this only affects non-production environments.
1256
+
1257
+ ### Forking
1258
+
1259
+ Objects created with the Temporal Ruby SDK cannot be used across forks. This includes runtimes, clients, and workers. By
1260
+ default, using `Client.connect` uses `Runtime.default` which is lazily created. If it was already created on the parent,
1261
+ an exception will occur when trying to reuse it to create clients or workers in a forked child. Similarly any RPC
1262
+ invocation or worker execution inside of a forked child separate from where the runtime or client or worker were created
1263
+ will raise an exception.
1264
+
1265
+ If forking must be used, make sure Temporal objects are only created _inside_ the fork.
1266
+
1157
1267
  ### Ractors
1158
1268
 
1159
1269
  It was an original goal to have workflows actually be Ractors for deterministic state isolation and have the library
@@ -1190,6 +1300,40 @@ section for how to build a the repository.
1190
1300
  The SDK works on Ruby 3.2+, but due to [an issue](https://github.com/temporalio/sdk-ruby/issues/162), fibers (and
1191
1301
  `async` gem) are only supported on Ruby versions 3.3 and newer.
1192
1302
 
1303
+ ### Migration from Coinbase Ruby SDK
1304
+
1305
+ The [Coinbase Ruby SDK](https://github.com/coinbase/temporal-ruby) predates this official Temporal SDK and has been a
1306
+ popular approach to developing in Temporal with Ruby. While Temporal encourages users to use the official SDK to get new
1307
+ features and support, this section covers differences from the Coinbase SDK to help those looking to migrate.
1308
+
1309
+ See [this Ruby sample](https://github.com/temporalio/samples-ruby/tree/main/coinbase_ruby) which demonstrates
1310
+ interoperability between Coinbase Ruby and Temporal Ruby clients, workflows, and activities. Specifically, it discusses
1311
+ how to disable API class loading on the Coinbase Ruby side if needing to use both dependencies in the same project,
1312
+ since two sets of API classes cannot both be present.
1313
+
1314
+ Migration cannot be done on a live, running workflow. Overall, Coinbase Ruby workflow events are incompatible with
1315
+ Temporal Ruby workflow events at runtime, so both SDK versions cannot have workers for the same task queue. A live
1316
+ workflow migration cannot occur, an separate task queue would be needed. However, Coinbase Ruby clients, workflows, and
1317
+ activities can be used with Temporal Ruby clients, workflows, and activities in either direction. Migrating from the
1318
+ Coinbase Ruby SDK to the Temporal Ruby SDK would be similar to migrating from Temporal Go SDK to Temporal Java SDK. You
1319
+ can interact across, but the workflow events are incompatible and therefore the task queues cannot be served by both at
1320
+ the same time.
1321
+
1322
+ Here is an overview of the primary differences between the SDKs:
1323
+
1324
+ | Feature | Coinbase Ruby | Temporal Ruby |
1325
+ | --- | --- | --- |
1326
+ | Base module | `Temporal::` | `Temporalio::` |
1327
+ | Client + start workflow | Global `Temporal.configure` + `Temporal.start_workflow` | `Temporalio::Client.connect` + `my_client.start_workflow` |
1328
+ | Client implementation | Ruby gRPC | Rust gRPC |
1329
+ | Activity definition | Extend `Temporal::Activity` + impl `execute` | Extend `Temporalio::Activity::Definition` + impl `execute` |
1330
+ | Workflow definition | Extend `Temporal::Workflow` + impl `execute` | Extend `Temporalio::Workflow::Definition` + impl `execute` |
1331
+ | Invoke activity from workflow | `MyActivity.execute!` or `workflow.execute_activity!(MyActivity)` | `Workflow.execute_activity(MyActivity)` |
1332
+ | Handle signal/query/update in workflow | `workflow.on_signal`/`workflow.on_query`/update-unsupported | Decorate with `workflow_signal`/`workflow_query`/`workflow_update` |
1333
+ | Run worker | `Temporal::Worker.new` + `start` | `Temporalio::Worker.new` + `run` |
1334
+
1335
+ This is just a high-level overview, there are many more differences on more specific Temporal components.
1336
+
1193
1337
  ## Development
1194
1338
 
1195
1339
  ### Build
@@ -1203,9 +1347,11 @@ Prerequisites:
1203
1347
 
1204
1348
  First, install dependencies:
1205
1349
 
1350
+ # Optional: Change bundler install path to be local
1351
+ bundle config --local path $(pwd)/.bundle
1206
1352
  bundle install
1207
1353
 
1208
- To build shared library for development use:
1354
+ To build shared library for development use (ensure you have cloned submodules) :
1209
1355
 
1210
1356
  bundle exec rake compile
1211
1357
 
@@ -1237,6 +1383,9 @@ the gem at `lib/temporalio/internal/bridge/3.2/temporalio_bridge.so` and
1237
1383
 
1238
1384
  ### Testing
1239
1385
 
1386
+ Note you can set `TEMPORAL_TEST_CLIENT_TARGET_HOST` and `TEMPORAL_TEST_CLIENT_TARGET_NAMESPACE`
1387
+ (optional, defaults to 'default') environment variables to use an existing server.
1388
+
1240
1389
  This project uses `minitest`. To test:
1241
1390
 
1242
1391
  bundle exec rake test
data/ext/Cargo.toml CHANGED
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "temporalio_bridge"
3
3
  version = "0.1.0"
4
- edition = "2021"
4
+ edition = "2024"
5
5
  authors = ["Chad Retz <chad@temporal.io>"]
6
6
  license = "MIT"
7
7
  publish = false
@@ -19,9 +19,9 @@ temporal-client = { version = "0.1.0", path = "./sdk-core/client" }
19
19
  temporal-sdk-core = { version = "0.1.0", path = "./sdk-core/core", features = ["ephemeral-server"] }
20
20
  temporal-sdk-core-api = { version = "0.1.0", path = "./sdk-core/core-api" }
21
21
  temporal-sdk-core-protos = { version = "0.1.0", path = "./sdk-core/sdk-core-protos" }
22
- tokio = "1.26"
22
+ tokio = "1.37"
23
23
  tokio-stream = "0.1"
24
24
  tokio-util = "0.7"
25
- tonic = "0.12"
25
+ tonic = "0.13"
26
26
  tracing = "0.1"
27
27
  url = "2.2"
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/error'
4
+
5
+ module Temporalio
6
+ module Activity
7
+ # Details that are set when an activity is cancelled. This is only valid at the time the cancel was received, the
8
+ # state may change on the server after it is received.
9
+ class CancellationDetails
10
+ def initialize(
11
+ gone_from_server: false,
12
+ cancel_requested: true,
13
+ timed_out: false,
14
+ worker_shutdown: false,
15
+ paused: false,
16
+ reset: false
17
+ )
18
+ @gone_from_server = gone_from_server
19
+ @cancel_requested = cancel_requested
20
+ @timed_out = timed_out
21
+ @worker_shutdown = worker_shutdown
22
+ @paused = paused
23
+ @reset = reset
24
+ end
25
+
26
+ # @return [Boolean] Whether the activity no longer exists on the server (may already be completed or its workflow
27
+ # may be completed).
28
+ def gone_from_server?
29
+ @gone_from_server
30
+ end
31
+
32
+ # @return [Boolean] Whether the activity was explicitly cancelled.
33
+ def cancel_requested?
34
+ @cancel_requested
35
+ end
36
+
37
+ # @return [Boolean] Whether the activity timeout caused activity to be marked cancelled.
38
+ def timed_out?
39
+ @timed_out
40
+ end
41
+
42
+ # @return [Boolean] Whether the worker the activity is running on is shutting down.
43
+ def worker_shutdown?
44
+ @worker_shutdown
45
+ end
46
+
47
+ # @return [Boolean] Whether the activity was explicitly paused.
48
+ def paused?
49
+ @paused
50
+ end
51
+
52
+ # @return [Boolean] Whether the activity was explicitly reset.
53
+ def reset?
54
+ @reset
55
+ end
56
+ end
57
+ end
58
+ end
@@ -61,7 +61,8 @@ module Temporalio
61
61
  # Users do not have to be concerned with burdening the server by calling this too frequently.
62
62
  #
63
63
  # @param details [Array<Object>] Details to record with the heartbeat.
64
- def heartbeat(*details)
64
+ # @param detail_hints [Array<Object>, nil] Hints to pass to converter.
65
+ def heartbeat(*details, detail_hints: nil)
65
66
  raise NotImplementedError
66
67
  end
67
68
 
@@ -70,6 +71,14 @@ module Temporalio
70
71
  raise NotImplementedError
71
72
  end
72
73
 
74
+ # @return [CancellationDetails, nil] Cancellation details if canceled. These are set just before cancellation is
75
+ # actually canceled. These details only represent when the cancel was first performed. Once set, this object is
76
+ # never mutated. Therefore, the situation on the server may have changed (e.g. unpause), but this still
77
+ # represents the values when cancellation first occurred for this attempt.
78
+ def cancellation_details
79
+ raise NotImplementedError
80
+ end
81
+
73
82
  # @return [Cancellation] Cancellation that is canceled when the worker is shutting down. On worker shutdown, this
74
83
  # is canceled, then the `graceful_shutdown_period` is waited (default 0s), then the activity is canceled.
75
84
  def worker_shutdown_cancellation
@@ -78,6 +78,21 @@ module Temporalio
78
78
 
79
79
  @activity_raw_args = value
80
80
  end
81
+
82
+ # Add activity hints to be passed to converter for activity args.
83
+ #
84
+ # @param hints [Array<Object>] Hints to add.
85
+ def activity_arg_hint(*hints)
86
+ @activity_arg_hints ||= []
87
+ @activity_arg_hints.concat(hints)
88
+ end
89
+
90
+ # Set activity result hint to be passed to converter for activity result.
91
+ #
92
+ # @param hint [Object] Hint to set.
93
+ def activity_result_hint(hint)
94
+ @activity_result_hint = hint
95
+ end
81
96
  end
82
97
 
83
98
  # @!visibility private
@@ -85,13 +100,20 @@ module Temporalio
85
100
  activity_name = @activity_name
86
101
  raise 'Cannot have activity name specified for dynamic activity' if activity_name && @activity_dynamic
87
102
 
103
+ # Disallow kwargs in execute parameters
104
+ if instance_method(:execute).parameters.any? { |t, _| t == :key || t == :keyreq }
105
+ raise 'Activity execute cannot have keyword arguments'
106
+ end
107
+
88
108
  # Default to unqualified class name if not dynamic
89
109
  activity_name ||= name.to_s.split('::').last unless @activity_dynamic
90
110
  {
91
111
  activity_name:,
92
112
  activity_executor: @activity_executor || :default,
93
113
  activity_cancel_raise: @activity_cancel_raise.nil? || @activity_cancel_raise,
94
- activity_raw_args: @activity_raw_args.nil? ? false : @activity_raw_args
114
+ activity_raw_args: @activity_raw_args.nil? ? false : @activity_raw_args,
115
+ activity_arg_hints: @activity_arg_hints,
116
+ activity_result_hint: @activity_result_hint
95
117
  }
96
118
  end
97
119
 
@@ -122,6 +144,12 @@ module Temporalio
122
144
  # @return [Boolean] Whether to use {Converters::RawValue}s as arguments.
123
145
  attr_reader :raw_args
124
146
 
147
+ # @return [Array<Object>, nil] Argument hints.
148
+ attr_reader :arg_hints
149
+
150
+ # @return [Object, nil] Result hint
151
+ attr_reader :result_hint
152
+
125
153
  # Obtain definition info representing the given activity, which can be a class, instance, or definition info.
126
154
  #
127
155
  # @param activity [Definition, Class<Definition>, Info] Activity to get info for.
@@ -142,7 +170,9 @@ module Temporalio
142
170
  instance: proc { activity.new },
143
171
  executor: details[:activity_executor],
144
172
  cancel_raise: details[:activity_cancel_raise],
145
- raw_args: details[:activity_raw_args]
173
+ raw_args: details[:activity_raw_args],
174
+ arg_hints: details[:activity_arg_hints],
175
+ result_hint: details[:activity_result_hint]
146
176
  ) { |*args| Context.current.instance&.execute(*args) }
147
177
  when Definition
148
178
  details = activity.class._activity_definition_details
@@ -151,7 +181,9 @@ module Temporalio
151
181
  instance: activity,
152
182
  executor: details[:activity_executor],
153
183
  cancel_raise: details[:activity_cancel_raise],
154
- raw_args: details[:activity_raw_args]
184
+ raw_args: details[:activity_raw_args],
185
+ arg_hints: details[:activity_arg_hints],
186
+ result_hint: details[:activity_result_hint]
155
187
  ) { |*args| Context.current.instance&.execute(*args) }
156
188
  when Info
157
189
  activity
@@ -167,6 +199,8 @@ module Temporalio
167
199
  # @param executor [Symbol] Name of the executor.
168
200
  # @param cancel_raise [Boolean] Whether to raise in thread/fiber on cancellation.
169
201
  # @param raw_args [Boolean] Whether to use {Converters::RawValue}s as arguments.
202
+ # @param arg_hints [Array<Object>, nil] Argument hints.
203
+ # @param result_hint [Object, nil] Result hint.
170
204
  # @yield Use this block as the activity.
171
205
  def initialize(
172
206
  name:,
@@ -174,6 +208,8 @@ module Temporalio
174
208
  executor: :default,
175
209
  cancel_raise: true,
176
210
  raw_args: false,
211
+ arg_hints: nil,
212
+ result_hint: nil,
177
213
  &block
178
214
  )
179
215
  @name = name
@@ -184,6 +220,8 @@ module Temporalio
184
220
  @executor = executor
185
221
  @cancel_raise = cancel_raise
186
222
  @raw_args = raw_args
223
+ @arg_hints = arg_hints
224
+ @result_hint = result_hint
187
225
  Internal::ProtoUtils.assert_non_reserved_name(name)
188
226
  end
189
227
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'temporalio/activity/context'
4
+ require 'temporalio/internal/proto_utils'
5
+
3
6
  module Temporalio
4
7
  module Activity
5
8
  Info = Data.define(
@@ -7,9 +10,10 @@ module Temporalio
7
10
  :activity_type,
8
11
  :attempt,
9
12
  :current_attempt_scheduled_time,
10
- :heartbeat_details,
11
13
  :heartbeat_timeout,
12
14
  :local?,
15
+ :priority,
16
+ :raw_heartbeat_details,
13
17
  :schedule_to_close_timeout,
14
18
  :scheduled_time,
15
19
  :start_to_close_timeout,
@@ -32,12 +36,15 @@ module Temporalio
32
36
  # @return [Integer] Attempt the activity is on.
33
37
  # @!attribute current_attempt_scheduled_time
34
38
  # @return [Time] When the current attempt was scheduled.
35
- # @!attribute heartbeat_details
36
- # @return [Array<Object>] Details from the last heartbeat of the last attempt.
37
39
  # @!attribute heartbeat_timeout
38
40
  # @return [Float, nil] Heartbeat timeout set by the caller.
39
41
  # @!attribute local?
40
42
  # @return [Boolean] Whether the activity is a local activity or not.
43
+ # @!attribute priority
44
+ # @return [Priority] The priority of this activity.
45
+ # @!attribute raw_heartbeat_details
46
+ # @return [Array<Converter::RawValue>] Raw details from the last heartbeat of the last attempt. Can use
47
+ # {heartbeat_details} to get lazily-converted values.
41
48
  # @!attribute schedule_to_close_timeout
42
49
  # @return [Float, nil] Schedule to close timeout set by the caller.
43
50
  # @!attribute scheduled_time
@@ -62,6 +69,20 @@ module Temporalio
62
69
  #
63
70
  # @note WARNING: This class may have required parameters added to its constructor. Users should not instantiate this
64
71
  # class or it may break in incompatible ways.
65
- class Info; end # rubocop:disable Lint/EmptyClass
72
+ class Info
73
+ # Convert raw heartbeat details into Ruby types.
74
+ #
75
+ # Note, this live-converts every invocation.
76
+ #
77
+ # @param hints [Array<Object>, nil] Hints, if any, to assist conversion.
78
+ # @return [Array<Object>] Converted details.
79
+ def heartbeat_details(hints: nil)
80
+ Internal::ProtoUtils.convert_from_payload_array(
81
+ Context.current.payload_converter,
82
+ raw_heartbeat_details.map(&:payload),
83
+ hints:
84
+ )
85
+ end
86
+ end
66
87
  end
67
88
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'temporalio/activity/cancellation_details'
3
4
  require 'temporalio/activity/complete_async_error'
4
5
  require 'temporalio/activity/context'
5
6
  require 'temporalio/activity/definition'
6
7
  require 'temporalio/activity/info'
8
+ require 'temporalio/priority'
7
9
 
8
10
  module Temporalio
9
11
  # All activity related classes.