Expert bridge self healing via end-2-end heartbeat monitoring#7887
Conversation
…FlowFuse/flowfuse into feat-expert-bridge-self-healing
|
@Steve-Mcl I'm unlikely to get to this before Monday |
|
@cstns added as reviewer in place of Ben since you have knowledge of the pathways involved. What you may not be aware of is the outage we had on Expert a couple of Saturdays ago. Long story short, the bridge became uni-directional. Stopping and starting it recovered the 2 way communications. The owner issue #7818 was raised to add heartbeat and self healing (have a read and decide if the Pr fulfils that) NOTE: In short, I chose the least path of resistance (the command/inflight channels) and used them in reverse as this mean no new acls and at least covered 2 directions - we can iterate if we find the bridge breaks down for a single channel. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7887 +/- ##
==========================================
+ Coverage 75.65% 75.72% +0.07%
==========================================
Files 432 434 +2
Lines 23086 23198 +112
Branches 6126 6161 +35
==========================================
+ Hits 17465 17567 +102
- Misses 5621 5631 +10
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| const payloadData = JSON.stringify(payload) | ||
|
|
||
| // Send the heartbeat request and set a timer to check for a response after the maxResponseTime has elapsed | ||
| this.client.publish(topic, payloadData, mqttOptions) |
There was a problem hiding this comment.
The heartbeat request is published with only correlationData (no userProperties) & the heartbeat handler at line ~159 in commsClient is placed after this early return which ignores messages without userProperties.
There was a problem hiding this comment.
in the heartbeat reply (generated by the other side, userProperties is added).
| await sleep(randomInt(0, 999)) | ||
|
|
||
| // Request a heartbeat from the Expert Agent via the bridge | ||
| expertCommsHandler.requestBridgeHeartbeat(maxResponseTime, async (err, result) => { |
There was a problem hiding this comment.
Consider using an object parameter here, e.g. async ({ err, result } = {}) => { ... }. That would make the callback a bit more flexible, since callers that only need the result wouldn’t have to pass null as the first argument just to skip the error
There was a problem hiding this comment.
can be easily retroed - lets leave for now
| // This event is triggered (emitted) in the commsClient when a valid heartbeat response is received from the Expert Broker via. | ||
| // the bridge (Expert Broker → FF App Instance Broker) in response to a heartbeat request made in `this.requestBridgeHeartbeat()`. | ||
| // Here we simply update the cache with the response. The cache is required since there may be replica instances of the FF App | ||
| // and the expert platform topic is a shared topic, so the response may be received by a different instance than the one that initiated the request. |
There was a problem hiding this comment.
Wouldn't it make more sense to keep the heartbeat cache in a shared chase then? this would mitigate cross talk from the initiator replica and responder replica
There was a problem hiding this comment.
using app.caches does resolve to an in memory or redis cache. On FFC, we use redis (so it is not a local cache)
| const deviceCommsHandler = DeviceCommsHandler(app, client) | ||
| const instanceCommsHandler = InstanceCommsHandler(app, client) | ||
| const platformAutomationHandler = PlatformAutomationHandler(app, client) | ||
| const expertCommsHandler = new ExpertCommsHandler(app, client) |
There was a problem hiding this comment.
we should we conditionally create/instantiate the expert commsHandler only if there's a bridge is enabled
There was a problem hiding this comment.
fair point. will consider.
Do not merge before https://github.com/FlowFuse/CloudProject/issues/1519
Description
TLDR
Adds an end-to-end heartbeat check for the Expert Agent bridge. On a schedule, the platform publishes a heartbeat through the MQTT bridge to the Expert Broker and back; if the agent misses enough heartbeats in a row, the platform automatically tears down and re-creates the bridge. New ACL rules gate the (deliberately reversed) heartbeat topics so only the platform or the agent can use them, in the correct direction.
Key Points
ExpertCommsHandler(forge/comms/expert.js) owns the heartbeat request/response lifecycle: publishes a heartbeat with a freshcorrelationData, tracks the outcome in a small (LRU/in memory or Redis for installs with replicas depending on the installation) cache (expert-bridge-heartbeat-cache, configurable viaapp.config.expert.centralBroker.heartbeat.{bridgeCacheMax,bridgeCacheTTL}), and reports{ errorCount }back to the caller via callback once the response arrives ormaxResponseTimeelapses. Wired intoforge/comms/index.jsasapp.comms.expert.forge/ee/lib/expert/tasks/heartbeat.js), registered inforge/ee/lib/expert/index.jsonly when the Expert Bridge is enabled (app.config.expert.centralBroker.serverset). Validates its own cron schedule at registration time (must be recurring, and must not run more often than2 * maxResponseTimeapart, to prevent overlapping runs) and fails safe (logs and skips registration) if the schedule is invalid. On repeated failure (errorCounthits a multiple ofmaxSuccessiveFailureCount, default 3), it force re-syncs the bridge viasyncBridge(app, { force: true }).forge/comms/commsClient.js: recognizes the agent's echoed heartbeat message on the bridge and re-emits it asresponse/platform/expert/bridge/heartbeat, whichExpertCommsHandlerlistens for.forge/comms/aclManager.js: adds four hardcoded, exact-topic ACL rules for the heartbeat handshake. The topic naming is intentionally reversed — the platform's heartbeat "request" is published on the topic literally named.../response, and vice versa — because the heartbeat reuses the existing platform command channel rather than adding new topics. Each direction is locked to the correct client type (forge_platformvsexpert-agent) and pub/sub direction.Tests added to
test/unit/forge/comms/expert_spec.jsNew file covering
ExpertCommsHandler: cache defaults and config overrides (centralBroker.bridgeCache.max/.ttl), the response-listener storing a payload in cache, the outgoing heartbeat request shape (topic/qos/correlationData), success/failure/reset oferrorCountacross repeated calls, ignoring a response with a mismatchedcorrelationData, clearing a still-pending timer when called again, tolerating no callback, and forwarding a cache-read error to the callback.Tests added to
test/unit/forge/comms/authRoutesV2_spec.jsAdded to the existing "Expert Agent" ACL block, plus a new "Platform (forge_platform)" block: allow/deny pairs for all four heartbeat topics, confirming each client type can only use its topic in the correct direction (e.g. the agent can subscribe to the heartbeat response topic but not publish to it, and vice versa for the platform).
Tests added to
test/unit/forge/ee/lib/expert/tasks/heartbeat_spec.jsNew file covering the task factory in isolation (fake
app/stubbedsyncBridge, no full app boot): schedule/startDelay/maxResponseTime/maxSuccessiveFailureCount validation (including thestartDelay: 0fix and the "too frequent schedule" guard), thestartDelaygate inrun(), and the re-sync callback logic (no re-sync on success or below the failure threshold, re-sync at each multiple ofmaxSuccessiveFailureCount, and that asyncBridgefailure is logged rather than thrown).Related Issue(s)
closes #7818
Checklist
flowforge.yml?FlowFuse/helmto update ConfigMap TemplateFlowFuse/CloudProjectto update values for Staging/ProductionLabels
area:migrationlabel