Skip to content

Wiring

RedisModule, RedisQueueModule and RedisWorkerModule — how producer-only and worker apps share a features crate without spawning workers they don't run.

The same features::audio crate compiles into the API binary and the worker binary. Only the worker drains the queue. That split is two module imports, two mains, one shared features crate — the framework decides which #[process] methods actually run by walking the access graph from each app’s root.

ModuleRoleImported by
RedisModule::for_root(cfg)Opens the one Redis connection every Redis binding shares — the queue producer, the worker, the rate-limit store.Every app that touches Redis.
RedisQueueModuleBinds the producer over that connection: Arc<dyn JobProducer>, the portable handle a feature injects to enqueue.Every app that enqueues.
RedisWorkerModule::for_root(cfg)Contributes the RedisWorker transport — drains #[process] methods at boot and spawns one worker each; its own settings are NESTRS_REDIS__WORKER__* (the drain window at shutdown).Worker apps only.

RedisModule::for_root(None) reads NESTRS_REDIS__URL from the environment; RedisModule::for_root(RedisConfig { url, .. }) supplies that URL as the base the environment overlays — so a deployment setting NESTRS_REDIS__URL still wins, field by field (the dual-path rule). One connection, one variable, named for the crate that parses it: a throttler-only app sets the same NESTRS_REDIS__URL. See fundamentals/modules for the four boot phases that resolve the connection before any transport attaches.

The worker imports the substrate, both queue bindings and the feature’s queue adapter. That’s it — AudioQueueModule pulls in the AudioModule port itself.

apps/worker/src/module.rs (from the demo)
use nest_rs::config::ConfigModule;
use nest_rs::core::module;
use nest_rs::redis::{RedisModule, RedisQueueModule, RedisWorkerModule};
use features::audio::AudioQueueModule;
#[module(imports = [
ConfigModule::for_root(),
RedisModule::for_root(None),
RedisQueueModule,
RedisWorkerModule::for_root(None),
AudioQueueModule,
])]
pub struct WorkerModule;

At boot:

  1. ConfigModule reads env, materializes RedisConfig from NESTRS_REDIS__URL.
  2. RedisModule::for_root(None) registers a factory that opens the RedisConnection.
  3. RedisQueueModule registers a factory — declared to run after the connection’s — that binds RedisQueueProducer as Arc<dyn JobProducer>.
  4. RedisWorkerModule::for_root(None) contributes the RedisWorker transport.
  5. AudioQueueModule brings AudioProcessor (the #[processor] host) and its #[process] methods into the reachable set.
  6. RedisWorker::configure drains inventory::iter::<ProcessMethod>(), keeps only the methods whose provider is reachable, and logs each one. serve runs one queue runtime draining every method’s queue, one job at a time per queue.

A producer-only binary imports RedisModule::for_root(...) and RedisQueueModule, and skips RedisWorkerModule entirely. It gets Arc<dyn JobProducer> to push with — and no worker spawn, no drained inventory, no queue runtime.

apps/api/src/module.rs (from the demo, abridged)
use nest_rs::config::ConfigModule;
use nest_rs::core::module;
use nest_rs::http::HttpModule;
use nest_rs::redis::{RedisModule, RedisQueueModule};
use nest_rs::seaorm::{SeaOrmDatabaseModule, SeaOrmModule};
use features::audio::{AudioHttpModule, AudioScheduleModule};
#[module(imports = [
ConfigModule::for_root(),
HttpModule::for_root(None),
SeaOrmModule::for_root(None),
SeaOrmDatabaseModule,
RedisModule::for_root(None),
RedisQueueModule,
AudioHttpModule,
AudioScheduleModule,
])]
pub struct ApiModule;

That apps/api is exactly this shape: producer-only. Its HTTP controller and scheduled timer both call AudioService::enqueue_transcode, which pushes through the bound JobProducer onto Redis. No worker runs in this process — that’s the worker binary’s job.

The worker depends on the shared features crate, so AudioController and AudioResolver are compiled in. They are not active: the worker doesn’t import AudioHttpModule or AudioGraphqlModule, so the access graph never marks those providers as reachable. Every transport — HttpTransport, RedisWorker, GraphqlTransport — filters its inventory through the ReachableProviders set the access graph computes from the running app’s root. Linked but unreachable ⇒ inert, with a boot tracing::warn if the discovery layer expected them to appear.

The same property cuts the other way: the API binary’s AudioQueueModule is not imported, so its #[process] methods sit in the binary as dead code. No worker is spawned for audio in the API. Even if you accidentally imported RedisWorkerModule in the API, omitting AudioQueueModule would keep AudioProcessor::transcode out of the reachable set — RedisWorker::serve would idle until shutdown.

This is what makes splitting producer and consumer across deploys cheap. The features crate is one library, the binaries are compositions of it, and module-gating is the line that decides what each binary actually does at runtime. See fundamentals/modules for the access graph that enforces this — boot fails with AccessGraphError if a provider injects something its module doesn’t own or import transitively.

Terminal window
$ nestrs run dev worker
INFO nest_rs::app: attached module-contributed transport transport="RedisWorker"
INFO nest_rs::queue: registered queue processor processor="AudioProcessor::transcode" queue="audio" retries=3

One attached line per transport, one registered line per discovered method that survived the reachability filter. The producer-only API’s boot opens the same connection, but logs no attached transport=RedisWorker and no registered queue processor lines — the worker side never wakes up.

  • crates/nest-rs-redis/src/module.rs — RedisModule, RedisSetup::collect (config resolution + the one connection).
  • crates/nest-rs-redis/src/queue/module.rs — RedisQueueModule binding RedisQueueProducer as dyn JobProducer over it.
  • crates/nest-rs-redis/src/worker/module.rs — RedisWorkerModule contributing the RedisWorker transport.
  • crates/nest-rs-redis/src/worker/consumer.rs — RedisWorker::configure filtering ProcessMethod by ReachableProviders.
  • apps/api/, apps/worker/ — producer-only and worker shapes side by side.
  • fundamentals/modules — boot phases, imports, dynamic modules, the access graph.