Skip to content

yshaul/restate-ruby

Repository files navigation

Restate SDK for Ruby

License

⚠️ DISCLAIMER: This is an unofficial, experimental SDK and is not officially supported by Restate. Use at your own risk. For production use, please wait for the official Restate Ruby SDK release or consult the official Restate documentation.

The Restate SDK for Ruby provides a complete framework for building resilient, distributed applications using durable execution primitives.

What is Restate?

Restate is a system for easily building resilient applications using distributed durable building blocks. It provides:

  • Durable Execution: Automatic state persistence and replay
  • Virtual Objects: Keyed, stateful services with exclusive access guarantees
  • Workflows: Long-running orchestrations with durable promises
  • Reliable Communication: Service-to-service calls with automatic retries
  • Idempotency: Built-in deduplication of operations

Installation

Add this line to your application's Gemfile:

gem 'restate-sdk-ruby'

And then execute:

bundle install

Or install it yourself:

gem install restate-sdk-ruby

Building the Native Extension

The SDK includes a Rust-based native extension that provides the Restate VM. To build it:

# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# The extension will be built automatically during gem installation
# Or build manually:
cd ext/restate_sdk_ruby_core
cargo build --release

Quick Start

1. Define a Service

require 'restate'

# Basic stateless service
class Greeter < Restate::BasicService
  handler :greet
  def greet(input)
    name = input["name"]
    "Hello, #{name}!"
  end
end

2. Configure the Endpoint

Restate.configure do |config|
  config.bind = "http://localhost:4100"
  config.ingress_url = "http://localhost:8080"
  config.admin_url = "http://localhost:9070"
end

Restate.endpoint.define do
  mount Greeter
end

3. Run the Server

The SDK provides a Rack application. You can use any Rack-compatible server, but Falcon is recommended for production due to its HTTP/2 and fiber-based concurrency support:

# config.ru
require 'restate'
require_relative 'services'

run Restate.endpoint.to_rack_app

Or use Falcon directly:

#!/usr/bin/env -S falcon host
# config/falcon.rb

require 'falcon/environment/server'
require 'restate'
require_relative 'services'

service "restate" do
  include Falcon::Environment::Server
  
  count 1
  url { "http://localhost:4100" }
  
  middleware do
    Falcon::Server.middleware(Restate.endpoint.to_rack_app, verbose:, cache:)
  end
end

Run with: falcon serve -c config/falcon.rb

4. Invoke the Service

# From within another Restate handler (durable)
result = Greeter.call.greet(name: "World")

# From outside Restate (using HTTP client)
result = Restate.client.service("Greeter").greet(name: "World")

Service Types

Basic Service (Stateless)

Stateless services for independent, concurrent invocations:

class EmailService < Restate::BasicService
  handler :send_email
  def send_email(input)
    to = input["to"]
    subject = input["subject"]
    
    # Durable execution - only runs once even on retries
    run("send_via_api") do
      EmailAPI.send(to: to, subject: subject)
    end
  end
end

Virtual Object (Stateful, Keyed)

Keyed services with durable state and exclusive access per key:

class Counter < Restate::VirtualObject
  state :count, Integer, default: 0
  
  # Exclusive handler - can mutate state
  handler :increment
  def increment(amount = 1)
    self.count += amount
    count
  end
  
  # Shared handler - read-only, concurrent access
  shared :get
  def get
    count
  end
end

# Invoke with a key
Counter.call("user-123").increment(5)
value = Counter.call("user-123").get

Workflow (Long-Running Orchestration)

Workflows with durable promises for complex, long-running processes:

class OrderWorkflow < Restate::Workflow
  state :status, String, default: "pending"
  
  # Main workflow entrypoint
  def run(input)
    order_id = input["order_id"]
    
    # Wait for external confirmation
    payment = promise("payment").value
    
    self.status = "processing"
    
    # Call other services durably
    InventoryService.call.reserve(order_id: order_id)
    ShippingService.call.ship(order_id: order_id)
    
    self.status = "completed"
  end
  
  # Shared handlers for querying/completing
  shared :get_status
  def get_status
    status
  end
  
  shared :confirm_payment
  def confirm_payment(payment_info)
    promise("payment").resolve(payment_info)
  end
end

# Start workflow
OrderWorkflow.send("order-123").run(order_id: "order-123")

# Query status
status = OrderWorkflow.call("order-123").get_status

# Complete payment
OrderWorkflow.call("order-123").confirm_payment(amount: 100)

Durable Execution Primitives

Side Effects with run

Execute non-deterministic operations durably:

handler :process
def process(input)
  # Result is journaled - block only runs once
  api_result = run("fetch_data") do
    ExternalAPI.fetch(input["id"])
  end
  
  # Process the result
  process_data(api_result)
end

Durable Sleep

# Sleep for 30 seconds
sleep(30)

# Or use a Time object
sleep_until(Time.now + 1.hour)

Service Invocations

# Call another service and wait
result = OtherService.call.method_name(input)

# Send one-way (fire and forget)
OtherService.send.method_name(input)

# Send with delay
OtherService.send(after: 1.hour).method_name(input)

# Call with idempotency key
OtherService.call.idempotency_key("unique-id").method_name(input)

Awakeables

Create external completion points:

handler :start_task
def start_task(input)
  awakeable = awakeable
  
  # Send awakeable ID to external system
  ExternalQueue.enqueue(awakeable_id: awakeable.id, task: input)
  
  # Wait for external completion
  result = awakeable.value
  result
end

Complete from outside:

Restate.client.resolve_awakeable(awakeable_id, payload)

Configuration

Restate.configure do |config|
  # Local endpoint binding (where this service listens)
  config.bind = "http://localhost:4100"
  
  # URL Restate uses to reach this endpoint
  config.advertise_url = "http://host.docker.internal:4100"
  
  # Restate ingress URL (for invoking services)
  config.ingress_url = "http://localhost:8080"
  
  # Restate admin API URL (for registration)
  config.admin_url = "http://localhost:9070"
  
  # Auto-register on startup
  config.create_deployment_on_start = true
  
  # Identity verification keys (optional)
  config.identity_keys = ["your-signing-key"]
  
  # Custom JSON serializer (defaults to Oj)
  config.default_json_serializer = CustomSerializer
end

Middleware

Add custom middleware to the Rack stack:

middleware = Restate::MiddlewareStack.new
middleware.use Rack::CommonLogger
middleware.use MyCustomMiddleware, option: "value"

app = middleware.build(Restate.endpoint.to_rack_app)
run app

Rails Integration

The SDK includes automatic Rails integration:

# config/initializers/restate.rb
Restate.configure do |config|
  config.bind = "http://localhost:#{ENV.fetch('RESTATE_ENDPOINT_PORT', 4100)}"
  config.ingress_url = ENV.fetch("RESTATE_INGRESS_URL", "http://localhost:8080")
  config.admin_url = ENV.fetch("RESTATE_ADMIN_URL", "http://localhost:9070")
  config.create_deployment_on_start = Rails.env.development?
end
# config/services.rb
Restate.endpoint.define do
  mount Greeter
  mount Counter
  mount OrderWorkflow
end

Services are automatically loaded and reloaded in development.

Server Requirements

For production use, your server must support:

  • HTTP/2 (required for bidirectional streaming)
  • Stream hijacking (for Restate protocol)
  • Fiber-based concurrency (recommended for performance)

Recommended Servers

  1. Falcon (recommended) - Full HTTP/2 support with fiber concurrency
  2. Puma (limited) - HTTP/1.1 only, request-response mode
  3. WEBrick (development only) - Simple but not production-ready

See examples/ directory for configuration examples.

Development

Running Tests

# Install dependencies
bundle install

# Run all tests
bundle exec rake test

# Run a specific test file
bundle exec ruby -Ilib:test test/utils_test.rb

Test Status

Core SDK tests are passing. Some complex tests that require the native Rust extension (VM-dependent tests like context_test.rb, handler_test.rb, etc.) are temporarily skipped with .skip extension.

These tests validate:

  • ✅ Service definitions (BasicService, VirtualObject, Workflow)
  • ✅ Configuration and utilities
  • ✅ Codecs (JSON, Bytes, Void)
  • ✅ Retry policies
  • ⏭️ VM integration tests (skipped - require compiled native extension)

Building Native Extension

# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Build the extension
cd lib/restate/ext
cargo build --release

Examples

# Install dependencies
bundle install

# Build native extension
cargo build --release

# Run tests
bundle exec rake test

# Run a single test
bundle exec ruby test/basic_service_test.rb

Examples

See the examples/ directory for complete working examples:

  • examples/falcon/ - Falcon server configuration
  • examples/basic_service/ - Simple stateless service
  • examples/virtual_object/ - Stateful counter
  • examples/workflow/ - Order processing workflow

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/restatedev/sdk-ruby.

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Resources

About

A Ruby sdk for Restate

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors