Skip to content

Commit d1afbbc

Browse files
leorossigithub-actions[bot]
authored andcommitted
[automated commit] Bump docs to versions 3.66.0, 2.75.2
1 parent 84af26d commit d1afbbc

251 files changed

Lines changed: 1562 additions & 29 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
---
2+
title: Application Lifecycle
3+
label: Application Lifecycle
4+
---
5+
6+
# Application Lifecycle
7+
8+
This page explains what happens between `wattpm start` and your application serving a request, what
9+
happens on the way back down, and what the runtime does when something goes wrong in between.
10+
11+
Understanding this is what turns a startup log from noise into a diagnosis. The five-line failure you
12+
get when an application will not boot is much easier to read once you know which phase it came from.
13+
14+
## Two lifecycles, not one
15+
16+
Watt has a runtime lifecycle and, nested inside it, a lifecycle per application worker. They use
17+
similar names, which is a common source of confusion.
18+
19+
The **runtime** moves through `init``starting``started``stopping``stopped`, with
20+
`closing`/`closed` for teardown and `errored` for a failure it could not recover from.
21+
22+
Each **worker** moves through `init``starting``started``stopped`, with `start:error` when its
23+
capability throws during startup.
24+
25+
A worker reaching `started` does not mean the runtime has; the runtime reaches `started` only once
26+
every application it was asked to start has. Conversely, the runtime can be `started` while an
27+
individual worker is cycling through restarts.
28+
29+
## Phase 1 — Runtime initialisation
30+
31+
Before any application code runs, the runtime sets up everything that exists once for the whole
32+
process:
33+
34+
1. The management API starts, if configured.
35+
2. The logger is created. This happens early and deliberately, so that everything after it —
36+
extensions, health servers, application startup — logs through the same destination.
37+
3. Extensions are loaded. This is before any worker is created, so that custom ITC handlers an
38+
extension registers are available to every worker, and so that readiness and liveness checks are
39+
registered before the probe server starts listening.
40+
4. Prometheus and health-probe servers start.
41+
5. Applications are registered and their worker threads are created — created, not started.
42+
6. The undici dispatcher is installed and the [scheduler](../guides/scheduler.md) starts.
43+
44+
The runtime is now `init`. Nothing is serving yet.
45+
46+
## Phase 2 — Dependency resolution and ordered startup
47+
48+
Watt does not start applications in configuration order, and it does not start them all at once. It
49+
computes an order.
50+
51+
**Dependencies are collected.** The runtime asks each application, over ITC, for its dependencies.
52+
Most applications report none. A gateway is the interesting case: it reports every local application
53+
it is configured to compose, which it derives from its own `gateway.applications` list. That means a
54+
gateway's dependencies are correct without you declaring anything.
55+
56+
You can also declare dependencies explicitly with the `dependencies` property on an application, for
57+
the case where application A calls application B over the mesh during its own startup.
58+
59+
**The graph is sorted.** The runtime topologically sorts the applications. A cycle is a hard error —
60+
`ApplicationsDependenciesCycleError` — not a warning, because there is no order that satisfies it.
61+
62+
**The sort is grouped into levels.** This is the part worth knowing. Rather than starting
63+
applications one at a time in sorted order, the runtime groups them so that every application in a
64+
level has all of its dependencies in earlier levels. Levels start sequentially; applications within a
65+
level start in parallel.
66+
67+
So a system with a gateway over three independent APIs starts the three APIs concurrently, waits for
68+
that whole level, then starts the gateway. Startup time is the depth of your dependency graph, not
69+
the number of applications in it.
70+
71+
**Each capability then waits for its own dependencies.** Belt and braces: during its `init`, a
72+
capability calls `waitForDependenciesStart`, so it does not merely start after its dependencies were
73+
*launched* — it waits until they report `started`.
74+
75+
Once the last level is up, the runtime is `started` and the entrypoint is listening.
76+
77+
## Phase 3 — Serving
78+
79+
At this point the interesting behaviour is per request rather than per lifecycle, and it is covered
80+
in [The Multithread Model](./multithread-model.md): mesh routing over `MessagePort`, round-robin
81+
across an application's workers, and shared-nothing state.
82+
83+
One lifecycle-adjacent thing does keep running: health checks, described below.
84+
85+
## Phase 4 — Shutdown
86+
87+
Shutdown is not startup in reverse, and the difference matters.
88+
89+
The **entrypoint is stopped first**, deliberately and on its own. It is the only application with a
90+
public socket, so stopping it first means no new external requests enter the system while everything
91+
else is still up and able to finish in-flight work.
92+
93+
Then extension stop hooks run, so control-plane extensions can settle work and hand off state before
94+
the applications they were managing go away.
95+
96+
Then the remaining applications stop. Each capability calls `waitForDependentsStop` before shutting
97+
down, so an application does not disappear while something that depends on it is still finishing.
98+
99+
Finally the mesh interceptor and the broadcast channel close, and the runtime is `stopped`.
100+
101+
## When a worker crashes
102+
103+
A worker that exits unexpectedly is restarted. Two configuration values govern this, and their
104+
defaults differ between development and production in a way that is easy to misread.
105+
106+
`restartOnError` defaults to `true`. What `true` resolves to depends on the mode:
107+
108+
- **Development**: `true` becomes a **5000 ms** delay between attempts.
109+
- **Production**: the delay is forced to be effectively immediate.
110+
111+
Setting it to `false` or `0` disables restarts entirely.
112+
113+
Restarts are bounded: **5 bootstrap attempts**. After that the runtime gives up on the worker. This
114+
is the mechanism behind a log sequence that anyone who has broken a startup path will recognise:
115+
116+
```
117+
Failed to start worker 0 of the application "next": The worker 0 of the application "next"
118+
exited prematurely with error code 1
119+
Attempt 1 of 5 to start the worker 0 of the application "next" again will be performed in 5000ms ...
120+
```
121+
122+
Five of those, five seconds apart, is a worker whose capability throws during startup — not a
123+
transient fault. When you see it, the useful question is what the capability's own startup is doing,
124+
because the runtime has already told you everything it knows.
125+
126+
:::note
127+
The runtime reports the worker's **exit code**, which is often less informative than what the
128+
application printed on its way out. If the underlying error is not visible in the Watt logs, run the
129+
framework's own dev command directly in the application directory — the error usually appears
130+
immediately there.
131+
:::
132+
133+
Each restarted worker gets a **new worker index** rather than reusing the old one, which is why the
134+
log above shows worker 0, then worker 1, then worker 2 for what is conceptually the same worker
135+
restarting. This is intentional — it keeps identifiers unique — but it does mean "worker 4" in a
136+
crash loop is not the fifth worker of a five-worker application.
137+
138+
## When a worker is unhealthy but alive
139+
140+
Crashing is the easy failure. The harder one is a worker that is still running but no longer useful:
141+
event loop pinned, heap exhausted, health checks not returning. Watt polls each worker and replaces
142+
it when it stays bad.
143+
144+
The defaults:
145+
146+
| Setting | Default | Meaning |
147+
| --- | --- | --- |
148+
| `enabled` | `true` | Health checking is on |
149+
| `interval` | `30000` ms | How often a worker is checked |
150+
| `gracePeriod` | `30000` ms | Delay before the first check, so slow startups are not punished |
151+
| `maxUnhealthyChecks` | `10` | Consecutive bad checks before replacement |
152+
| `maxELU` | `0.99` | Event loop utilisation ceiling |
153+
| `maxHeapUsed` | `0.99` | Fraction of the heap limit in use |
154+
| `maxHeapTotal` | 4 GB | Absolute heap ceiling |
155+
| `maxYoungGeneration` | 128 MB | Young generation ceiling |
156+
157+
Two properties of this design are worth drawing out.
158+
159+
**The counter is consecutive, not cumulative.** A single healthy check resets it to zero. A worker
160+
that spikes over `maxELU` for one interval and recovers is left alone; only sustained badness
161+
triggers replacement. With the defaults, that means roughly five minutes of continuous unhealthiness
162+
before a worker is replaced.
163+
164+
**A failed health collection counts as unhealthy.** If the runtime cannot get an answer from a
165+
worker, that is not skipped — it is a bad check. This is what catches a genuinely stuck worker, which
166+
by definition cannot report that it is stuck.
167+
168+
Replacement is not restart: the runtime starts a fresh worker and retires the old one, so capacity is
169+
not lost while the replacement boots.
170+
171+
## Reading the lifecycle in practice
172+
173+
Three questions locate almost any startup problem in this model:
174+
175+
1. **Did the runtime reach `init`?** If not, the problem is configuration, logging, or an extension —
176+
no application code has run yet.
177+
2. **Which level did startup stop at?** An application stuck waiting is usually waiting on a
178+
dependency that never reached `started`. Look at the dependency, not the application reporting the
179+
problem.
180+
3. **Is it crashing or unhealthy?** A crash gives you `exited prematurely` and a bounded restart
181+
sequence. Unhealthiness gives you `is unhealthy ... Replacing it`. They have different causes and
182+
different fixes.
183+
184+
## Related reading
185+
186+
- [Watt Architecture](./watt-architecture.md) — why the runtime supervises rather than just spawns
187+
- [The Multithread Model](./multithread-model.md) — what a worker is and what it shares
188+
- [Runtime configuration](../reference/runtime/configuration.md)`health`, `restartOnError`, `workers`
189+
- [Troubleshooting](../reference/troubleshooting.md) — symptom-first debugging
190+
- [Dynamic Workers](../guides/dynamic-workers.md) — scaling workers on event loop utilisation
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
---
2+
title: The Modular Monolith
3+
label: The Modular Monolith
4+
---
5+
6+
# The Modular Monolith
7+
8+
"Modular monolith" is the phrase Watt's documentation leads with, and it is doing real work rather
9+
than acting as a slogan. This page explains what the term means, why it describes Watt accurately,
10+
and — more usefully — when you should stop using one.
11+
12+
If you want to build one rather than reason about one, go to
13+
[Build a modular monolith](../guides/build-modular-monolith.md), which walks through a complete
14+
multi-application example.
15+
16+
## The term
17+
18+
A **monolith** is one deployment unit. A **modular** system has enforced internal boundaries. Most
19+
architectures give you one or the other:
20+
21+
- A conventional monolith is one deployment unit with advisory boundaries. Modules are separated by
22+
directory layout and code review. Nothing stops one part importing another's internals, and over a
23+
few years, something always does.
24+
- Microservices give you enforced boundaries — you physically cannot import across a network — at the
25+
cost of one deployment unit per boundary, and all the machinery that implies.
26+
27+
A modular monolith is the combination that is usually assumed to be unavailable: one deployment unit,
28+
enforced boundaries. The boundaries are real, not conventional, but you still ship one thing.
29+
30+
## Why Watt is one, mechanically
31+
32+
The claim rests on the worker thread model. Each application runs in its own V8 isolate, so the
33+
boundary between applications is enforced by the runtime rather than by discipline:
34+
35+
- There is no import path from one application to another's code. Separate module registries.
36+
- There is no shared object graph. Separate heaps.
37+
- The only way to reach another application is its HTTP interface over the mesh, at
38+
`http://<id>.plt.local`.
39+
40+
That last point is the important one. The boundary is not merely enforced — it is enforced *in the
41+
shape of a network interface*. An application interacts with its neighbour exactly as it would if
42+
that neighbour were a service in another datacentre: a URL, a request, a response, a status code.
43+
44+
Meanwhile the deployment story stays monolithic. One `npm install`, one build, one process, one
45+
container, one thing to deploy and roll back.
46+
47+
## The property that makes it worth doing
48+
49+
Most architectural decisions are hard to reverse. This one is not, and that is the actual argument
50+
for it.
51+
52+
Because applications already talk over HTTP, extracting one to its own deployment does not require
53+
rewriting how it is called. The call site is a URL either way. Moving an application out means giving
54+
it its own runtime and pointing callers at a real hostname instead of a `.plt.local` one. The
55+
application's own code — routes, handlers, business logic — does not change at all.
56+
57+
This inverts the usual sequencing problem. The conventional advice is "start with a monolith, extract
58+
services when you need to", which is good advice with a bad failure mode: by the time you need to
59+
extract, the boundaries have eroded and the extraction is a rewrite. Watt's boundaries cannot erode,
60+
because the runtime enforces them from day one. The extraction stays cheap indefinitely.
61+
62+
You are deferring a decision rather than making one. You get to find out where your real service
63+
boundaries are by operating the system, instead of guessing during design.
64+
65+
## What you are actually trading
66+
67+
Being concrete about the cost is more useful than repeating the benefit.
68+
69+
**You get, compared to a conventional monolith:**
70+
71+
- Enforced boundaries and per-application dependency trees — two applications can use incompatible
72+
versions of the same library
73+
- Per-application scaling — give the expensive one four workers and the rest one
74+
- Event loop isolation — one application's CPU-bound work does not stall the others
75+
- A migration path that stays open
76+
77+
**You get, compared to microservices:**
78+
79+
- One deployment unit, one build, one rollback
80+
- No service discovery, no port allocation, no internal TLS, no network hop
81+
- Merged logs and connected traces without assembling a pipeline first
82+
- Far cheaper internal calls — a `MessagePort` message rather than a TCP round trip
83+
84+
**You give up, compared to microservices:**
85+
86+
- **Independent deployment.** Everything ships together. This is the big one.
87+
- **Process-level fault isolation.** Threads share a process. A native segfault or a process-wide OOM
88+
takes down every application, as discussed in [The Multithread Model](./multithread-model.md).
89+
- **Per-application resource limits.** No per-application CPU quota or memory cgroup.
90+
- **Language diversity.** Worker threads are a Node.js mechanism.
91+
- **Independent technology upgrades.** One Node.js version for everyone.
92+
93+
## When to stop
94+
95+
A modular monolith is a good default, not a permanent answer. Extract an application to its own
96+
deployment when one of these becomes true — and note that none of them are about code size:
97+
98+
- **Release cadence diverges.** Two parts of the system need to ship on genuinely independent
99+
schedules, because different teams own them or because their risk profiles differ.
100+
- **Resource profiles diverge sharply.** One application wants 16 GB and a GPU; the rest want 512 MB.
101+
A shared process cannot express that.
102+
- **A fault domain must be isolated.** An unstable native dependency, or untrusted third-party code,
103+
that you cannot allow to take the process down.
104+
- **Compliance draws a boundary.** Payment or health data that must live in a separately audited
105+
deployment.
106+
- **Scaling limits are reached.** You need more capacity than one process on one machine can provide,
107+
and running several identical Watt instances behind a load balancer is no longer the right shape.
108+
109+
Notice what is not on this list: number of applications, number of engineers, lines of code, or the
110+
system feeling "big". Those are the usual triggers for reaching for microservices, and none of them
111+
are reasons on their own — they are reasons to want enforced boundaries, which you already have.
112+
113+
## Extracting, when the time comes
114+
115+
The move is mechanical, which is the whole point:
116+
117+
1. Give the application its own Watt runtime and deploy it.
118+
2. Change callers from `http://products.plt.local` to the real hostname. If you routed through
119+
configuration or an environment variable rather than hardcoding the internal URL, this is a
120+
config change.
121+
3. Replace what the mesh was giving you for free: TLS, retries, timeouts, authentication between the
122+
two sides, and a way for traces to keep connecting across the new network boundary.
123+
124+
Step 3 is where the cost you deferred finally arrives. That is the correct time to pay it — when a
125+
specific application has a specific reason to be separate, rather than for every boundary up front on
126+
the assumption that some of them will one day need it.
127+
128+
## Related reading
129+
130+
- [Build a modular monolith](../guides/build-modular-monolith.md) — a complete worked example
131+
- [Watt Architecture](./watt-architecture.md) — the design reasoning underneath
132+
- [The Multithread Model](./multithread-model.md) — what the boundary is made of
133+
- [Watt Architecture Patterns](../guides/watt-architecture-patterns.md) — pyramid and funnel topologies
134+
- [Comparison with Alternatives](../overview/comparison-with-alternatives.md) — Watt versus other approaches

0 commit comments

Comments
 (0)