Repository navigation
chore: NATS broker for micro services - #35305
sampaiodiego wants to merge 44 commits into
Conversation
|
Looks like this PR is not ready to merge, because of the following issues:
Please fix the issues and try again If you have any trouble, please check the PR guidelines |
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #35305 +/- ##
===========================================
+ Coverage 71.27% 71.33% +0.05%
===========================================
Files 4861 4866 +5
Lines 207137 207663 +526
Branches 36571 36303 -268
===========================================
+ Hits 147631 148130 +499
- Misses 54382 54415 +33
+ Partials 5124 5118 -6
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
|
@kody start-review |
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
3b2cc8e to
6d69c4a
Compare
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
6d69c4a to
7b81a9c
Compare
f48f4b5 to
05ab9ff
Compare
|
Layne could not analyze all changed content. Review the Check Run summary before merging. |
9e2d731 to
f6952ba
Compare
69d51e9 to
38cd813
Compare
a49da52 to
190dcf4
Compare
| this.services.forEach((service) => this.broker?.createService(service)); | ||
|
|
||
| if (this.started) { | ||
| void this.broker.start(); |
There was a problem hiding this comment.
why does it need to be started here?
4554582 to
8f73eb7
Compare
9bff77f to
b501542
Compare
NatsBroker awaited `created()`/`started()` directly, so a service whose hook rejected rejected `api.start()` too. Nothing catches that, so the unhandled rejection killed the container - silently, taking every other service hosted in the same process with it. Endpoints are already registered before any hook runs, so a service that fails to initialize can still answer with its defaults. Log the failure and keep going instead.
Moleculer holds a registry and waits for a service to appear; nats has no such thing, so a request to a subject nobody listens on fails at once with 503. That is the normal state of affairs while a peer boots or is rolled, and every caller was left to handle it - or not, as the crash on startup showed. Back off and retry for a few seconds. Only 503 is retried: it means nothing received the request, so a second attempt cannot duplicate a side effect. A timeout may have been delivered, so it still surfaces to the caller. `isNatsError` is declared by the nats typings but not exported at runtime, so the check uses `NatsError` itself.
Services that read configuration from another service need to do it on first use rather than at boot, and need a failed attempt to be retried rather than remembered. Three of them are about to want exactly that.
`started()` fetched `Accounts_LoginExpiration` from the settings service, which runs in another process. During a cold start it is not reachable yet, so the read threw and took the whole account-service down with it. The field is already kept current by `onSettingChanged`; the boot read only primed it. Prime it on the first login instead, where a transient failure fails that one login rather than the process.
`started()` fetched seven settings from the settings service, which runs in another process. During a cold start it is not reachable yet, so the read threw and took the whole omnichannel-transcript-service down with it. All seven fields already have defaults and are kept current by `onSettingChanged`; the boot read only primed them. Prime them on the first transcript instead, so a transient failure fails that one job - which the queue already retries - rather than the process.
`started()` read four settings from the settings service, which runs in another process. During a cold start it is not reachable yet, so the read threw and took the whole authorization-service down with it - and even once that is survivable, it left the service unconfigured: `this.pdp` stays null, and `checkUsernamesMatchAttributes` silently skips the check in that state. Keep the boot read, but make it repeatable and run it again on the two decision paths, so a service that came up before the settings service configures itself on the first decision rather than staying open until a setting happens to change. The read now only fills in what is still unknown, and leaves an already chosen PDP alone: settings events arrive independently of it, so whatever they delivered is newer than what it just fetched.
Moleculer dispatches a call to a locally registered service straight to the instance, without going near the serializer, and call sites depend on it: `ee/server/configuration/abac.ts` hands `LDAPEnterprise` a mongo cursor, which only survives because the argument arrives by reference. NatsBroker sent everything over nats, so EJSON met the cursor and threw `Converting circular structure to JSON`. Route through a LocalServiceRegistry that tracks the services running in this process. Calls to them keep argument and result identity, and skip a round trip through the nats server on the way. `getCallableMethods` now defines what a service answers to in one place, so the local and remote paths cannot expose a different set. Set BROKER_LOCAL_ROUTING=false to turn it off and send everything over the wire again.
LocalBroker already indexed its services by `<service>.<method>` and dispatched
straight to the instance - the same thing NatsBroker just grew, down to its own
copy of the method enumeration in both createService and destroyService.
Move LocalServiceRegistry and getInstanceMethods into core-services, which both
brokers already depend on, and let LocalBroker route through the registry too.
MoleculerBroker takes getInstanceMethods from the same place.
This also stops LocalBroker exposing lifecycle hooks and event handlers as
callable: `api.call('some-service.started')` used to re-run the hook. Nothing
calls those through a broker.
Covers all three implementations rather than only the new one: how a proxied call reaches a broker at all, what LocalBroker and MoleculerBroker each provide, and what NatsBroker had to rebuild to match.
Priming only covered the two decision paths, but five room attribute mutators and four other entry points read `this.pdp` as well. In microservices mode the monolith does not register AbacService at all, so every call lands on authorization-service - whose `started()` loses the race with the settings service and leaves `this.pdp` null. `ensurePdpAvailable` then reported the PDP as unavailable rather than as not yet configured, and the ABAC API answered 400 `error-pdp-unavailable` to 35 requests. Prime at `ensurePdpAvailable` instead, which is the chokepoint all five mutators already go through, and at the entry points that read `this.pdp` without it. `computeEffectiveStoreType` primes too, so attribute store selection stops depending on whether the boot read happened to succeed - that was only left out earlier because `started()` was gone at the time, and with it back the extra call is a memoised no-op. Two specs mocked core-services without `Settings`, which only showed up once those paths started reading it.
…st use NatsBroker ignored the dependencies every service declares - `settings` and `license` implicitly, plus any passed to `api.registerService` - so services that read configuration in `started()` lost the race with the monolith. The workaround was `primeOnce`: read on first use, retry on failure. That changed when and where those services configure themselves compared to Moleculer. Reproduce Moleculer's `waitForServices` instead. A service is registered, and its hooks run, only once every dependency is available: a local one as soon as it registers, a remote one through discovery every second. It waits forever by default, like Moleculer; a `dependencyTimeout` constructor option bounds the wait, which the tests use so that an unmet dependency fails fast instead of hanging. With that in place, revert the priming in Account, OmnichannelTranscript and AbacService back to their boot reads, and delete `primeOnce`. ddp-streamer now declares `meteor` as a dependency, since the client versions it publishes come from MeteorService, and loads them in `started()` again - still after the socket server is listening, and still non fatally. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
IBroker gained emitToOne on develop (#42415) for domain events that one instance of each listening service must handle, such as room.user-activity reaching FederationMatrix. Each service now also subscribes to emit.<event> in a queue group named after itself, so NATS delivers each emit to exactly one of its instances. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
As PID 1 in a container node ignores SIGTERM without a listener, so under NatsBroker every container was killed at the end of the grace period: no service ran stopped(), nothing was drained, and exit hooks such as the coverage writer never ran. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
876ba6c to
77dda08
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
https://rocketchat.atlassian.net/browse/RDI-278
Proposed changes (including videos or screenshots)
This is a proof of concept. It adds
NatsBroker, a second network broker for the microservices deployment. It talks to NATS directly instead of going through Moleculer. It uses the NATS services protocol for calls and discovery, and plain subjects for events. Moleculer stays the default.BROKER=natsselects the new broker in every service and in the monolith running in microservices mode.Broker selection
startBroker()(ee/packages/network-broker/src/startBroker.ts) picksMoleculerBrokerorNatsBrokerfromBROKER. The NATS factory (nats.ts) readsNATS_URL, falling back toTRANSPORTER, plusREQUEST_TIMEOUTandBROKER_LOCAL_ROUTING.Api.setBroker()now throws once the API has started, so a broker can no longer be swapped under running services.What
NatsBrokerimplementsrpc.<service>.<method>, load balanced by a NATS queue group.node.<nodeID>.<service>.<method>pins a call to one instance and backsCallingOptions.nodeID. A service in the same process is called directly, so arguments keep their identity instead of going through EJSON.BROKER_LOCAL_ROUTING=falseturns that off.broadcastpublishes onevent.<name>and reaches every node.emitToOnepublishes onemit.<name>, where each service subscribes in a queue group named after itself, so one instance of each listening service receives it.broadcastLocalstays in process.<service>.<method>, e.g.accounts.login.MeteorErrorkeeps itserror,reason,detailsandisClientSafe.$node.listand$node.servicesare answered from a$SRV.PINGround trip, with a short cache. Each service carries its node id as metadata. License enforcement uses that list, the same way it does under Moleculer.settingsandlicenseby default) are reachable locally or remotely.created()/started()hooks run.api.start().503) is retried with backoff for about 4s, which covers peers that are still booting or being rolled. Nothing else is retried, because a timed-out request may already have run.Readablein a call's arguments or its result is replaced by an inbox subject. The other side pulls 256KB chunks from that subject, which provides backpressure. An idle timeout cleans up the subject when the consumer never pulls.asyncLocalStorage, soServiceClass.contextis populated. Under NATS, ddp-streamer's login handler depends on this.SIGTERM/SIGINTevery service runsstopped(), then the connection drains and the process exits.SKIP_PROCESS_EVENT_REGISTRATION=truedisables this.Shared with
MoleculerBrokerwithDefaultDependencies.ts, used by both brokers.startBrokerfrommoleculer.tsis renamed tostartMoleculerBroker.Tests and docs
NatsBroker.test.tsruns the broker against an in-memoryFakeNatsConnection. It covers calls, events,emitToOne, errors, retries, dependency waiting, lifecycle failures, streams, context and shutdown.nats.test.tscovers the signal handling.docs/service-brokers.mddescribes all three brokers, everything that had to be rebuilt for NATS, how events reach other instances in each deployment, what is not implemented yet, and the configuration.Not done yet
Warning
Tracing, metrics and namespaces are not implemented. Switching to
BROKER=natssilently loses all three. See "Not implemented yet" indocs/service-brokers.md.broker.metricsisundefined. ddp-streamer'srocketchat_subscription,users_connectedandusers_loggedare not recorded, and no Prometheus reporter is exported (noMS_METRICSequivalent).MS_NAMESPACEequivalent. Subjects and service names are global to the NATS server, so two deployments on one server would answer each other's calls, receive each other's events, and discover each other's services. Until this lands, give each deployment its own NATS account.Also missing:
broadcastToServicesis a no-op.$node.connected/$node.disconnectedare never emitted. Presence relies ononNodeDisconnectedto drop the sessions of a node that died.$services.changedand the other broker lifecycle events are never emitted. Nothing in the codebase listens to them today.Issue(s)
Steps to test or reproduce
yarn testunitinee/packages/network-broker.brokerinput that is passed to docker compose asBROKER. The workflow checks that the requested broker actually started. The input defaults tonatson this branch so CI exercises the new broker. That default has to go before merge.BROKER=nats.Further comments
The
natsdependency was bumped to^2.29.1in every app so the repo keeps a single version.🤖 Generated with Claude Code