ruby-concurrency
Use when Ruby/Rails code performs concurrent work, shares mutable state across threads, uses queues or executors, uses Fibers, or has race conditions, deadlocks, thread-safety, or ordering hazards.
SKILL.mdskills/ruby-concurrency/SKILL.md
GitHubRuby Concurrency
Purpose
Design and review concurrent Ruby/Rails code so execution model, ownership, synchronization, lifecycle, failure propagation, and resource limits are explicit.
Concurrency is not automatically parallel CPU execution. Identify the workload and bottleneck before selecting a primitive.
Activate when
Thread,Queue,Mutex,Monitor, or thread pools are introduced- shared mutable state is accessed concurrently
- an intermittent race, ordering bug, deadlock, or starvation issue exists
- Rails jobs/services perform concurrent I/O
- database or HTTP connection pools constrain concurrency
- Fibers, schedulers, or async I/O are introduced
- thread safety of caches, registries, memoization, or clients is under review
- code relies on Ruby engine execution behavior
Do not activate merely because code is slow. Establish whether concurrency addresses the actual bottleneck.
Repository inspection
Inspect:
- Ruby engine and runtime version
- existing thread, queue, pool, Fiber, job, and executor abstractions
- ownership of shared mutable state
- database connection-pool configuration
- HTTP/client limits and timeouts
- executor/thread lifecycle and shutdown behavior
- existing synchronization conventions
- tests covering ordering, retries, failure, and cleanup
- deployment process/worker topology when relevant
Resolve runtime compatibility before relying on engine-specific behavior.
Concurrency model selection
| Workload | Candidate |
|---|---|
| independent blocking I/O | bounded threads/thread pool |
| CPU-heavy Ruby work | processes or an appropriate external worker |
| producer/consumer | Queue or existing bounded queue |
| shared small mutable state | Mutex/ Monitor around the invariant |
| state-transition waiting | condition variable or existing coordination abstraction |
| asynchronous I/O | Fibers/scheduler only when supported |
| Rails background work | existing job/executor infrastructure |
| one-off parallel tasks | explicit start, wait, failure, and shutdown lifecycle |
Do not create a custom executor when the repository/framework already owns the concern.
Core rules
Ownership
Prefer single ownership of mutable state.
Document who creates, reads, mutates, and releases shared state.
Synchronization
Protect an invariant, not an entire service.
Prefer a narrow critical section:
mutex.synchronize { @balance += amount }
Keep critical sections small. Avoid database/network I/O while holding a lock.
Lifecycle
Every worker/executor needs:
create -> start -> perform -> observe failure -> stop/join -> release
Do not leave application-managed threads detached from shutdown.
Failure propagation
Worker failures must be observable. Define whether the parent fails, remaining work stops, retries occur, or cleanup continues.
Mutex, Monitor, and Queue
Use Mutex for narrow mutual exclusion. Use Monitor only when re-entrant coordination is actually required.
Use Queue for thread-safe producer/consumer coordination.
Avoid nested locks with inconsistent ordering, sleeping while locked, and calling unknown collaborators while locked.
For queues, define worker count, backpressure, shutdown/sentinel behavior, queue growth, and exception handling.
Bounded concurrency
For blocking I/O, bound concurrency against:
- database connection pool
- HTTP connection limits
- external API rate limits
- memory per worker
- CPU availability
- remote service limits
- timeout duration
Never create one thread per unbounded input.
A larger worker count is not automatically faster.
Rails-specific rules
Prefer Rails' existing job, executor, and lifecycle mechanisms over long-lived application-managed threads.
When Active Record is used concurrently:
- account for connection-pool capacity;
- follow the repository's connection-management conventions;
- do not assume a transaction in one thread covers work in another;
- use database constraints, atomic updates, transactions, or locking for persisted invariants;
- remember that an in-process
Mutexcannot coordinate separate processes, workers, containers, or hosts.
For jobs, assume concurrent execution and retries. Make important state transitions idempotent.
Database concurrency
Let the database own persisted invariants where appropriate:
- unique constraints for uniqueness
- atomic updates for counters
- transactions for related writes
- locking mechanisms when repository/database conventions require them
- optimistic locking when concurrent edits should be detected
Do not use Ruby synchronization to protect a cross-process database invariant.
Fibers
Fibers are cooperative execution and are not interchangeable with threads.
Before using them:
- confirm runtime support;
- identify the scheduler/evented I/O model;
- confirm libraries cooperate;
- define cancellation/error behavior;
- keep execution-model boundaries explicit.
Do not introduce Fibers merely because they are lightweight.
Race conditions
Look for check-then-act sequences:
if cache[key].nil?
cache[key] = expensive_load
end
If correctness depends on interleaving, establish an atomic/locked boundary or use a data structure with the required contract.
Test races with controlled interleavings rather than arbitrary sleeps.
Deadlocks
Typical causes include inconsistent lock ordering, nested locks, waiting for workers while holding their required resource, and executor starvation.
For multiple locks, establish one acquisition order.
Diagnosis:
blocked stacks
-> held/waited resources
-> wait-for graph
-> cycle
-> remove/reorder dependency
-> regression test
Do not "fix" deadlocks with larger timeouts or sleeps without identifying the cycle.
Thread-safety review
Classify shared objects as:
immutable
thread-confined
synchronized
atomic
process-local
externally synchronized
unsafe
Do not call an object thread-safe merely because of Ruby's GVL/GIL-like implementation detail. Establish safety from the object contract and access pattern.
Testing
Prefer deterministic synchronization:
- barriers/latches where supported
- controlled queues
- injected collaborators
- explicit state transitions
Avoid sleep as the primary synchronization mechanism.
Test normal completion, concurrent updates, worker failure, shutdown, retries/idempotency, and bounded resource behavior.
A single successful stress run does not prove race or deadlock freedom.
Performance
Measure before increasing concurrency.
Inspect throughput, latency, queue depth, lock contention, pool saturation, external limits, CPU, memory, and thread count.
Compare representative concurrent workloads with a sequential baseline.
Anti-patterns / failure modes
- one thread per input item
- detached background threads
- global mutex around an entire service
- sleeping to fix races
- relying on the GVL for application correctness
- sharing non-thread-safe clients
- in-memory locks for cross-process invariants
- I/O inside locks
- inconsistent nested lock ordering
- swallowed worker exceptions
- unbounded queues
- custom executors replacing existing Rails/repository infrastructure
- Fibers without a compatible scheduler/I/O model
- increasing concurrency without measuring capacity
Implementation procedure
- Resolve Ruby engine/version and framework runtime.
- Classify workload and actual bottleneck.
- Identify ownership and shared invariants.
- Prefer existing repository/framework concurrency infrastructure.
- Select the smallest primitive that enforces the invariant.
- Bound workers and queues against real resource limits.
- Define lifecycle, failure propagation, and shutdown.
- Add deterministic concurrency tests.
- Measure representative performance when optimization is the goal.
- Run focused and broader regression checks.
- Inspect the final diff for leaked resources and unnecessary abstraction.
Agent review checklist
- Ruby engine/version resolved
- workload/bottleneck identified
- ownership is explicit
- shared invariants identified
- concurrency primitive justified
- critical sections are minimal
- external I/O is not unnecessarily held under locks
- workers/queues are bounded
- failures are observable
- lifecycle is explicit
- Rails connection-pool implications checked
- cross-process invariants are not protected by in-process locks
- tests use deterministic synchronization
- retry/idempotency behavior checked
- performance measured when relevant
- focused/regression tests actually run
Verification
Use repository-configured test/lint commands first.
For concurrent changes, run the focused concurrency test and affected test group. Run RuboCop when configured.
Never claim race freedom, deadlock freedom, or scalability from a single successful run.
Source foundation
Repository-process guidance for AI coding agents based on Ruby/Rails concurrency engineering principles. It intentionally avoids assuming a particular Ruby engine, Rails version, executor library, or deployment topology. Repository evidence takes precedence.
Book integration: The Well-Grounded Rubyist
Chapter 14 includes Ruby thread examples and shared-variable concerns. Treat it as introductory conceptual material only: it is based on Ruby 2.1 and does not define the complete behavior of current Ruby runtimes, servers, or concurrency libraries. Resolve the runtime and deployment model, make ownership/synchronization explicit, and validate race/failure behavior with tests.