115 أسطر
7.0 KiB
Markdown
115 أسطر
7.0 KiB
Markdown
# Oudelaa microservices migration
|
|
|
|
## Current architecture
|
|
|
|
The backend is a NestJS modular monolith backed by MongoDB. Redis is used for cache,
|
|
BullMQ jobs, and the optional Socket.IO adapter. Domain modules call the in-process
|
|
`NotificationsService`. Public HTTP routes use the global `/api/v1` prefix and the
|
|
notifications Socket.IO namespace is owned by the same application.
|
|
|
|
## Phase 1 boundary
|
|
|
|
- `apps/api-gateway` is the new monorepo entry point. It deliberately bootstraps the
|
|
existing `src/AppModule`, so the application remains runnable while modules are moved
|
|
incrementally.
|
|
- `apps/notification-service` owns notification persistence, queries, unread counts,
|
|
read mutations, and the `follow.request.approved` consumer.
|
|
- The four existing Flutter notification endpoints remain on the gateway. The gateway
|
|
delegates them over Nest RMQ only when `NOTIFICATION_SERVICE_ENABLED=true`; otherwise
|
|
it uses the original in-process service.
|
|
- Socket.IO authentication, namespace, rooms, and public event names remain in the
|
|
gateway. Internal RMQ responses include realtime data which the gateway emits.
|
|
- Other notification producers stay in-process in phase 1. They can migrate one event
|
|
at a time in later phases.
|
|
|
|
## Reliability and topology
|
|
|
|
The main durable queue is `oudelaa.notifications`. Rejected messages are dead-lettered
|
|
to the durable `oudelaa.notifications.dlq` queue through the default exchange. Consumers
|
|
use manual acknowledgement and a prefetch limit. Event handling retries three times
|
|
inside one delivery, then rejects without requeue. Gateway calls have a bounded timeout,
|
|
one configurable retry, and a small circuit breaker. Rabbit message headers carry
|
|
`correlationId` and `requestId`; logs are JSON records containing both.
|
|
|
|
Follow-request approval is the first true transactional-outbox flow. The request state,
|
|
follow relationship, user counters, and deterministic outbox event are committed in one
|
|
MongoDB transaction. No BullMQ or RabbitMQ call occurs inside that transaction. When both
|
|
phase-1 flags are enabled, a polling dispatcher atomically leases pending/failed events,
|
|
waits for the Notification Service RPC response, and only then marks the event processed.
|
|
Timeouts use bounded exponential backoff and eventually move the event to `dead`; expired
|
|
`processing` leases are recovered after restart. `OutboxService.listByStatuses()` and
|
|
`retryEvent()` are the intentionally non-public administrative surface in this phase.
|
|
|
|
`eventId` is stored on notifications under a unique partial index. The consumer checks
|
|
it before insert and also handles Mongo duplicate-key races. The existing unique index on
|
|
recipient/type/reference remains as an additional guard for follow approval records.
|
|
|
|
## Planned migration
|
|
|
|
1. Phase 1: monorepo shell, contracts/events/common libraries, gateway compatibility
|
|
facade, and Notification Service.
|
|
2. Phase 2: move Auth and Users behind versioned internal contracts; centralize JWT
|
|
verification primitives in `libs/auth`; add tracing/metrics in `libs/observability`.
|
|
3. Phase 3: extract Posts and Media; replace direct notification calls with outbox events.
|
|
4. Phase 4: extract Feed and Search read models, consuming post/user events.
|
|
5. Phase 5: extract Moderation, then stabilize cross-service workflows and data ownership.
|
|
6. Phase 6: remove transitional imports and local fallbacks after traffic comparison,
|
|
replay tests, and a controlled cutover.
|
|
|
|
At every phase the gateway retains public paths and response envelopes. Internal event
|
|
contracts evolve additively and are versioned before any breaking change.
|
|
|
|
## Configuration and operating modes
|
|
|
|
`NOTIFICATION_SERVICE_ENABLED=false` and `OUTBOX_DISPATCHER_ENABLED=false` remain the
|
|
safe defaults. In that mode the gateway does not instantiate or connect an RMQ client;
|
|
the in-process notification path and public Flutter contracts remain unchanged. The
|
|
standalone-Mongo fallback exists only for this disabled legacy transition mode.
|
|
|
|
The reliable phase-1 path requires both flags to be `true`. Dispatcher interval, batch,
|
|
lease, attempt limit, and bounded backoff are configured by `OUTBOX_*`. RabbitMQ accepts
|
|
an injected `RABBITMQ_URL` or separate host, port, username, password, and vhost values.
|
|
TLS supports `amqps://`, CA/client material injected as base64 or mounted paths, peer
|
|
verification, and SNI. Passwords and URL userinfo are never logged.
|
|
|
|
`docker-compose.microservices.yml` is local-only. It deliberately uses guest/guest inside
|
|
the private Compose network, exposes management only on loopback, and runs a single-node
|
|
Mongo replica set so transactions are exercised locally. It is not a production template.
|
|
Queue names remain `oudelaa.notifications` and `oudelaa.notifications.dlq` to avoid an
|
|
undeclared migration; both are durable, non-exclusive, and non-auto-delete. RPC messages
|
|
are persistent, and consumers use manual acknowledgement with a prefetch limit.
|
|
|
|
## Production prerequisites
|
|
|
|
- Managed RabbitMQ (or an operated HA cluster), a dedicated vhost, non-guest
|
|
least-privilege credentials, and TLS/amqps. Inject credentials and certificates from
|
|
the deployment secret store; never copy local Compose values.
|
|
- A transaction-capable MongoDB replica set or sharded cluster. Gateway startup rejects
|
|
dispatcher activation against standalone MongoDB.
|
|
- Production Redis, service-specific secrets, persistent storage where applicable, and
|
|
network policy that permits RMQ/Mongo/Redis access only from the services.
|
|
- Liveness probes at `/api/v1/health` and `/health/live`; readiness probes at
|
|
`/api/v1/health/ready` and `/health/ready`. A temporary RMQ outage fails readiness but
|
|
does not fail liveness. RabbitMQ management must not be publicly exposed.
|
|
- Alerts for pending/failed/dead outbox counts, oldest event age, dispatcher failures,
|
|
notification DLQ depth, RMQ connectivity, and duplicate-key activity.
|
|
|
|
### Controlled activation and rollback
|
|
|
|
1. Provision production RabbitMQ with TLS, a dedicated vhost, and least-privilege user.
|
|
2. Confirm MongoDB is a replica set/mongos and Redis is production-ready.
|
|
3. Deploy Notification Service with injected secrets and verify liveness/readiness.
|
|
4. Deploy Gateway with both phase-1 flags still `false`; verify the monolith path.
|
|
5. Inject the same RMQ endpoint/trust settings into Gateway without logging them.
|
|
6. Enable both flags on one canary Gateway replica.
|
|
7. Approve a controlled private follow and observe outbox
|
|
`pending -> processing -> processed`, one notification, and one Socket emission.
|
|
8. Monitor retries, dead events, DLQ, readiness, latency, and duplicate-key metrics.
|
|
9. Expand the flags gradually; atomic leases make multiple Gateway replicas safe.
|
|
10. Roll back by setting both flags to `false`. Do not delete pending events or queues;
|
|
diagnose or retry them before the next canary, then drain Notification Service.
|
|
|
|
Notification Service still uses the same Mongo database and the same `notifications`
|
|
collection as Gateway. This is a transitional shared-database model, not independent data
|
|
ownership; physical separation belongs to a later migration phase.
|