feat: add notification microservices and transactional outbox
هذا الالتزام موجود في:
114
docs/MICROSERVICES_MIGRATION.md
Normal file
114
docs/MICROSERVICES_MIGRATION.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 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.
|
||||
المرجع في مشكلة جديدة
حظر مستخدم