Module: GenevaDrive

Defined in:
lib/geneva_drive.rb,
lib/geneva_drive/version.rb,
lib/generators/geneva_drive/install/install_generator.rb

Overview

GenevaDrive provides durable workflows for Rails applications.

It offers a clean DSL for defining multi-step workflows that execute asynchronously, with strong guarantees around idempotency, concurrency control, and state management.

Examples:

Defining a workflow

class SignupWorkflow < GenevaDrive::Workflow
  step :send_welcome_email do
    WelcomeMailer.welcome(hero).deliver_later
  end

  step :send_reminder, wait: 2.days do
    ReminderMailer.remind(hero).deliver_later
  end
end

Starting a workflow

SignupWorkflow.create!(hero: current_user)

Defined Under Namespace

Modules: FlowControl, Generators, MigrationHelpers, TestHelpers Classes: CombinedExceptionPolicy, Engine, ExceptionPolicy, Executor, FlowControlSignal, HousekeepingJob, InvalidStateError, InvalidStateTransition, PerformStepJob, PreconditionError, StepCollection, StepConfigurationError, StepDefinition, StepExecution, StepExecutionError, StepFailedError, StepNotDefinedError, Workflow

Constant Summary collapse

VERSION =
"0.5.0"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.delete_completed_workflows_afterActiveSupport::Duration?

How long to keep completed workflows before cleanup. Set to nil to disable automatic cleanup.

Returns:

  • (ActiveSupport::Duration, nil)


55
56
57
# File 'lib/geneva_drive.rb', line 55

def delete_completed_workflows_after
  @delete_completed_workflows_after
end

.enqueue_after_commitBoolean

Whether to defer job enqueueing to after the database transaction commits. When true (the default in non-test environments), jobs are enqueued inside an after_all_transactions_commit callback to ensure the step execution record is visible to the job worker.

When false (the default in test environments), jobs are enqueued immediately. This is necessary for transactional tests (especially with SQLite) where the outermost test transaction never commits, causing after_commit callbacks to misbehave or not fire at all.

Returns:

  • (Boolean)


88
89
90
# File 'lib/geneva_drive.rb', line 88

def enqueue_after_commit
  @enqueue_after_commit
end

.housekeeping_batch_sizeInteger

Maximum number of workflows to process in a single housekeeping run. Prevents runaway processing if there's a large backlog.

Returns:

  • (Integer)


70
71
72
# File 'lib/geneva_drive.rb', line 70

def housekeeping_batch_size
  @housekeeping_batch_size
end

.stuck_in_progress_thresholdActiveSupport::Duration

How long a step execution can be in "in_progress" state before being considered stuck and eligible for recovery.

Returns:

  • (ActiveSupport::Duration)


60
61
62
# File 'lib/geneva_drive.rb', line 60

def stuck_in_progress_threshold
  @stuck_in_progress_threshold
end

.stuck_recovery_actionSymbol

Default recovery action for stuck step executions. Can be :reattempt or :cancel

Returns:

  • (Symbol)


75
76
77
# File 'lib/geneva_drive.rb', line 75

def stuck_recovery_action
  @stuck_recovery_action
end

.stuck_scheduled_thresholdActiveSupport::Duration

How long a step execution can be past its scheduled_for time while still in "scheduled" state before being considered stuck.

Returns:

  • (ActiveSupport::Duration)


65
66
67
# File 'lib/geneva_drive.rb', line 65

def stuck_scheduled_threshold
  @stuck_scheduled_threshold
end

Class Method Details

.enqueues_deferred?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns true if job enqueueing should be deferred to after transaction commit.

Returns:

  • (Boolean)


151
152
153
154
# File 'lib/geneva_drive.rb', line 151

def enqueues_deferred?
  return false if Thread.current[:geneva_drive_inline_enqueue]
  enqueue_after_commit
end

.with_inline_enqueue { ... } ⇒ Object

Temporarily enables inline job enqueueing for the duration of the block. Jobs will be enqueued immediately instead of waiting for transaction commit.

When to use this

This method is useful when creating multiple workflows inside a bulk enqueueing context (e.g., BulkEnqueue.in_bulk). Normally, GenevaDrive defers job enqueueing to after_all_transactions_commit callbacks. However, these callbacks fire after the bulk enqueue block ends and clears its buffer, causing jobs to be enqueued individually instead of in bulk.

Requirements

Only use this if you have a co-committing, database-backed ActiveJob adapter (such as SolidQueue, GoodJob, or Gouda) running on the same database as your application models. With such adapters, the job INSERT and the workflow/step_execution records are written in the same database transaction, guaranteeing atomicity without needing after_commit deferral.

If you use a non-transactional queue adapter (Redis-based Sidekiq, Resque, etc.), do NOT use this method in production — jobs may be enqueued before their associated records are visible, causing "record not found" errors.

Thread safety

Uses a thread-local flag that won't affect other threads or requests. Safe to use in multi-threaded web servers (Puma, etc.).

Examples:

Create workflows in bulk with BulkEnqueue

BulkEnqueue.in_bulk do
  GenevaDrive.with_inline_enqueue do
    drafts.each do |draft|
      DeliverBriefWorkflow.create!(hero: draft)
    end
  end
end

Batch workflow creation without bulk enqueue helper

GenevaDrive.with_inline_enqueue do
  User.pending_onboarding.find_each do |user|
    OnboardingWorkflow.create!(hero: user)
  end
end

Yields:

  • the block to execute with immediate (inline) job enqueueing

Returns:

  • (Object)

    the result of the block

See Also:

  • #enqueue_after_commit


139
140
141
142
143
144
145
# File 'lib/geneva_drive.rb', line 139

def with_inline_enqueue
  previous = Thread.current[:geneva_drive_inline_enqueue]
  Thread.current[:geneva_drive_inline_enqueue] = true
  yield
ensure
  Thread.current[:geneva_drive_inline_enqueue] = previous
end