Temporal Ruby SDK

repository·master·Indexed 43 days ago

https://github.com/coinbase/temporal-ruby

A pure Ruby library for defining and running Temporal workflows and activities, enabling distributed, fault-tolerant orchestration. It provides tools for managing workflow state machines, implementing idempotent activities, handling asynchronous activity completion, and configuring gRPC connections with SSL and OAuth2.

Tokens
10.8K
Snippets
35
Records
45
Agent score
55%

What's inside Temporal Ruby

  1. How the Synchronous Proxy pattern works

    master

    The Synchronous Proxy pattern allows a non-workflow process (like a stateless web application) to advance a workflow state machine from an initial state to a terminal state by both sending data to and receiving data from the workflow.

    The Problem

    • Signals are one-way (fire and forget). A Temporal client can send a signal, but a workflow cannot send a signal back to a client.
    • Queries can retrieve information, but they are intended to be idempotent and should not modify the workflow's state. They also require inefficient polling.

    The Solution

    To achieve a request-response loop between a non-workflow process and a workflow, you use Proxy Workflows:

    1. Primary Workflow: Holds the main state machine and business logic.
    2. Proxy Workflow: A secondary workflow started by the non-workflow process. It signals the Primary Workflow with data and then blocks (waits) for a response. When the Primary Workflow signals the Proxy Workflow with a result, the Proxy Workflow exits and returns that value to the caller.

    This allows the non-workflow caller to treat the interaction as a synchronous, blocking call that receives a return value.

  2. Handle breaking changes in Workflows using releases

    master

    Because workflow execution must be deterministic, you cannot simply deploy breaking changes (like adding/removing activities or changing the order of operations) without breaking running workflows.

    To manage this, use workflow.has_release?(release_name). This method returns a consistent result for a given release_name throughout the lifecycle of a specific workflow execution, allowing you to branch logic based on whether a specific version of your code has been deployed.

    class MyWorkflow < Temporal::Workflow
      def execute
        Activity1.execute!
    
        if workflow.has_release?(:fix_1)
          ActivityNew1.execute!
        end
    
        # ... logic continues
      end
    end
  3. Implement Middleware for Workflows and Activities

    master

    Middleware allows you to execute custom code before or after the execution of workflows or activities (e.g., for logging, error handling, or metrics).

    Types of Middleware:

    • Activity Middleware: Executed around each activity method.
    • Workflow Middleware: Executed around an entire workflow.
    • Workflow Task Middleware: Executed around each individual workflow task.

    Implementation Pattern: Define a class with a call(metadata) method. You must call yield within call to allow the next middleware or the actual target method to execute.

    Note on Order: Middleware added last is the first to execute (it sits at the top of the stack).

    class MyMiddleware
      def call(metadata)
        puts "Before execution"
        yield
        puts "After execution"
        result
      end
    end
    
    # Adding to a worker
    worker.add_activity_middleware(MyMiddleware)
  4. Use explicit local configurations for multiple clients

    master

    While Temporal.configure sets a global default, you can use Temporal::Configuration objects to manage multiple connections (e.g., connecting to different hosts or using different credentials) within the same application.

    config_1 = Temporal::Configuration.new
    config_1.host = 'temporal-01'
    
    worker_1 = Temporal::Worker.new(config_1)
    client_1 = Temporal::Client.new(config_1)
    
    client_1.start_workflow(MyWorkflow)
    config_1 = Temporal::Configuration.new
    config_1.host = 'temporal-01'
    
    config_2 = Temporal::Configuration.new
    config_2.host = 'temporal-01'
    
    worker_1 = Temporal::Worker.new(config_1)
    worker_2 = Temporal::Worker.new(config_2)
    
    client_1 = Temporal::Client.new(config_1)
    client_1.start_workflow(MyWorkflow)
    
    client_2 = Temporal::Client.new(config_2)
    client_2.start_workflow(OtherWorkflow)
  5. Handle asynchronous activity completion

    master

    If an activity needs to wait for an external event (like a webhook or callback), you can use asynchronous completion.

    1. In your activity, call activity.async to prevent it from completing immediately.
    2. Pass the activity.async_token to your external system.
    3. When the external event occurs, use Temporal.complete_activity(async_token, result) to finish the activity successfully, or Temporal.fail_activity(async_token, error) to fail it.

    Note: Ensure you configure appropriate timeouts. Do not set a heartbeat timeout if using async activities, as you won't be able to emit heartbeats and the activity will time out.

    class AsyncActivity < Temporal::Activity
      def execute(user_id)
        user = User.find_by(id: user_id)
    
        # Pass the async_token to complete your activity later
        ExternalSystem.verify_user(user, activity.async_token)
    
        activity.async # prevents activity from completing immediately
      end
    end
    
    # Later, to complete:
    Temporal.complete_activity(async_token, result)
    
    # Or to fail:
    Temporal.fail_activity(async_token, MyError.new('Something went wrong'))
  6. Run Ruby Temporal Examples

    master

    To run the examples provided in this repository, you must have a running Temporal service.

    1. Install dependencies: Run bundle install to install the required gems.
    2. Configure connection: Modify the init.rb file to point to your specific Temporal cluster address.
    3. Start Workers: You must start three separate worker processes. Each process uses a different task queue to demonstrate different payload serialization methods. It is recommended to run these in separate terminals or using a multiplexer like tmux.
    4. Trigger Workflows: Use the bin/trigger command to start a workflow from the workflows directory.
    # Install dependencies
    bundle install
    
    # Start the three required worker processes
    bin/worker
    USE_ENCRYPTION=1 bin/worker
    USE_ERROR_SERIALIZATION_V2=1 bin/worker
    
    # Trigger a workflow
    bin/trigger NAME_OF_THE_WORKFLOW [argument_1, argument_2, ...]
  7. Set up and run a Worker

    master

    A Worker is a process that communicates with the Temporal server to manage the execution of Workflows and Activities. You must register the specific workflows and activities you want the worker to handle before starting it.

    Calling worker.start is a blocking operation that takes over the current process until a TERM or INT signal is received.

    require 'temporal/worker'
    
    worker = Temporal::Worker.new
    worker.register_workflow(HelloWorldWorkflow)
    worker.register_activity(SomeActivity)
    worker.register_activity(SomeOtherActivity)
    worker.start
  8. Set up a local Temporal service with Docker

    master

    The Temporal service manages persistence and coordination. To run it locally, use the official Docker Compose configuration:

    curl -O https://raw.githubusercontent.com/temporalio/docker-compose/main/docker-compose.yml
    
    docker-compose up
  9. Configure SSL and OAuth2 credentials

    master

    For production environments, you may need to use SSL or OAuth2 tokens via gRPC credentials.

    SSL Configuration

    Use GRPC::Core::ChannelCredentials to provide the root certificate, client key, and client chain.

    OAuth2 Token Configuration

    Use GRPC::Core::CallCredentials with an updater_proc. The updater_proc should be a method that returns a proc (similar to how the googleauth library works).

    Combining SSL and OAuth2

    Use the .compose method to combine both credential types.

    # SSL
    Temporal.configure do |config|
        config.credentials = GRPC::Core::ChannelCredentials.new(root_cert, client_key, client_chain)
    end
    
    # OAuth2
    Temporal.configure do |config|
        config.credentials = GRPC::Core::CallCredentials.new(updater_proc)
    end
    
    # Combined SSL and OAuth2
    Temporal.configure do |config|
        config.credentials = GRPC::Core::ChannelCredentials.new(root_cert, client_key, client_chain).compose(
            GRPC::Core::CallCredentials.new(token.updater_proc)
        )
    end
  10. Test Workflows and Activities locally

    master

    The Temporal Ruby SDK provides a testing framework to run workflows locally.

    1. Requirement: You must explicitly require 'temporal/testing' in your test setup (e.g., spec_helper.rb).
    2. Unit Testing: Use WorkflowClass.execute_locally(args) to run a workflow locally and pass arguments directly to the #execute method.
    3. Integration Testing: Use Temporal::Testing.local! to make all Temporal.start_workflow calls run inline and locally. This can be used with a block to restore original behavior after the test.
    # In your test setup
    require 'temporal/testing'
    
    # Unit test style
    HelloWorldWorkflow.execute_locally(arg1, arg2)
    
    # Integration test style
    Temporal::Testing.local! do
      Temporal.start_workflow(HelloWorldWorkflow)
    end
  11. Define an Activity and a Workflow

    master

    In Temporal, Activities perform the actual work (side effects), while Workflows define the high-level orchestration logic.

    Important: Workflow code must be deterministic. Do not include network requests, database queries, or non-deterministic logic inside a Temporal::Workflow class, as it may be replayed during execution.

    require 'temporal-ruby'
    
    # Define an activity
    class HelloActivity < Temporal::Activity
      def execute(name)
        puts "Hello #{name}!"
        nil
      end
    end
    
    # Define a workflow that calls the activity
    class HelloWorldWorkflow < Temporal::Workflow
      def execute
        HelloActivity.execute!('World')
        nil
      end
    end