This document is for humans developing the gem further: how to work on it, the design rules that must survive refactors, and the agreed roadmap with its reasoning — so decisions aren't relitigated from scratch every time someone new touches the code.
Monorepo: gravis/ (core) + gravis-ecs/ (ECS adapter), lockstep-versioned from one shared Gravis::VERSION, released together. Per gem:
cd gravis # or gravis-ecs
bundle install
bundle exec rspec # full suite (~seconds; in-memory SQLite, no AWS, no Docker)
bundle exec rubocop # rubocop-rails-omakaseNo Rakefile, no dummy app. spec/spec_helper.rb boots a minimal inline Rails app and loads Solid
Queue's real schema from the installed gem — specs run against the same tables a host app has.
AWS is never called: dispatch specs stub Gravis.executor; the ECS adapter's specs use
Aws::ECS::Client.new(stub_responses: true).
CI runs lint + tests on every PR (.github/workflows/main.yml).
Read CLAUDE.md for the file-by-file map. The one-paragraph version: jobs opt in with
include Gravis::Job and declare a size; each size maps to an internal queue
(gravis-vcpu-<cpu>-mem-<gb>); enqueues trigger Gravis::DispatchJob, which launches/stops on-demand
workers through a provider adapter (Gravis::Executor, ECS Fargate today) and re-arms itself
only while work is in flight.
- The dispatch core is provider-blind.
dispatch_job.rb,job.rb,sizes.rb, andgravis.rbmust never reference AWS (or any provider). Everything cloud-specific goes behindGravis::Executor. - Gravis reads Solid Queue, never writes it. Lifecycle decisions come from counting
ReadyExecution/ClaimedExecutionandjobs.finished_at. Claiming, retries, and failure handling stay stock Solid Queue. - Ruby never touches infrastructure identity. No task-definition registration, no IAM, no
network config invented at runtime — those come from the deployment (today: the
GRAVIS_TARGETenv var written by CDK). - Fail-safe, not fail-loud. Broken config (
InvalidTarget) disables dispatch and reports viaRails.erroronce; transient provider errors (ExecutorError) are retried next tick. Jobs always wait safely inready— gravis being broken must never lose or corrupt work. - The queue names are internal. Consumers declare sizes, never queues. Anything that leaks
gravis-vcpu-8-mem-16into user-facing API is a regression. - Deliberate non-features (decided, with reasons — see README "contract" section): no pre-warm API (paying for guesses), no always-warm pool (that's the always-on worker gravis replaces), no "lite" Rails boot (jobs are app code; they need the app).
Subclass Gravis::Executor, implement configured?, validate_size!, launch, running,
stop (see executor.rb for the contract, executor/ecs.rb for the reference
implementation in gravis-ecs/). Ship it as its own gravis-<provider> gem in this monorepo,
exact-pinned to the core version. Raise Gravis::ExecutorError for transient failures,
Gravis::InvalidTarget for broken configuration. config.executor = :<provider> resolves
gravis/executor/<provider> via require. Size menus are a provider property — ship your own,
don't reuse Fargate's.
In order. Items further down depend on the ones above proving out.
- Infra companion — DONE (2026-07). Proven shape: a reusable construct that clones the
worker service's task definition (same image/env/secrets/roles/SG), sets command
./bin/rails gravis:work, grants dispatch IAM, and writesGRAVIS_TARGETincluding thecontainerkey. Reference recipes: docs/infrastructure.md. - RunTask override smoke test — DONE (2026-07-21). Verified against a real production
Fargate cluster: a dispatcher-launched task ran with
{"cpu":"8192","memory":"16384"}overrides anddescribe-tasksconfirmed the size took effect. - Migrate one real app end-to-end — DONE (2026-07-21). A production Rails app runs its video-render pipeline on gravis: cold boot, warm reuse across jobs, idle stop at grace — all observed live. (Version now 0.3.1; rubygems publication tracked in item 7.)
- Layered target resolution — designed, not implemented.
TaskTarget.resolvemerges three sources per field: explicit initializer values →GRAVIS_TARGETJSON → ECS-metadata self-introspection (cluster/subnets/SG/container discovered from the worker's own task viaECS_CONTAINER_METADATA_URI_V4). Goal: non-CDK users configure onlytask_definition; env-var deployments unchanged.enabled?becomes "target resolvable". - Gem split — DONE (2026-07). Monorepo:
graviscore +gravis-ecsadapter, judoscale pattern (consumers installgravis-ecs; core arrives as an exact-pinned dependency). Lockstep versioning from the sharedGravis::VERSION; release workflow publishes both. - Second provider candidates (build on demand, not speculatively): Fly Machines (model fits exactly — see fly.io's "Rails Background Jobs with Fly Machines" post), GCP Cloud Run Jobs, Kubernetes Jobs.
- Publish on rubygems.org — repo is public and scrubbed; GitHub
releaseenvironment exists and the publish workflow is wired. Remaining: register pending trusted publishers for both gem names (org owner action; repozarpay/gravis, workflowrelease.yml, environmentrelease), then cut the v0.3.1 GitHub release. Prior art / positioning research: cookpad/barbeque, rails-lambda/lambdakiq, judoscale adapters.
Bump gravis/lib/gravis/version.rb (shared — both gems release in lockstep at this version),
merge, create a GitHub release tagged vX.Y.Z. The release workflow re-runs lint + tests for
both gems and publishes core then adapter to rubygems.org via trusted publishing.
Breaking changes: major-ish bump (pre-1.0: minor), BREAKING CHANGE: footer in the commit, and a
migration note in the release description.
Conventional Commits (feat:, fix:, refactor:, docs:…), imperative subject ≤ 72 chars,
body explains why. Breaking changes get ! and a BREAKING CHANGE: footer.