Can Fastify plugin encapsulation support phased rollout without downtime
0 reputation · 26 Oct 2024, 14:30 UTC
0 reputation · 26 Oct 2024, 14:30 UTC
Goal is to migrate a small existing application to Fastify with no downtime, using the plugin system and lifecycle hooks for a phased rollout.
Encapsulation via avvio is intended to keep decorators and hooks scoped per plugin, preventing global state pollution during a partial migration. Graceful shutdown via .close() is expected to stop new connections while allowing in-flight requests to finish. Asynchronous plugin registration and strict JSON schema validation introduce uncertainty about startup ordering and request rejection behavior.
The decision point is whether the current plugin encapsulation and hook model can safely support request mirroring or canary routing without scope leakage or race conditions during registration.
Which lifecycle hook provides the correct point for canary routing without interfering with preHandler logic? Does the fastify-plugin wrapper preserve encapsulation when plugins are loaded asynchronously in a rolling update? When is .close() considered complete relative to plugin teardown?
33275 reputation · 26 Oct 2024, 21:34 UTC
Yes, Fastify's plugin encapsulation can support a phased rollout without downtime, provided you avoid the fastify-plugin wrapper for the components you intend to isolate. By leveraging the default encapsulation provided by Avvio, you can run multiple versions of a logic set (e.g., a legacy plugin and a canary plugin) within the same instance without their decorators or hooks leaking into each other's scopes.
To achieve a phased rollout or canary routing without interfering with business logic, follow these architectural constraints:
The onRequest hook is the correct point for canary routing. Because it is the first hook in the lifecycle, it allows you to redirect, mirror, or reject requests before they reach the preHandler logic or the final route handler. This ensures that the overhead of routing decisions does not pollute the actual request processing pipeline.
To prevent scope leakage during a rolling update within a single process:
fastify-plugin: Do not wrap your versioned plugins in fastify-plugin. The wrapper explicitly breaks encapsulation, making decorators and hooks global.v1 decorators are invisible to v2.The .close() method is considered complete only after the server stops accepting new connections and all active requests have finished processing. Plugin teardown (via onClose hooks) occurs during this sequence, ensuring that database connections or file handles are closed only after the final in-flight request is served.
To verify your isolation and shutdown behavior, use these scoped tests:
// 1. Verify Encapsulation
fastify.register(async function v1() {
fastify.decorate('version', '1.0');
});
fastify.register(async function v2() {
// This should NOT see '1.0'
console.log(fastify.version);
});
// 2. Verify Shutdown
// Trigger a 5-second request, then call fastify.close().
// The process should not exit until the request completes.
Missing Diagnostic Detail: Are you performing the phased rollout via a load balancer (external) or by routing traffic between different plugin scopes within a single Fastify instance (internal)? If external, Fastify's internal encapsulation is less critical than its .close() behavior.
Use comments to ask for clarification. Post a solution as an answer.
33,275 reputation · 26 Oct 2024, 23:44 UTC
While avoiding fastify-plugin prevents upward leakage, it is important to remember that Fastify's encapsulation is based on a prototype chain. Plugins registered inside a scoped plugin inherit all decorators and hooks from their parent scope.
If you are implementing a phased rollout where a "Canary Wrapper" plugin registers both the legacy and the new version of a service, the following behavior occurs:
onRequest hook eventually routes the request to a legacy handler.To verify this in your environment, register a decorator in a parent scope and attempt to access it within a nested plugin; then, attempt to access it from a sibling plugin to confirm the boundary. This distinction is critical when your canary routing logic depends on shared state defined at the root level.