Files
Donald Pinckney d5c47df820 Add Ruby SDK support (#41)
* Add all Ruby SDK reference files (11 files, ~2100 lines)

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>

* Fix alignment issues in Ruby reference files

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>

* Fix correctness issues in Ruby reference files

- 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>

* Add Ruby to all language references in SKILL.md and core files

- 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>

* Apply suggestions from code review

Co-authored-by: Bart de Water <118401830+bdewater-thatch@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>

* Apply suggestions from code review

Co-authored-by: Bart de Water <118401830+bdewater-thatch@users.noreply.github.com>
Co-authored-by: Donald Pinckney <donald_pinckney@icloud.com>

* Apply suggestion from @chris-olszewski

Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>

* copy over sample code

* Remove useless section, mention Mutex

* cleanup mutex mentions

* Clean up transitive NDE section

* Menial changes to align to python structure

* Add Workflow Init section to Ruby advanced-features

Document the workflow_init class method (Ruby's equivalent of Python's
@workflow.init) for initializing workflow state before signal/update
handlers run. Parallels the Python reference's Workflow Init section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Document graceful_shutdown_period in Ruby Worker Tuning

Add the graceful_shutdown_period worker option (Ruby's equivalent of
Python's graceful_shutdown_timeout) to the Worker Tuning section, with
an explanation of the worker shutdown sequence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Propagate cancellation in Ruby activity-error handling

Update the Handling Activity Errors example to re-raise when
Temporalio::Error.canceled? is true (Ruby's equivalent of Python's
is_cancelled_exception), so a canceled activity cancels the workflow
rather than failing it. Also clarify that only ApplicationError fails a
workflow; other exceptions only fail/retry the workflow task.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Align Ruby Workflow Failure section to Python

Replace the workflow_failure_exception_type / worker-option examples
(misaligned with Python and already covered in advanced-features.md)
with Python's example of raising an ApplicationError to deliberately
fail a workflow. Add the terse note about not using non_retryable
inside a workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add logger configuration to Ruby observability

Document configuring the logger via Client.connect (logger: kwarg),
which is used by both Temporalio::Workflow.logger and the activity
logger. Parallels Python's Customizing Logger Configuration section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Make Ruby Saga compensations cancellation-proof

Run saga compensations with a detached Temporalio::Cancellation so they
still execute when the workflow is canceled mid-saga. Previously they
used the workflow cancellation, which is already canceled at that point,
so the compensation activities would be canceled before starting. This
is the Ruby equivalent of Python's asyncio.shield.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Document patched() memoization caveat in Ruby versioning

Note that Temporalio::Workflow.patched memoizes per patch ID, so it
can't be used reliably in loops; append a sequence number to the patch
ID per iteration. This behavior is shared with Python and .NET.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add default versioning behavior to Ruby worker versioning

Document configuring default_versioning_behavior on
Temporalio::Worker::DeploymentOptions, paralleling Python's Worker
Configuration with Default Behavior section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix worker versioning config API names in Ruby docs

The Configuring Workers for Versioning example used class/kwarg names
that don't exist in the SDK. Correct them to deployment_options:,
Temporalio::Worker::DeploymentOptions, and
Temporalio::WorkerDeploymentVersion, matching the actual API and the
Worker Configuration with Default Behavior example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix worker concurrency config in Ruby Worker Tuning

max_concurrent_workflow_tasks and max_concurrent_activities are not
valid Worker.new kwargs. Use the tuner: option with
Temporalio::Worker::Tuner.create_fixed(workflow_slots:, activity_slots:)
to control concurrent execution slots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Align Ruby Workflow Init title with Python

Rename the section to 'Workflow Init Decorator' to match the Python
reference's heading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Structure Ruby Metrics to match Python

Split the flat Metrics section into 'Enabling SDK Metrics' and
'Key SDK Metrics' subsections, matching the Python reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Bart de Water <118401830+bdewater-thatch@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>
2026-05-28 17:51:06 -04:00

5.1 KiB

Ruby Workflow Determinism Protection

Overview

The Ruby SDK uses two mechanisms to enforce workflow determinism:

  1. Illegal Call Tracing -- TracePoint-based interception of forbidden method calls on the workflow fiber.
  2. Durable Fiber Scheduler -- a custom Fiber::Scheduler that makes fiber operations deterministic.

This differs from Python's sandbox (SandboxedWorkflowRunner) and TypeScript's V8 isolate sandbox. Ruby's approach is runtime tracing, not code isolation.

How Illegal Call Tracing Works

A TracePoint is installed on the workflow fiber thread. On every :call and :c_call event, the SDK checks the receiver class and method name against a configurable set of illegal calls.

# Internally, the SDK does something like:
TracePoint.new(:call, :c_call) do |tp|
  if illegal?(tp.defined_class, tp.method_id)
    raise Temporalio::Workflow::NondeterminismError,
      "Illegal call: #{tp.defined_class}##{tp.method_id}"
  end
end

Key behaviors:

  • Raises Temporalio::Workflow::NondeterminismError on violation.
  • Detects transitive calls -- a gem calling IO.read deep in its internals will still be caught.
  • Only active on the workflow fiber, not on activity threads or other fibers.

Forbidden Operations in Workflows

Default forbidden operations:

  • Kernel.sleep -- use Temporalio::Workflow.sleep
  • Time.now (no args) -- use Temporalio::Workflow.now
  • Thread.new -- not allowed in workflows
  • IO.* -- all IO class methods (IO.read, IO.write, IO.pipe, etc.)
  • Socket.* -- all socket operations
  • Net::HTTP.* -- all HTTP client calls
  • Random.* -- use Temporalio::Workflow.random
  • SecureRandom.* -- use Temporalio::Workflow.uuid for UUIDs
  • Timeout.timeout -- use Temporalio::Workflow.sleep with cancellation
  • Mutex / synchronize -- use an explicit Temporalio::Workflow::Mutex

Note: Time.new('2000-12-31') with arguments IS deterministic and allowed. Only Time.now (wall-clock) is forbidden.

Disabling Illegal Call Tracing

Use Temporalio::Workflow::Unsafe.illegal_call_tracing_disabled when third-party code is known safe:

class MyWorkflow < Temporalio::Workflow::Definition
  def execute
    # Third-party gem that does harmless Time.now internally
    result = Temporalio::Workflow::Unsafe.illegal_call_tracing_disabled do
      SomeGem.format_data(input)
    end
    result
  end
end

The block disables tracing only for its duration. Keep it as narrow as possible.

Customizing Illegal Calls

Pass illegal_workflow_calls: to Temporalio::Worker.new:

worker = Temporalio::Worker.new(
  client: client,
  task_queue: 'my-queue',
  workflows: [MyWorkflow],
  illegal_workflow_calls: Temporalio::Worker.default_illegal_workflow_calls.merge(
    'MyInternalClass' => :all,
    'AnotherClass' => { dangerous_method: true }
  )
)

Default set available via:

Temporalio::Worker.default_illegal_workflow_calls
# => { 'Kernel' => { sleep: true }, 'IO' => :all, ... }

Hash format:

  • { 'ClassName' => :all } -- block all methods on the class.
  • { 'ClassName' => { method_name: true } } -- block specific methods.

Common Issues

Third-party gems triggering NondeterminismError

Gems that call IO, Time.now, or Socket internally will trigger errors even if you don't call those methods directly. The correct fix is context-dependent and requires understanding and possibly debugging of the situation.

Fix 1: The call to a gem genuinely is doing IO, side effects, or other non-deterministic things. Then just like with other Temporal Workflow code, it should be moved into an activity.

Fix 2: Escape hatch: for code that needs IO to run within the workflow, use io_enabled. You should very seriously consider why IO is needed in the workflow and not in an activity. If you use this escape hatch to create non-determinism issues, you might later face non-determinism errors.

Temporalio::Workflow::Unsafe.io_enabled do
  config = YAML.load_file('config.yml')
end

Fix 3: Escape hatch: for code that triggers the illegal call tracing but doesn't cause actual non-determinism issues, you can disable illegal_call_tracing_disabled. Again, you must be sure that you are semantically correct that there is no non-determinism involved.

Temporalio::Workflow::Unsafe.illegal_call_tracing_disabled do
  ThirdPartyGem.safe_pure_computation(data)
end

Durable scheduler conflicts

If a gem requires its own fiber scheduler behavior, disable the durable scheduler for that block:

Temporalio::Workflow::Unsafe.durable_scheduler_disabled do
  some_fiber_aware_gem.call
end

Best Practices

  • Use Temporalio::Workflow.sleep, .now, .random, .uuid, .logger for all workflow-level operations.
  • Never perform IO, network calls, or file access in workflow code -- delegate to activities.
  • Use illegal_call_tracing_disabled sparingly and only when you are certain the code is deterministic.
  • Wrap side-effect-only code in unless Temporalio::Workflow::Unsafe.replaying? to avoid duplicate emissions during replay.
  • Prefer activities over io_enabled blocks -- activities have proper retry, timeout, and heartbeat semantics.