Skip to content

Code-With-Rails/rails-event-jobs-spike

Repository files navigation

rails-event-jobs-spike

A small Rails 8.1 app that verifies a single claim: Rails.event plus structured Active Job events plus Continuations are enough to build user-facing live progress for background jobs — without per-job broadcast wiring and without polling.

What this proves

  • Active Job lifecycle events (active_job.enqueued, started, completed) flow through Rails.event with matching job_id and duration populated.
  • Continuation active_job.step_started and active_job.step events carry cursor and resumed fields, sufficient to drive an in-progress UI.
  • Context set via Rails.event.set_context(user_id: ...) inside a before_perform callback reaches every event the job emits — including framework events.
  • Rails' executor wraps each job in app.reloader.wrap and clears event reporter context on to_complete, so context does not leak between consecutive jobs on the same worker thread. No around_perform cleanup is needed for the common case.

The five tests in test/jobs/import_contacts_job_test.rb assert each of the above.

Try the live demo

Two-thread streaming demo. The main thread enqueues the job and renders user-facing progress messages. A :async worker thread executes the job and emits raw structured events. Both streams print interleaved, with [main ] and [worker] tags so you can see which thread did what.

bin/run             # user_id=42, total_records=4
bin/run 99 6        # custom user_id and record count

Random 1-5s sleeps live inside the job (gated by the DEMO_SLEEP=1 env var that bin/run sets), so step events stream in over time rather than all at once.

Sample output:

19:38:33 [worker] • import.record_processed    {index: 0}      [user_id=42]
19:38:33 [main  ] ▶ UI: [███████·············] Imported 1 of 3  (33%)
19:38:35 [worker] • import.record_processed    {index: 1}      [user_id=42]
19:38:35 [main  ] ▶ UI: [█████████████·······] Imported 2 of 3  (67%)
19:38:38 [worker] • import.record_processed    {index: 2}      [user_id=42]
19:38:38 [main  ] ▶ UI: [████████████████████] Imported 3 of 3  (100%)

The same shape, swapping Queue for Turbo::StreamsChannel.broadcast_*, is what powers a browser progress card in production.

Run the tests

bin/rails test test/jobs/import_contacts_job_test.rb

Five tests, ~0.3 seconds (no sleeps in test mode).

Files worth reading

File What it does
app/jobs/import_contacts_job.rb Continuation job with three steps; sets event context in before_perform.
app/jobs/recording_subscriber.rb Test-only in-memory subscriber.
app/jobs/leaky_attribution_job.rb, safe_attribution_job.rb, bystander_job.rb Used by test 4 to demonstrate that the executor auto-clears context between jobs.
config/initializers/job_progress_subscriber.rb Wires the subscriber via Rails.event.subscribe.
bin/run Live two-thread streaming demo with progress bar rendering.
test/jobs/import_contacts_job_test.rb Five integration tests covering each verified claim.

Stack

  • Rails 8.1.3
  • Ruby 3.4.5
  • SQLite (default)
  • :async adapter for the demo, :inline for tests

Caveat: cursor semantics

step.advance!(from: x) calls x.succ, so the cursor reflects "next position to start from on resume" rather than "last index processed." For percentage display, either:

  • Use step.advance!(from: i) after processing index i (cursor becomes i + 1, equal to count of records done), or
  • Emit your own progress event (e.g. Rails.event.notify("import.record_processed", index: i)) and compute percentage in the subscriber from that count instead of from the cursor.

The bin/run demo takes the second approach.

About

Rails 8.1 spike: user-facing live progress for background jobs via Rails.event + Active Job Continuations

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors