Skip to content

adamlyons2/active_job_continuation_poc

Repository files navigation

ActiveJob Continuation POC

A proof of concept demonstrating the difference between resumable jobs using ActiveJob::Continuable and regular jobs when interrupted during processing.

Overview

This Rails 8 application demonstrates:

  • ContinuableUserUpdateJob: Uses ActiveJob::Continuable to track progress with cursors and resume from the last checkpoint after interruption
  • RegularUserUpdateJob: Standard job that restarts from the beginning if interrupted

Both jobs update 100 users by touching their updated_at timestamp with a 1s delay per user (~1 seconds total).

Setup

Prerequisites

  • Ruby 3.4.2
  • Rails 8.1.0

Installation

cd active_job_continuation_poc
bundle install
bin/rails db:prepare
bin/rails db:seed

Running the Application

1. Start the Rails server

bin/rails server

Visit http://localhost:3000

2. Start the Solid Queue worker (in a separate terminal)

bin/jobs

Testing Interruption & Resumption

Test A: Continuable Job (Resumes from Checkpoint)

  1. Reset timestamps (in Rails console):

    User.update_all(updated_at: 1.week.ago)
  2. Queue the continuable job: Click "Queue Continuable Job" button in the browser

  3. Monitor progress: Watch the Solid Queue worker terminal output

  4. Interrupt at ~50%: Press Ctrl+C in the worker terminal after ~20 seconds (when ~20 users processed)

  5. Restart worker: Run bin/jobs again

  6. Observe: The job automatically resumes from the last user ID it processed. Check the browser - refresh to see only remaining users being updated.

Expected Result: Job picks up exactly where it left off. No duplicate work. Users 1-20 remain unchanged, users 21-100 get updated.

Test B: Regular Job (Restarts from Beginning)

  1. Reset timestamps (in Rails console):

    User.update_all(updated_at: 1.week.ago)
  2. Queue the regular job: Click "Queue Regular Job" button in the browser

  3. Monitor progress: Watch the Solid Queue worker terminal output

  4. Interrupt at ~50%: Press Ctrl+C in the worker terminal after ~5 seconds

  5. Restart worker: Run bin/jobs again

  6. Observe: The job restarts from User #1. Check the browser - refresh to see duplicate work happening on already-processed users.

Expected Result: Job restarts completely. Duplicate work occurs. Users 1-50 get touched twice, wasting compute time.

Key Implementation Details

Continuable Job (app/jobs/continuable_user_update_job.rb)

class ContinuableUserUpdateJob < ApplicationJob
  include ActiveJob::Continuable

  def perform
    step :update_users do |step|
      User.find_each(start: step.cursor) do |user|
        user.touch
        sleep 1
        step.advance! from: user.id  # Checkpoint at each user
      end
    end
  end
end

How it works:

  • step.cursor starts as nil, then holds the last processed user ID
  • step.advance! from: user.id creates a checkpoint after each user
  • On interruption, Solid Queue's stopping? method triggers graceful shutdown
  • On retry, find_each(start: step.cursor) resumes from last checkpoint

Regular Job (app/jobs/regular_user_update_job.rb)

class RegularUserUpdateJob < ApplicationJob
  def perform
    User.find_each do |user|
      user.touch
      sleep 1
    end
  end
end

How it works:

  • Standard job with no progress tracking
  • On interruption, job state is lost
  • On retry, starts from User #1 again

Comparing Results

Metric Continuable Job Regular Job
Total processing time (with interruption) ~10s ~15s
Duplicate work None 50 users
Resume point User #51 User #1
Efficiency ✅ Optimal ❌ 50% wasted

Console Helpers

# Reset all timestamps
User.update_all(updated_at: 1.week.ago)

# Check recently updated users
User.where("updated_at > ?", 1.minute.ago).count

# See update distribution
User.select(:id, :updated_at).order(:id)

# Clear all jobs
SolidQueue::Job.destroy_all

Architecture

app/
├── models/
│   └── user.rb                           # User model with timestamps
├── jobs/
│   ├── continuable_user_update_job.rb   # Resumable job with cursors
│   └── regular_user_update_job.rb       # Standard job
├── controllers/
│   └── users_controller.rb              # Trigger jobs, show users
└── views/
    └── users/
        └── index.html.erb                # UI with job buttons and user table

Key Takeaways

  1. Continuable jobs are resilient: Interruptions don't cause lost progress or duplicate work
  2. Cursors enable precision: Each step.advance! creates a checkpoint at the exact user ID
  3. Solid Queue integration: The stopping? method enables graceful shutdown at checkpoints
  4. Production benefits: For long-running batch jobs, continuation saves significant processing time and resources
  5. Trade-offs: Continuable jobs add slight complexity but provide substantial benefits for batch processing

Requirements

  • Rails 8.0+: ActiveJob::Continuable is available in Rails 8
  • Solid Queue: Provides the stopping? method required for graceful interruption
  • Database: SQLite (default), but works with any Rails-supported database

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published