modrun 1.1.0

Modular application composer for Tokio: domain modules, typed wiring, lifecycle
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
# modrun

[![crates.io](https://img.shields.io/crates/v/modrun.svg)](https://crates.io/crates/modrun)
[![docs.rs](https://docs.rs/modrun/badge.svg)](https://docs.rs/modrun)

[English]README.md | [简体中文]README_zh.md

**A modular application composer for Rust.**

```toml
[dependencies]
modrun = "1.0"
```

Build large Tokio services as domain-oriented modules with constructor injection,
explicit module boundaries, and coordinated lifecycle — wired once at the composition root.

Requires **Rust 1.85** or newer (edition 2024). This is not a general-purpose DI
container: there are no string qualifiers, no request-scoped objects, no annotations,
no auto-scanning, and no `get<T>()` after the graph has been built. Two of the same
type use newtypes; swap test doubles with [`supply`](#concepts) at the composition root.
**API stable since 1.0.0** — see [CONTRIBUTING.md](CONTRIBUTING.md) for stability and API boundaries.

If you have written a `main` that builds a config, then a pool, then a repo, then a
service, then a server — and a shutdown path that has to unwind all of it in the
right order — modrun is that `main`, written once.

```rust,no_run
use modrun::{Hook, Lifecycle, Modrun};

#[derive(Clone)]
struct Config {
    port: u16,
}

#[derive(Clone)]
struct Server {
    cfg: Config,
}

impl Hook for Server {
    async fn on_start(&mut self) -> modrun::Result<()> {
        println!("listening on {}", self.cfg.port);
        Ok(())
    }

    async fn on_stop(&mut self) -> modrun::Result<()> {
        println!("goodbye");
        Ok(())
    }
}

fn new_config() -> Config {
    Config { port: 8080 }
}

fn new_server(cfg: Config) -> Server {
    Server { cfg }
}

fn boot(lc: Lifecycle, server: Server) -> modrun::Result<()> {
    lc.append(server)
}

#[tokio::main]
async fn main() -> modrun::Result<()> {
    Modrun::builder()
        .provide(new_config)
        .provide(new_server)
        .invoke(boot)
        .run()
        .await
}
```

`run()` builds the graph, runs every OnStart hook, waits for an OS signal or
[`Shutdowner`](https://docs.rs/modrun/latest/modrun/struct.Shutdowner.html), then runs every OnStop hook in reverse.
With the default `signal` feature, handlers are installed at the start of
`run()` on **Unix** (Ctrl-C / SIGTERM) and **Windows** (Ctrl-C / Ctrl-Break /
Ctrl-Close / Ctrl-Shutdown); on other targets only [`Shutdowner`](https://docs.rs/modrun/latest/modrun/struct.Shutdowner.html)
unblocks `run()`. A shutdown request during build or start cancels that phase
and unwinds hooks that already started, plus any stop-only hooks already
registered (even if OnStart never ran). A timeout or hook failure still returns
an error; a concurrent shutdown does not turn that into `Ok(())`.
Disable the default `signal` feature if you only use `start()` or wait on
[`Shutdowner::wait`](https://docs.rs/modrun/latest/modrun/struct.Shutdowner.html#method.wait) yourself.

## Concepts

### Five wiring verbs

Most applications only need five registration concepts. Pick the method that matches
your function signature (sync / async / fallible) — see the [API matrix](#api-matrix) below.

**Entry points** — both are fluent wiring builders:

| Level | Start with | Then |
|-------|------------|------|
| Composition root | `Modrun::builder()` | `.module(...)`, `.invoke(boot)`, `.run()` / `.start()` |
| Domain module | `Module::builder("name")` | `.provide*`, `.invoke(...)`, return via `.module(...)` |

| Verb | Role |
|------|------|
| [`provide`]#provide-variants | Register a constructor; dependencies come from function parameters |
| [`supply`]#supply | Hand in a value you already have; skip the constructor |
| [`invoke`]#invoke | Pull the graph; invoker parameters are the roots of what gets built |
| [`module`]#modules | Group domain wiring under a name with a private scope |
| [`provide_group`]#groups | Contribute one member to a [`Group<T>`]https://docs.rs/modrun/latest/modrun/struct.Group.html aggregate |

### API matrix

How to pick a registration method (application code only; no `*_dyn`):

| | Sync | `Result` | Async | Async `Result` |
|---|------|----------|-------|----------------|
| **Singleton** [`provide`]#provide | `provide` | `provide_result` | `provide_async` | `provide_result_async` |
| **Singleton, module-private** | `provide_private` | `provide_result_private` | `provide_async_private` | `provide_result_async_private` |
| **Group member** [`provide_group`]#groups | `provide_group` | `provide_group_result` | `provide_group_async` | `provide_group_result_async` |
| **Pre-built value** | `supply` / `supply_private` | — | — | — |
| **Graph root** [`invoke`]#invoke | `invoke` *(also accepts `Result`)* | *(same)* | `invoke_async` | *(same)* |

[`invoke`](#invoke) does **not** use `_result` suffixes: a fallible invoker returns
`Result<(), E>` from plain `invoke` / `invoke_async`. Only constructors split
`provide` vs `provide_result` at compile time.

Only [`ModrunBuilder`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html):
[`init_group`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.init_group),
[`require_group`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.require_group),
[`supply_group`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.supply_group).
When at least one module calls `provide_group`, an invoker that depends on `Group<T>` usually
does **not** need `init_group` — use `init_group` / `require_group` only for empty groups or
when you must fail the build if no member registered.

### Common patterns

Three shapes cover most apps. Each maps to an example:

| Pattern | When | Example |
|---------|------|---------|
| **Domain module** | Hide internal types; expose only services | [`basic`]examples/basic.rs — `provide_private` + public `provide` + `invoke` |
| **Group** | Many modules contribute the same element type | [`handlers`]examples/handlers.rs — `provide_group` → inject `Group<T>` at root |
| **Wrapper** | Metrics, naming, timeouts without a `decorate` API | [`wrap`]examples/wrap.rs — compose in one ctor, or `provide_private` + newtype |

**Domain module** — private repo, public service, module-local invoker:

```text
fn user_domain() -> Module {
    Module::builder("user")
        .provide_private(new_user_repo)
        .provide(new_user_service)
        .invoke(register_user_hooks)
}

Modrun::builder().module(user_domain()).invoke(boot).run().await
```

**Group** — plugins, routes, handlers from many modules:

```text
Modrun::builder()
    .module(Module::builder("user").provide_group(user_routes))
    .module(Module::builder("order").provide_group(order_routes))
    .invoke(|routes: Group<Route>| mount(routes))
```

**Wrapper** — see [Cross-cutting concerns](#cross-cutting-concerns-wrapper-constructors) and [`wrap.rs`](examples/wrap.rs).

Replaceable dependencies and test doubles belong at the **composition root** with
[`supply`](#supply), not inside a module — see [`swap`](examples/swap.rs).

### Provide variants

Pick the method that matches your constructor signature — not a separate DI concept:

| Method | Constructor |
|--------|-------------|
| `provide` | `fn(...) -> T` |
| `provide_result` | `fn(...) -> Result<T, E>` |
| `provide_async` | `async fn(...) -> T` |
| `provide_result_async` | `async fn(...) -> Result<T, E>` |

Returning `Result` from plain `provide` is a **compile error** (modrun points you at
`provide_result`). Group members follow the same four-way split with a `provide_group_*`
prefix. Type-erased `provide_dyn` / `invoke_dyn` / `provide_group_dyn` live under
[`modrun::__wiring`](https://docs.rs/modrun/latest/modrun/__wiring/index.html) for
wrapper libraries only (`#[doc(hidden)]`, not shown in application docs).

### Cross-cutting concerns (wrapper constructors)

modrun will not add a `decorate` API. Horizontal concerns (metrics, named loggers,
timeouts) are ordinary Rust functions. Each type can only be `provide`d once — compose
wrappers inside **one** constructor, or return a **newtype / service struct** from a
module (`provide_private` for the raw value, public `provide` for the wrapped type):

```rust
use modrun::{Modrun, Module};

#[derive(Clone, PartialEq, Eq)]
struct Logger { name: &'static str }

#[derive(Clone, PartialEq, Eq)]
struct AppLogger(Logger); // public binding ≠ private raw type

fn new_logger() -> Logger { Logger { name: "default" } }
fn named(log: Logger) -> Logger { /* ... */ log }
fn new_app_logger(log: Logger) -> AppLogger { AppLogger(named(log)) }

fn logging() -> Module {
    Module::builder("logging")
        .provide_private(new_logger)
        .provide(new_app_logger)
        .invoke(|log: AppLogger| { /* ... */ })
}

Modrun::builder().module(logging());
```

At the composition root, one composed constructor is enough:
`provide(|| named(new_logger()))` or a named `fn app_logger() -> Logger { ... }`.
See [`examples/wrap.rs`](examples/wrap.rs).

### Provide

**`provide`** registers a constructor. Nothing is built until something asks for it,
and each type is built at most once. Constructors that return `Result<T, E>` must
use `provide_result` (or `provide_result_async`); handing one to plain `provide`
is a compile error. `provide_async` and `provide_result_async` take `async fn`s.
When several independent constructors are needed at once, modrun builds them by
DAG layer: **async** constructors in the same layer are polled concurrently on one
task; **sync** constructors run inside `construct()` and defer creation of later
futures in that layer. Shared dependencies still run first, then dependents
continue in dependency order.

### Supply

**`supply`** hands the container a value you already have, skipping the constructor.

### Invoke

**`invoke`** pulls the graph. An invoker's parameters are the roots of what gets
built, so a type nobody invokes (directly or transitively) is never constructed.
A migrator or background consumer that is only `provide`d will stay inert until
something `invoke`s it (or a type that depends on it). Invokers run once, during
build, and are where you normally register lifecycle hooks.

**`Lifecycle`** collects start/stop hooks. It is injected automatically, so any
constructor or invoker can take it as a parameter. OnStart hooks run in registration
order; OnStop hooks run in reverse. If a start hook fails or start is cancelled,
hooks that already finished OnStart are stopped again before the error propagates —
and any OnStop failures are retained in the returned error. A start hook that fails
or is cancelled mid-flight does **not** run its own OnStop. A stop-only hook (one
without OnStart) is considered active immediately and is included in unwind.
Register hooks during `invoke` (or from an OnStart factory);
`append` returns an error if start has already finished or stop has begun. An
invoker may itself return `modrun::Result<()>`, so give `boot` that return type
and hand the `append` result straight back instead of unwrapping it.
Implement [`Hook`](https://docs.rs/modrun/latest/modrun/trait.Hook.html) on a
struct when start and stop share state (`&mut self`). For a struct that only
implements OnStop, override [`has_start`](https://docs.rs/modrun/latest/modrun/trait.Hook.html#method.has_start)
to return `false` so trailing activation still runs it after a failed start.
For one-off closures, use
[`hook()`](https://docs.rs/modrun/latest/modrun/fn.hook.html); OnStop callbacks
must be repeatable (`Fn`) when they capture shared state — clone an [`Arc`](std::sync::Arc)
inside the closure on each call. Hook and constructor
errors use [`Error`](https://docs.rs/modrun/latest/modrun/enum.Error.html) (`thiserror`); hooks should return
[`Error::hook`](https://docs.rs/modrun/latest/modrun/enum.Error.html#method.hook) or
[`Error::io`](https://docs.rs/modrun/latest/modrun/enum.Error.html#method.io) so the original
failure stays on [`std::error::Error::source`](std::error::Error::source). There is no
[`From<std::io::Error>`](std::convert::From); wrap I/O with [`Error::io`](https://docs.rs/modrun/latest/modrun/enum.Error.html#method.io)
(`bind(addr).await.map_err(|e| Error::io(format!("bind {addr}"), e))?`).

Override [`Hook::name`](https://docs.rs/modrun/latest/modrun/trait.Hook.html#method.name) from the default
`"unnamed"` for clearer logs and errors. Constructors and invokers accept at most eight parameters; group extra
dependencies in a struct rather than stretching arity.

Hook futures must be cancellation-safe. A start/stop timeout drops the in-progress
future, but it cannot cancel detached tasks created with `tokio::spawn`. Prefer
[`task()`](https://docs.rs/modrun/latest/modrun/fn.task.html) for workers, or
[`task_with()`](https://docs.rs/modrun/latest/modrun/fn.task_with.html) when
bind/listen must finish during OnStart (see the axum example). Both signal
[`Stopped`](https://docs.rs/modrun/latest/modrun/struct.Stopped.html) on OnStop,
join, and abort if the hook is dropped mid-flight. If that background work
returns `Err` or panics after start has succeeded, shutdown is requested
automatically so `run()` does not wait forever for a signal. Custom tasks
spawned with [`tokio::spawn`](https://docs.rs/tokio/latest/tokio/fn.spawn.html)
must still call
[`Shutdowner::shutdown`](https://docs.rs/modrun/latest/modrun/struct.Shutdowner.html#method.shutdown).
Hook panics are treated as fatal programming errors and may bypass lifecycle
unwind (logged as `panicked`).

**`Shutdowner`** is also injected automatically. Calling `shutdown()` unblocks `run()`
from inside the app, which is how you shut down in response to something other than a
signal. During build/start that cancellation is cooperative (next `.await`).

The container is dropped when build finishes. Singletons stay alive only through
values you capture in hooks (or other `Clone` handles taken during invoke). modrun
wires startup; it is not a live service locator. Build is not transactional: if a
later invoker fails, earlier constructors may already have run their side effects
for that failed build call.

Dependencies must be `Clone` to inject by value, because singletons are cached
by type and cloned on inject. Prefer `Arc<T>` when a type is large, shared across
many constructors, or injected more than once — `Arc<T>` is registered as an alias,
so a constructor that returns `T` can be injected as `Arc<T>` without `T: Clone`.
Put shared mutable state behind an `Arc` inside your type:

```rust
use std::sync::Arc;

struct DbInner;

#[derive(Clone)]
struct Db(Arc<DbInner>);
```

Since the cache key is the type, a type can only be provided once. To wire two of the
same thing — a primary and a replica pool, say — give each its own newtype:

```rust
# use std::sync::Arc;
# struct PgPool;
#[derive(Clone)]
struct PrimaryDb(Arc<PgPool>);
#[derive(Clone)]
struct ReplicaDb(Arc<PgPool>);
# let _ = std::any::type_name::<(PrimaryDb, ReplicaDb)>();
```

Connect pools with `provide_result_async` at the composition root (so tests can
`supply` a fake). Do not open connections inside OnStart unless you want pool
failure to look like a start-hook failure rather than a constructor error.

## Performance

modrun cost is almost entirely **cold start**: validate the graph, run constructors
and invokers once, then run lifecycle hooks. There is no service locator or
background wiring after `start()` — the container is dropped when build finishes.

Typical overhead is small compared to opening databases, binding sockets, or loading
config. To keep it that way:

* **Prefer `Arc<T>`** for large or widely shared singletons — injection clones cached
  values; `Arc<T>` is a cheap pointer clone (see above).
* **Prefer `Arc<Group<T>>` or `Group<Arc<T>>`** when group members are heavy — aggregating
  `Group<T>` may clone each member (`T: Clone` is required).
* **Keep modules shallow** when possible — private bindings walk the scope ancestor chain
  on each resolve (usually a handful of lookups).
* **Disable framework logging in production** — `default-features = false` skips the
  optional subscriber; without a subscriber, `tracing` events are no-ops. Use
  [`.no_banner()`]https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.no_banner
  in tests and benchmarks.
* **Do not enable `dot_graph()` on every start** — DOT export validates and renders the
  full graph; use it for debugging or CI, not hot paths.

Independent async constructors in the same wave run concurrently on the current task
(no `tokio::spawn` storm). Run `cargo bench --bench build` locally to compare group
sizes and module depth.

## Modules

A `Module` groups related wiring under a name and gives it a private scope.
`provide_private` keeps a type invisible outside the module, so two domains can each
have their own `Repo` without colliding:

```rust
use modrun::{Modrun, Module};

# #[derive(Clone)] struct UserRepo;
# #[derive(Clone)] struct UserService;
# fn new_user_repo() -> UserRepo { UserRepo }
# fn new_user_service(_r: UserRepo) -> UserService { UserService }
# fn boot_user(_s: UserService) {}
fn user_domain() -> Module {
    Module::builder("user")
        .provide_private(new_user_repo)
        .provide(new_user_service)
        .invoke(boot_user)
}

# #[tokio::main]
# async fn main() -> modrun::Result<()> {
Modrun::builder()
    .module(user_domain())
    .start()
    .await?
    .stop()
    .await
# }
```

Private types are visible to the module that declared them and to its nested modules.
Everything registered with plain `provide` or `supply` is visible everywhere, wherever
it was declared. `provide_private` / `supply_private` exist only on [`Module`](https://docs.rs/modrun/latest/modrun/struct.Module.html),
not on the root builder.

## Groups

Multiple modules can each contribute one value of the same type; a consumer receives
them as [`Group<T>`](https://docs.rs/modrun/latest/modrun/struct.Group.html) (not
`Vec<T>`). Register members with [`provide_group`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.provide_group)
(and `provide_group_async` / `provide_group_result` / `provide_group_result_async`
when needed). Members are aggregated in registration order and do **not** occupy the
singleton slot for `T`, so they can coexist with a separate `provide` of the same type.

```rust
use modrun::{Group, Modrun, Module};

# #[derive(Clone, PartialEq, Eq)] struct Handler(&'static str);
# fn user_handler() -> Handler { Handler("user") }
# fn order_handler() -> Handler { Handler("order") }
# fn boot(_: Group<Handler>) {}

Modrun::builder()
    .module(Module::builder("user").provide_group(user_handler))
    .module(Module::builder("order").provide_group(order_handler))
    .invoke(boot)
# ;
```

Inject `Group<T>` (or `Arc<Group<T>>`) in an invoker or constructor; iterate with
`for item in group`. Members and injected groups require `T: Clone`; prefer
`Arc<Group<T>>` when several consumers need the same collection, or return
`Arc<T>` / `Arc<dyn Trait>` from group member constructors when values are heavy.
When one or more modules already `provide_group`, root invokers that take `Group<T>` aggregate
members automatically — no `init_group` call required (see [`handlers`](examples/handlers.rs)).
With **no** members, register the empty group with
[`init_group`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.init_group)
or [`require_group`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.require_group)
(`require_group` also fails the build if the group stays empty; only the composition
root can call it). `T` is inferred from the constructor return type (no `provide_group::<T>`
turbofish); for a trait-object group, return `Arc<dyn Trait>`. A module-private
[`provide_private`](https://docs.rs/modrun/latest/modrun/struct.Module.html#method.provide_private)
of `Group<T>` shadows the aggregated group inside that module — use
[`provide_group`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.provide_group)
to contribute members instead. For two groups of the same element type, use
newtypes (same as duplicate singletons).

**Method matrix** (group members mirror the four `provide_*` variants above):

| Register | Use when |
|----------|----------|
| `provide_group` | sync infallible member ctor |
| `provide_group_result` | sync fallible (`Result`) |
| `provide_group_async` | async infallible |
| `provide_group_result_async` | async fallible |
| `supply_group` | pre-built member value |
| `init_group` / `require_group` | composition root only; empty vs non-empty policy |

## Dependency graph

Export the wiring graph as [Graphviz DOT](https://graphviz.org/doc/info/lang.html) for
documentation or debugging. Validation runs first (missing providers and cycles surface
as errors), but no constructor or invoker runs.

```rust
use modrun::Modrun;

# #[derive(Clone)] struct Config;
# #[derive(Clone)] struct Server;
# fn new_config() -> Config { Config }
# fn new_server(_: Config) -> Server { Server }
# fn boot(_: Server) {}

// Return DOT as a string (no file I/O)
let dot = Modrun::builder()
    .provide(new_config)
    .provide(new_server)
    .invoke(boot)
    .render_dot()?;
# Ok::<(), modrun::Error>(())
```

To write a file before graph construction when starting the app, chain
[`.dot_graph("modrun.dot")`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.dot_graph)
on the builder passed to [`run`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.run).

Nodes show the type, constructor name, and module scope (subgraph per module).
Solid arrows are constructor / invoker dependencies; dotted edges link group members
to their `Group<T>` aggregate. Built-in `Lifecycle` and `Shutdowner` nodes are
omitted for clarity. See [docs/graph-sample.dot](docs/graph-sample.dot) for sample
output. Render with `dot -Tpng modrun.dot -o modrun.png`.

## Logging

Framework events (provide / supply / invoke / construct / OnStart / OnStop) are
emitted through [`tracing`](https://docs.rs/tracing) with target `modrun`, using
[uber/fx](https://github.com/uber-go/fx)-style console lines such as
`[modrun] PROVIDE    my::Type <= my::new`. The same events carry structured
fields (`constructor`, `module`, `elapsed`, `error`, …) so a JSON subscriber
can filter them in production.

[`modrun::logging::init()`](https://docs.rs/modrun/latest/modrun/logging/fn.init.html)
is for examples and local binaries (enabled by default via the `logging`
feature). It writes to stderr, enables ANSI only when stderr is a TTY, and is a
**no-op** if a subscriber is already installed (it will not panic). Production
services should install their own subscriber and skip this helper. Without a
subscriber the events are cheap no-ops:

`RUST_LOG` / per-target filtering comes from the `env-filter` feature (on by
default, pulls `regex-automata` / `matchers`). For console output without those
regex crates, use `default-features = false` with `features = ["logging"]`; the
helper then installs a global `INFO` max level instead.

```rust,no_run
fn main() {
    #[cfg(feature = "logging")]
    modrun::logging::init();
}
```

Set `RUST_LOG=modrun=info` with your own subscriber. Cancelled hooks and
constructors emit `ERROR` lines. A successful stop emits `STOPPED`. Leak
warnings on `RunningApp` go through tracing; debug builds also print to stderr.

## Startup banner

[`ModrunBuilder::run`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.run) and
[`start`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.start) print a modrun
ASCII banner to stderr before wiring (Spring Boot style), and only when stderr
is a TTY. Custom text (or `include_str!("banner.txt")` in your crate) is always
printed to stderr:

```rust,no_run
# use modrun::Modrun;
Modrun::builder()
    .banner("my service")
    // ...
# ;
```

Disable with [`.no_banner()`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.no_banner).
Piped processes and daemons without a TTY skip the default banner automatically.
Tests that run in a terminal should still call `.no_banner()` so captured stderr
stays quiet.

## Failure modes

The graph is checked before anything is constructed, so these are build-time errors
rather than surprises later:

* a provider whose dependency nothing provides, even if nobody uses that provider
* a dependency cycle
* the same type provided twice

At runtime, `build_timeout`, `start_timeout`, and `stop_timeout` (15s by default)
bound graph construction, OnStart, and OnStop respectively. Timeouts are
cooperative: work that yields at `.await` is cancelled when the budget expires.
Synchronous blocking (for example `std::thread::sleep` in a sync invoker,
constructor, or hook) cannot be preempted, but an over-budget success is still
reported as a timeout error rather than `Ok`. The cancellation timer follows
Tokio's clock; the over-budget `Ok` check uses wall-clock `Instant`, so
`tokio::time::pause` in tests can make those two disagree. If a timeout is set more than once
on the builder, the last value wins. `no_build_timeout` / `no_start_timeout` /
`no_stop_timeout` disable the budget. `no_start_timeout` only disables OnStart;
`stop_timeout` still budgets unwind after a failed or cancelled start. When the
budget expires, remaining OnStop hooks are abandoned and the timeout is reported
as an error rather than hanging — connection pools may not get a clean close.
For production start that runs migrations or cache warm-up, set an explicit
`start_timeout` (and `stop_timeout`) instead of relying on the 15s default.

`run()` treats Ctrl-C / SIGTERM / [`Shutdowner`](https://docs.rs/modrun/latest/modrun/struct.Shutdowner.html)
during build or start as a graceful stop: it unwinds hooks that already started
and any registered stop-only hooks, and returns `Ok(())` if cleanup succeeds.
A background [`task`](https://docs.rs/modrun/latest/modrun/fn.task.html) that
fails or panics during start is **not** treated as graceful — `run()` returns
the join error (or `background task failed during start` if unwind reported
success). If unwind then times out, both are retained on
[`Error::CleanupAfterFailure`](https://docs.rs/modrun/latest/modrun/enum.Error.html).
If that phase had already failed, `run()` still returns the failure.
Shutdown and OS signals are cooperative in the same way as timeouts: they take
effect at the next `.await`, so a `shutdown()` from a synchronous OnStart does
not skip later hooks that have not yet yielded. After `RUNNING`, `run()` waits
until a signal or `Shutdowner::shutdown()`; a background [`task`](https://docs.rs/modrun/latest/modrun/fn.task.html)
that fails or panics requests shutdown on its own. Work spawned with
[`tokio::spawn`](https://docs.rs/tokio/latest/tokio/fn.spawn.html) must still
call `shutdown()` or `run()` waits forever. A panic in a
constructor, invoker, or hook is not converted into [`Error`](https://docs.rs/modrun/latest/modrun/enum.Error.html)
and may skip lifecycle unwind (tracing records it as `panicked`).

## Testing

`start()` builds and starts without waiting for a signal, returning a `RunningApp` you
can `stop()` yourself. A background [`task`](https://docs.rs/modrun/latest/modrun/fn.task.html)
that fails after its OnStart has returned does **not** fail `start()` or skip later
hooks; wait on [`Shutdowner`](https://docs.rs/modrun/latest/modrun/struct.Shutdowner.html)
or call `stop()` to observe it. Use [`run`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.run)
when a failed worker should tear the process down:

```rust
# use std::sync::Arc;
# use std::sync::atomic::{AtomicUsize, Ordering};
# use modrun::{hook, Lifecycle, Modrun};
# #[derive(Clone)] struct Hits(Arc<AtomicUsize>);
# fn boot(lc: Lifecycle, hits: Hits) {
#     let n = Arc::clone(&hits.0);
#     lc.append(hook().on_start(move || async move {
#         n.fetch_add(1, Ordering::SeqCst);
#         Ok(())
#     })).unwrap();
# }
# #[tokio::main]
# async fn main() -> modrun::Result<()> {
let hits = Arc::new(AtomicUsize::new(0));
let app = Modrun::builder()
    .no_banner()
    .supply(Hits(Arc::clone(&hits)))
    .invoke(boot)
    .start()
    .await?;
assert_eq!(hits.load(Ordering::SeqCst), 1);
app.stop().await
# }
```

Replaceable types belong at the composition root (`provide` / `supply`), not
inside the domain [`Module`](https://docs.rs/modrun/latest/modrun/struct.Module.html).
If the module also `provide`s `Repo`, a test `supply(FakeRepo)` will hit
`already provided`. Runnable version: `cargo run --example swap`.

```rust
# use modrun::{Modrun, Module};
# #[derive(Clone)] struct Repo;
# #[derive(Clone)] struct Service;
# fn connect_repo() -> Repo { Repo }
# fn fake_repo() -> Repo { Repo }
# fn new_service(_: Repo) -> Service { Service }
# fn boot(_: Service) {}
fn user_domain() -> Module {
    Module::builder("user")
        .provide(new_service)
        .invoke(boot)
}

# #[tokio::main]
# async fn main() -> modrun::Result<()> {
// production
Modrun::builder()
    .no_banner()
    .provide(connect_repo)
    .module(user_domain())
    .start()
    .await?
    .stop()
    .await?;

// test: same module, fake at the root
Modrun::builder()
    .no_banner()
    .supply(fake_repo())
    .module(user_domain())
    .start()
    .await?
    .stop()
    .await
# }
```

Tests that sleep in hooks should set an explicit timeout (or `no_start_timeout`);
the default budget is 15s. Prefer [`.no_banner()`](https://docs.rs/modrun/latest/modrun/struct.ModrunBuilder.html#method.no_banner)
so stderr stays quiet when tests run in a terminal.

## Errors

Graph problems fail before constructors run. Typical `Display` text:

* `type already provided: my::Config`
* `invoker in module '<root>' needs a dependency nothing provides: my::Db`
* `provider for my::Svc in module 'user' needs a dependency nothing provides: my::Repo`
* `dependency cycle detected involving: A -> B -> A`
* `application start timed out after 15s`
* `application stop timed out after 15s while unwinding`
* `invoker my::boot failed: …`
* `hook 'http.serve' failed: …`
* `background task failed during start`
* `required group is empty: modrun::Group<my::Route>`
* `provide_group_dyn type mismatch: expected my::Route, got alloc::string::String`
* `invoker in module '<root>' needs a dependency nothing provides: modrun::Group<my::Route>; register the group with init_group, provide_group, or require_group`

Constructor and hook failures keep the original error on
[`std::error::Error::source`](https://doc.rust-lang.org/std/error/trait.Error.html#tymethod.source).
Every hook has a [`Hook::name`](https://docs.rs/modrun/latest/modrun/trait.Hook.html#method.name)
in logs and errors (default `"unnamed"`; [`task`](https://docs.rs/modrun/latest/modrun/fn.task.html) sets its own).
Several OnStop failures become [`MultipleStopError`](https://docs.rs/modrun/latest/modrun/struct.MultipleStopError.html).
If unwind fails after an earlier phase error, both are retained on
[`Error::CleanupAfterFailure`](https://docs.rs/modrun/latest/modrun/enum.Error.html).

## When not to use this

* Request-scoped objects (one instance per HTTP request)
* Looking up a type from a live container after `start()`
* String-named bindings (`"primary"` vs `"replica"`) — use newtypes
* Runtime-agnostic libraries; modrun targets Tokio

## Examples

```bash
cargo run --example basic    # domain modules, private deps, an async constructor
cargo run --example handlers # multiple modules contribute to a Group
cargo run --example worker   # newtype pools + task that selects on Stopped
cargo run --example swap     # supply a fake at the composition root (tests)
cargo run --example wrap     # wrapper constructors for cross-cutting concerns
cargo run --example axum     # HTTP server: task_with binds in OnStart, then serve
```

## License

MIT