Files
stevenlai ef2c99ac6f Fix Common Gotchas links in language guides (#266)
* Fix Common Gotchas links in language guides

* Fix Standalone Activities links in language guides

---------

Co-authored-by: Brian Strauch <brian@brianstrauch.com>
2026-09-10 10:08:25 -07:00

6.9 KiB

Ruby Gotchas

Ruby-specific mistakes and anti-patterns. See also Common Gotchas for language-agnostic concepts.

File Organization

Unlike Python, Ruby doesn't reload workflow files (no sandbox). Still best practice to separate workflows and activities for clarity and maintainability.

# BAD - Everything in one file
# app.rb
class MyWorkflow < Temporalio::Workflow::Definition
  def execute(name)
    Temporalio::Workflow.execute_activity(
      MyActivity,
      name,
      start_to_close_timeout: 30
    )
  end
end

class MyActivity < Temporalio::Activity::Definition
  def execute(name)
    # Heavy I/O, external calls, etc.
  end
end
# GOOD - Separate files
# workflows/my_workflow.rb
require 'temporalio/workflow'

class MyWorkflow < Temporalio::Workflow::Definition
  def execute(name)
    Temporalio::Workflow.execute_activity(
      MyActivity,
      name,
      start_to_close_timeout: 30
    )
  end
end

# activities/my_activity.rb
require 'temporalio/activity'

class MyActivity < Temporalio::Activity::Definition
  def execute(name)
    # Heavy I/O, external calls, etc.
  end
end

# worker.rb
require_relative 'workflows/my_workflow'
require_relative 'activities/my_activity'

Wrong Retry Classification

Transient network errors should be retried. Authentication errors should not be. See references/ruby/error-handling.md to understand how to classify errors with non_retryable: true and non_retryable_error_types.

Heartbeating

Forgetting to Heartbeat Long Activities

# BAD - No heartbeat, can't detect stuck activities
class ProcessLargeFile < Temporalio::Activity::Definition
  def execute(path)
    File.foreach(path).each_slice(1000) do |chunk|
      process(chunk)  # Takes hours, no heartbeat
    end
  end
end
# GOOD - Regular heartbeats with progress
class ProcessLargeFile < Temporalio::Activity::Definition
  def execute(path)
    File.foreach(path).each_slice(1000).with_index do |chunk, i|
      Temporalio::Activity::Context.current.heartbeat("Processing chunk #{i}")
      process(chunk)
    end
  end
end

Heartbeat Timeout Too Short

# BAD - Heartbeat timeout shorter than processing time between heartbeats
Temporalio::Workflow.execute_activity(
  ProcessChunk,
  start_to_close_timeout: 1800,
  heartbeat_timeout: 10  # Too short!
)

# GOOD - Heartbeat timeout allows for processing variance
Temporalio::Workflow.execute_activity(
  ProcessChunk,
  start_to_close_timeout: 1800,
  heartbeat_timeout: 120
)

Set heartbeat timeout as high as acceptable for your use case -- each heartbeat counts as an action.

Cancellation

Not Handling Workflow Cancellation

# BAD - Cleanup doesn't run on cancellation
class BadWorkflow < Temporalio::Workflow::Definition
  def execute
    Temporalio::Workflow.execute_activity(AcquireResource, start_to_close_timeout: 300)
    Temporalio::Workflow.execute_activity(DoWork, start_to_close_timeout: 300)
    Temporalio::Workflow.execute_activity(ReleaseResource, start_to_close_timeout: 300)  # Never runs if cancelled!
  end
end
# GOOD - Use ensure with detached cancellation for cleanup
class GoodWorkflow < Temporalio::Workflow::Definition
  def execute
    Temporalio::Workflow.execute_activity(AcquireResource, start_to_close_timeout: 300)
    Temporalio::Workflow.execute_activity(DoWork, start_to_close_timeout: 300)
  ensure
    # Create a detached cancellation (not tied to workflow cancellation)
    # so cleanup activity runs even after workflow is cancelled
    detached_cancel, _cancel_proc = Temporalio::Cancellation.new
    Temporalio::Workflow.execute_activity(
      ReleaseResource,
      start_to_close_timeout: 300,
      cancellation: detached_cancel
    )
  end
end

Not Handling Activity Cancellation

Activities must opt in to receive cancellation. This requires:

  1. Heartbeating -- cancellation is delivered via the heartbeat response
  2. Catching the cancellation exception -- Temporalio::Error::CancelledError is raised when a heartbeat detects cancellation
# BAD - Activity ignores cancellation
class LongActivity < Temporalio::Activity::Definition
  def execute
    do_expensive_work  # Runs to completion even if cancelled
  end
end
# GOOD - Heartbeat and handle cancellation
class LongActivity < Temporalio::Activity::Definition
  def execute
    items.each do |item|
      Temporalio::Activity::Context.current.heartbeat
      process(item)
    end
  rescue Temporalio::Error::CancelledError
    cleanup
    raise
  end
end

Testing

Not Testing Failures

Make sure workflows work as expected under failure paths, not just happy paths. See references/ruby/testing.md for more info.

Not Testing Replay

Replay tests help you detect hidden sources of non-determinism in your workflow code and should be considered in addition to standard testing. See references/ruby/testing.md for more info.

Timers and Sleep

Using Kernel.sleep

# BAD - Kernel.sleep raises NondeterminismError
class BadWorkflow < Temporalio::Workflow::Definition
  def execute
    sleep(60)          # NondeterminismError!
    Kernel.sleep(60)   # NondeterminismError!
  end
end
# GOOD - Use Temporalio::Workflow.sleep for durable timers
class GoodWorkflow < Temporalio::Workflow::Definition
  def execute
    Temporalio::Workflow.sleep(60)  # Deterministic, durable timer
  end
end

Why this matters: Kernel.sleep uses the system clock, which differs between original execution and replay. Temporalio::Workflow.sleep creates a durable timer in the event history, ensuring consistent behavior during replay.

Illegal Call Tracing Gotchas

Third-Party Gems Triggering NondeterminismError

The Ruby SDK uses TracePoint-based illegal call tracing on the workflow fiber. Any gem that internally uses Thread, IO, Socket, Net::HTTP, or other forbidden operations will trigger NondeterminismError -- even if the call is deep in the gem's internals.

# BAD - Logging gem that uses Thread internally
class MyWorkflow < Temporalio::Workflow::Definition
  def execute
    SomeFancyLogger.info("Starting workflow")  # NondeterminismError if gem uses Thread.new!
  end
end

Fix: Disable Illegal Call Tracing for Specific Code

# Wrap non-deterministic but safe code
Temporalio::Workflow::Unsafe.illegal_call_tracing_disabled do
  # Code here won't trigger NondeterminismError
  SomeFancyLogger.info("Starting workflow")
end

For code that performs IO that you know is safe and want to allow:

# Disable the durable scheduler for IO operations
Temporalio::Workflow::Unsafe.durable_scheduler_disabled do
  # IO operations allowed here
end

Side Effects and Replay Safety

Always check replaying status before performing side effects in workflows:

unless Temporalio::Workflow::Unsafe.replaying?
  # Only runs during original execution, not replay
  Temporalio::Workflow.logger.info("Processing started")
end