Add Ruby SDK support#41
Conversation
Created complete Ruby reference documentation covering: - ruby.md: Overview, quick start, key concepts, file organization - patterns.md: Signals, queries, updates, child workflows, saga, cancellation, etc. - determinism.md: Illegal call tracing, safe alternatives table - determinism-protection.md: TracePoint, durable fiber scheduler, customization - versioning.md: Patching API, type versioning, worker versioning - testing.md: WorkflowEnvironment, mocking, replay, activity testing - error-handling.md: ApplicationError, retries, timeouts, workflow failure - data-handling.md: Data converter, ActiveModel, hints, search attributes - observability.md: Logging, metrics, best practices - gotchas.md: Common mistakes, illegal call tracing issues - advanced-features.md: Schedules, async completion, worker tuning, Rails Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Self-review fixes: - patterns.md: Remove non-existent `workflow_run` annotation; entry point is `def execute` (no annotation needed, unlike Python's @workflow.run) - patterns.md: Remove conflicting manual query methods that duplicated workflow_query_attr_reader - error-handling.md: Remove `await` keyword (doesn't exist in Ruby) - gotchas.md: Replace TS-style CancellationScope with Ruby's Temporalio::Cancellation token-based detached cancellation - data-handling.md: Replace homemade ActiveModel mixin with official SDK pattern using ActiveSupport::Concern + ActiveModel::Serializers::JSON - data-handling.md: Fix list_workflows call signature (positional, not kw) - ruby.md, gotchas.md: Fix require paths to use 'temporalio/activity' instead of 'temporalio/activity/definition' Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- patterns.md: Fix external workflow signal to use class method ref (TargetWorkflow.data_ready instead of TargetWorkflow, :data_ready) - patterns.md: Add ? suffix to all_handlers_finished (Ruby boolean convention) - ruby.md: Add 'default' namespace to Client.connect calls Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SKILL.md: Add "Temporal Ruby" trigger phrase to description - SKILL.md: Update Overview to list Ruby as supported language - SKILL.md: Add Ruby entry to Getting Started guide - core/determinism.md: Add Ruby SDK Protection Mechanism entry (Illegal Call Tracing via TracePoint + Durable Fiber Scheduler) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Thank you for doing this! As a ruby sdk user, very interested in this MR. Any update on ETA? |
|
Looks good! |
|
Yes please |
chris-olszewski
left a comment
There was a problem hiding this comment.
Overall looks good. Just a few suggestions
| - Subclass `Temporalio::Workflow::Definition` | ||
| - Define `def execute(args)` as the entry point | ||
| - Use `Temporalio::Workflow.execute_activity` to call activities | ||
| - Use signals, queries, and updates via class-level DSL methods |
There was a problem hiding this comment.
| - Use signals, queries, and updates via class-level DSL methods | |
| - Define signals, queries, and updates via class-level DSL methods |
|
|
||
| class ReplayTest < Minitest::Test | ||
| def test_replay_from_json | ||
| json = File.read('test/fixtures/my_workflow_history.json') |
There was a problem hiding this comment.
Would showing how to construct this JSON file be helpful?
|
|
||
| ## Cancellation (Token-based) | ||
|
|
||
| Ruby uses `Temporalio::Cancellation` tokens instead of asyncio cancellation. |
There was a problem hiding this comment.
asyncio seems to be in reference to Python
| Ruby uses `Temporalio::Cancellation` tokens instead of asyncio cancellation. | |
| Ruby uses `Temporalio::Cancellation` tokens. |
| @@ -0,0 +1,62 @@ | |||
| # Ruby SDK Observability | |||
There was a problem hiding this comment.
Might be worth documenting the OTEL interceptor we provide: https://github.com/temporalio/samples-ruby/blob/fc306b7c72157169ba22112758d58c6becd59eec/open_telemetry/worker.rb#L27
| The SDK provides a custom `Fiber::Scheduler` implementation that makes fiber primitives deterministic within the workflow. | ||
|
|
||
| - `Kernel.sleep` and `Mutex` technically work under the durable scheduler but are **disabled by default** via illegal call tracing to prevent accidental misuse. | ||
| - `Temporalio::Workflow.sleep` and `Temporalio::Workflow.wait_condition` are the correct alternatives. |
There was a problem hiding this comment.
We do offer Temporalio::Workflow::Mutex which is perfectly safe to use if a user truly wants a mutex and not a wait_condition
|
|
||
| ## Common Issues | ||
|
|
||
| ### Third-party gems triggering NondeterminismError |
There was a problem hiding this comment.
Should the first fix be to attempt to move the call into an activity? We should only be suggesting these escape hatches after there has been verification that the call will not introduce non-determinism.
|
|
||
| ```ruby | ||
| # In Puma config (puma.rb) | ||
| on_worker_boot do |
There was a problem hiding this comment.
This has been renamed in Puma 7.
| on_worker_boot do | |
| before_worker_boot do |
|
|
||
| Rails uses Zeitwerk for autoloading. Workflow and activity classes must be loadable by Zeitwerk or explicitly required. | ||
|
|
||
| ```ruby |
There was a problem hiding this comment.
Between the line above and this example it's not clear if the example code is required or not? "Loadable by Zeitwerk" seems to imply it should be autoloadable/reloadable in development without additional work, assuming the conventions are followed. Is that right?
In my experience both humans and and LLMs will default to using the example if it's not clear it is not required.
| ## Heartbeating | ||
|
|
||
| ### Forgetting to Heartbeat Long Activities | ||
|
|
||
| ```ruby | ||
| # BAD - No heartbeat, can't detect stuck activities | ||
| class ProcessLargeFile < Temporalio::Activity::Definition | ||
| def execute(path) | ||
| File.readlines(path).each_slice(1000) do |chunk| | ||
| process(chunk) # Takes hours, no heartbeat | ||
| end | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| ```ruby | ||
| # GOOD - Regular heartbeats with progress | ||
| class ProcessLargeFile < Temporalio::Activity::Definition | ||
| def execute(path) | ||
| File.readlines(path).each_slice(1000).with_index do |chunk, i| | ||
| Temporalio::Activity::Context.current.heartbeat("Processing chunk #{i}") | ||
| process(chunk) | ||
| end | ||
| end | ||
| end | ||
| ``` |
There was a problem hiding this comment.
This example seems a lot like the one above.
Personally I'd like to see an example that allows me to compare Temporal activities with solutions like job-iteration or Active Job Continuation. Claude came up with the following:
class ProcessAllUsers < Temporalio::Activity::Definition
def execute
context = Temporalio::Activity::Context.current
cursor = context.info.heartbeat_details.first # nil on first attempt
# `start` argument below is inclusive
User.find_each(start: cursor) do |user|
context.cancellation.check!
process(user)
context.heartbeat(user.id.succ) # next id to start from
end
end
end| # BAD - No heartbeat, can't detect stuck activities | ||
| class ProcessLargeFile < Temporalio::Activity::Definition | ||
| def execute(path) | ||
| File.readlines(path).each_slice(1000) do |chunk| |
There was a problem hiding this comment.
| File.readlines(path).each_slice(1000) do |chunk| | |
| File.foreach(path).each_slice(1000) do |chunk| |
Same for the other example below. See https://felipeelias.github.io/ruby/2017/01/02/fast-file-processing-ruby.html
Summary
Details
The Ruby SDK (
temporaliogem, GA) uses a fundamentally different determinism approach from Python (sandbox) and TypeScript (V8 isolate):TracePointto detect forbidden method calls at runtimeRuby-specific sections added that don't exist in Python/TypeScript:
Temporalio::Cancellationobjectsworkflow_arg_hint/workflow_result_hintfor type guidanceTest plan
🤖 Generated with Claude Code