Skip to content

Named ActiveJob class per service for async execution (v0.6.0) - #49

Merged
sebscholl merged 3 commits into
mainfrom
feature/named-per-service-async-jobs
Jul 21, 2026
Merged

Named ActiveJob class per service for async execution (v0.6.0)#49
sebscholl merged 3 commits into
mainfrom
feature/named-per-service-async-jobs

Conversation

@sebscholl

@sebscholl sebscholl commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Release: v0.6.0 — minor bump under 0.x semver (new features + a documented breaking change to the async enqueue payload).

What & why

Every async service invocation today runs through a single generic Servus::Extensions::Async::Job. In job runners (Sidekiq, GoodJob, delayed_job, …) that means every background service shows up as the same class — so dashboards, per-queue metrics, retry tuning, and log filtering can't tell one service from another.

This change generates a named ActiveJob class per service and adds a per-service async(...) DSL for configuring the underlying ActiveJob options.

What changed

  • Named job per serviceTreasury::TransferGold::Service gets Treasury::TransferGold::ServiceJob, generated automatically. Async::Job is now an abstract base bound to its service via a class_attribute; perform(**args) calls that service (the class identifies what to run — nothing is resolved from a serialized name string).
  • async(...) DSLasync queue: :critical, priority: 10 for the common cases, plus an async do … end block class_eval'd into the job for the full ActiveJob API (retry_on, discard_on, callbacks). Declared settings are class-level defaults; inline .call_async options layer on top per enqueue and win.
  • Generation — an inherited hook creates the job eagerly for named services (worker-safe under Rails eager-load), with a lazy servus_job_class accessor as the single memoization point.
  • Removed the now-unreachable ServiceNotFoundError.

Public API is unchanged

Service.call_async(user_id: 123, queue: :critical, wait: 5.minutes) works exactly as before.

Upgrading (deployment notes, not code changes)

  • Drain queues before deploying — payload changed from perform_later(name:, args:) to perform_later(**args); jobs enqueued by an older version won't run after the upgrade.
  • Workers must eager-load their services (Rails' production default) so the named job resolves by class name.

Verification

  • Full suite green (778 examples), rubocop clean.
  • Rewrote call_spec/job_spec, added dsl_spec; coverage for naming, worker-resolvability, the eager inherited path, and the DSL (stable global fixtures, no conditional teardown).
  • Exercised in a real Rails 8.1 app (generated, tested, deleted): named jobs per service, async config applied, clean perform_later(**args) payload, inline queue override wins, inline execution runs the full lifecycle, and in a RAILS_ENV=production process eager_load! alone defines each sibling job so it constantizes from its name string.
  • Gem builds cleanly at 0.6.0.

Docs

  • site/features/async-execution.md — named jobs, the async DSL, eager-load note.
  • CHANGELOG.md — 0.6.0 entry with upgrade notes.

🤖 Generated with Claude Code

sebscholl and others added 2 commits July 21, 2026 03:24
Replace the single generic Servus::Extensions::Async::Job with a dedicated,
named ActiveJob subclass generated per service (e.g. Treasury::TransferGold::Service
=> Treasury::TransferGold::ServiceJob), so background runners show a meaningful
per-service job name instead of one generic class for every invocation.

- Slim Async::Job to an abstract base bound to its service via a class_attribute;
  perform(**args) calls that service (the class itself identifies what to run).
- call.rb owns the extension: call_async enqueues the service's named job with a
  clean perform_later(**args) payload; an async(queue:, priority:, &block) DSL
  exposes per-service ActiveJob config (block is class_eval'd for retry_on/etc.);
  an inherited hook generates the sibling job eagerly for named services, with a
  lazy servus_job_class accessor as the single memoization point.
- Remove the now-unreachable ServiceNotFoundError (no name-based resolution).
- Specs use stable global fixtures (no conditional teardown) and cover naming,
  worker-resolvability, the eager inherited path, and the async DSL.
- Docs + CHANGELOG: public .call_async API is unchanged; document the async DSL
  and the upgrade notes (drain queues, workers must eager-load).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Release the named per-service async jobs and async(...) DSL. Minor bump under
0.x semver: new features alongside the documented breaking change to the async
enqueue payload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sebscholl sebscholl changed the title Named ActiveJob class per service for async execution Named ActiveJob class per service for async execution (v0.6.0) Jul 21, 2026
Sync the lockfile to the bumped gemspec version so CI's frozen bundle install
resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sebscholl
sebscholl merged commit b0a48c8 into main Jul 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant