Temporal Ruby SDK

repository·main·Indexed 41 days ago

https://github.com/temporalio/sdk-ruby

A framework for authoring distributed, scalable, and resilient workflows and activities using the Ruby programming language. Supports Ruby 3.3, 3.4, and 4.0. The SDK provides tools for implementing Temporal Activities and Workflows, running Workers, and interacting with the Temporal cluster via a thread-safe, fiber-compatible client.

Tokens
25K
Snippets
75
Records
102
Agent score
74%

What's inside Temporal

  1. Deterministic Constraints in Workflows

    main

    Temporal Workflows must be deterministic. This means the execution must produce the same results given the same history.

    The following actions are prohibited inside a workflow:

    • Performing I/O (network, disk, stdio, etc.).
    • Accessing or altering external mutable state.
    • Using threading or making blocking calls.
    • Using the system clock (e.g., Time.now).
    • Making non-deterministic random calls.
    • Any other non-guaranteed-deterministic calls.

    Note on Logging: You should use Temporalio::Workflow.logger instead of the standard Ruby logger. While the standard logger might work for quick debugging, it uses mutexes that can violate determinism during high contention.

  2. Use Converter Hints for custom deserialization

    main

    Hints allow you to provide metadata to the converter to assist in translation. For example, you can specify a target Ruby type for deserialization.

    Hints can be defined at the following levels:

    • Activities: activity_arg_hint, activity_result_hint
    • Workflows: workflow_arg_hint, workflow_result_hint
    • Signals/Queries/Updates: arg_hints, result_hint (Note: Signals do not accept result hints).

    For advanced cases like workflow memos where hints aren't directly supported, you can use Temporalio::Converters::RawValue and manually handle conversion.

  3. How the Ruby Rust Bridge handles async operations

    main

    The Ruby SDK uses a Rust extension to bridge the gap between Tokio (Rust) async and Ruby async. Because C extensions cannot easily turn a non-Ruby thread into a Ruby thread, the bridge uses a "command loop" pattern.

    The Command Loop Pattern

    1. The Ruby side instantiates and runs a command loop, which consumes a single Ruby thread for the life of the process.
    2. This loop unlocks the GVL and waits on a Rust mpsc channel for work.
    3. When the Rust side receives work, it re-acquires the GVL and executes a Ruby callback on that specific Ruby thread.

    Usage Pattern for Async Calls

    To perform an async operation, the bridge follows this flow:

    1. Ruby side: Calls a method implemented in Rust. To avoid blocking the entire thread, users should use a mechanism like a Ruby Queue to receive results.
    2. Rust side: Spawns a Tokio async task. Once the task completes, it converts the result to a Ruby object and pushes it to the Ruby thread via the command loop.
    3. Ruby side: Receives the result (e.g., via queue.pop). If this is called within a Fiber, the Ruby thread remains non-blocking.

    Important: The Ruby callback executed by the bridge must be very fast, as it runs serially on the command loop thread.

    queue = Queue.new
    some_bridge_thing.do_foo(queue)
    # If in a Fiber, this pop is non-blocking for the thread
    result = queue.pop
  4. Bypass the durable scheduler and illegal call tracing in Workflows

    main

    Temporal enforces determinism by using a durable fiber scheduler and an illegal call tracer. In advanced scenarios, you may need to bypass these:

    1. Bypass Durable Scheduler: Use Temporalio::Workflow::Unsafe.durable_scheduler_disabled when you need to perform non-deterministic side effects (like local stdout logging) that would otherwise hang the workflow due to mutex contention or IO wait.
    2. Enable IO Wait: If you need the durable scheduler but also require IO wait, use Temporalio::Workflow::Unsafe.io_enabled.
    3. Bypass Illegal Call Tracing: Use Temporalio::Workflow::Unsafe.illegal_call_tracing_disabled to allow specific calls like sleep or Time.now if they are used deterministically.

    Note: durable_scheduler_disabled implies illegal_call_tracing_disabled. It is a best practice to wrap these calls in an unless Temporalio::Workflow.replaying? block to prevent side effects from executing during replay.

  5. Understand Data Converters and Serialization

    main

    Data converters translate raw Temporal payloads to and from Ruby types. They consist of:

    • Payload Converters: Ruby values $\leftrightarrow$ serialized bytes.
    • Payload Codecs: Bytes $\leftrightarrow$ bytes (e.g., compression/encryption).
    • Failure Converters: Exceptions $\leftrightarrow$ serialized failures.

    Custom converters can be set via the data_converter argument in Temporalio::Client.connect.

    The default payload converter supports:

    • nil
    • String with Encoding::ASCII_8BIT (bytes)
    • Google::Protobuf::MessageExts instances
    • Everything else via the JSON module.

    Note on JSON: When using the default JSON converter, Ruby objects are serialized using JSON.generate. Upon deserialization, they often appear as Hashes. Hashes passed with symbol keys will return with string keys. While "JSON Additions" are supported, they are Ruby-specific and not cross-language compatible.

  6. Handle Workflow Exceptions and Failures

    main

    Workflows can raise exceptions to either fail the workflow/update or to fail the 'workflow task' (which causes a retry of the task until the code is fixed).

    Failing a Workflow/Update

    To explicitly fail a workflow or update with a user-defined error, raise Temporalio::Error::ApplicationError.

    By default, the following will fail the workflow/update:

    • Instances of Temporalio::Error::Failure.
    • Timeout::Error.
    • Exceptions from activity execution, child execution, or cancellation.

    Failing a Workflow Task (Retries)

    By default, all other exceptions (including non-deterministic exceptions) fail the 'workflow task'. This means the workflow will continually retry the task until the code is updated to fix the error. This is intended for unexpected code errors.

    Customizing Failure Behavior

    You can customize which exceptions cause a workflow failure (instead of a task retry) at the worker level using the workflow_failure_exception_types option, or per-workflow using the workflow_failure_exception_type definition method.

  7. Understand the SDK type signatures (RBS and RBI)

    main

    The SDK provides two sets of type signatures:

    1. RBS: Maintained throughout development and recently made public.
    2. RBI: Sorbet types maintained in parallel with RBS signatures.

    Key Details:

    • Protobuf Generation: Both RBS and RBI types for Protobuf messages and services are generated using protoc and the protoc-gen-rbi plugin.
    • Runtime Validation: RBI signatures can be validated at runtime by running the test suite with TEMPORAL_SORBET_RUNTIME_CHECK=1. This uses a SigApplicator to apply every RBI signature to the real implementation at runtime.
  8. Manage Workflow Cancellation

    main

    Every workflow has an associated Temporalio::Cancellation available at Temporalio::Workflow.cancellation. When a workflow is canceled, this token is canceled, and the cancellation propagates to any Temporal-native calls (like execute_activity or sleep) that use it.

    Creating Custom Cancellations

    To perform more granular cancellation, you can derive a new token from the workflow's cancellation:

    my_cancel, my_cancel_proc = Cancellation.new(Temporalio::Workflow.cancellation)

    Passing my_cancel to a specific operation allows you to trigger that specific cancellation by calling my_cancel_proc.call.

    Detached Cancellations

    You can create "detached" (standalone) Cancellation tokens that are not derived from the workflow cancellation. This is useful for cleanup activities in ensure blocks that must run even if the main workflow is canceled.

  9. Argument passing conventions in the Ruby Rust Bridge

    main

    When interacting with methods implemented in Rust via the bridge, follow these conventions for passing arguments:

    • Small argument sets: Use standard Ruby positional or keyword arguments.
    • Large argument sets: Use a Ruby Struct to group parameters. This is more efficient and cleaner for complex data.
    • Parameter Defaults: Do not define default values for parameters in Ruby functions defined in Rust. This ensures that missing arguments are caught eagerly by the Ruby interpreter during the initial call.
  10. Define a Workflow

    main

    Workflows are classes that extend Temporalio::Workflow::Definition.

    Key Requirements:

    • The execute method is the primary entry point and must be defined. Its return value/exception is the workflow's result/failure.
    • Workflow code must be deterministic.
    • Use Temporalio::Workflow.execute_activity to call activities.
    • Use Temporalio::Workflow.wait_condition to pause execution until a state change occurs.

    Handling Interactions: Methods marked with the following decorators become accessible to clients:

    • workflow_signal: For receiving signals.
    • workflow_query: For querying workflow state.
    • workflow_update: For sending updates (and receiving results).
    • workflow_query_attr_reader: A shortcut for creating query methods for attributes.
    require 'temporalio/workflow'
    
    class GreetingWorkflow < Temporalio::Workflow::Definition
      workflow_query_attr_reader :current_greeting
    
      def execute(params)
        loop do
          @current_greeting = Temporalio::Workflow.execute_activity(
            CreateGreeting,
            params,
            schedule_to_close_timeout: 300
          )
    
          # Wait for signal/update or completion
          Temporalio::Workflow.wait_condition { @greeting_params_update || @complete }
    
          if @greeting_params_update
            params, @greeting_params_update = @greeting_params_update, nil
          else
            return @current_greeting
          end
        end
      end
    
      workflow_update
      def update_greeting_params(greeting_params_update)
        @greeting_params_update = greeting_params_update
      end
    
      workflow_signal
      def complete_with_greeting
        @complete = true
      end
    end
  11. Install the Temporal Ruby SDK

    main

    The Temporal Ruby SDK supports Ruby 3.3, 3.4, and 4.0. If you are using Fibers, Ruby 3.3.2+ is recommended.

    Supported Platforms:

    • macOS (ARM/x64)
    • Linux (ARM/x64)

    Note: MinGW-based Windows is not currently supported. There are known caveats with the Google Protobuf dependency on musl-based Linux.

    To install via Gemfile:

    gem 'temporalio'

    To install via command line:

    gem install temporalio
  12. Execute a Workflow

    main

    To trigger a workflow from a client (e.g., from a web server or CLI), use client.execute_workflow. You must provide the Workflow class, the input arguments, and configuration options like id and task_queue.

    require 'temporalio/client'
    require_relative 'say_hello_workflow'
    
    # Create a client
    client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
    
    # Run workflow
    result = client.execute_workflow(
      SayHelloWorkflow,
      'Temporal', # This is the input to the workflow
      id: 'my-workflow-id',
      task_queue: 'my-task-queue'
    )
    puts "Result: #{result}"