[fpmsyncd]: coalesce ZMQ route sends behind a dedicated send thread - #4855
deepak-singhal0408 wants to merge 4 commits into
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
This PR updates fpmsyncd to avoid silent route drops on the ZMQ route path by buffering steady-state route/label-route updates in an in-memory last-writer-wins coalescing map that a dedicated send thread drains in bounded chunks, with retry/backoff, liveness asserts, and STATE_DB telemetry.
Changes:
- Add
RouteSendCoalescer(map + send thread) to coalesce and reliably retry ZMQ route sends, with congestion/health telemetry and deliberate-exit on prolonged stalls. - Integrate the coalescer into
RouteSyncfor the ZMQ route path, including warm-restart pause/resume to preserve single-writer semantics. - Add extensive mock unit tests for coalescing, chunking, retry/episode behavior, liveness asserts, and telemetry schema.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/mock_tests/Makefile.am | Adds the new coalescer unit test and implementation file to the fpmsyncd mock test build. |
| tests/mock_tests/fpmsyncd/routesendcoalescer_ut.cpp | New unit tests covering coalescing, chunking, retries, liveness, pause/resume, and telemetry. |
| fpmsyncd/routesync.h | Wires in coalescer/state DB members and helper methods for the ZMQ-backed tables. |
| fpmsyncd/routesync.cpp | Routes steady-state ZMQ table writes through the coalescer; pauses during warm-restart reconcile; stops thread on teardown. |
| fpmsyncd/routesendcoalescer.h | New coalescer class interface (config, ingest APIs, thread lifecycle, telemetry accessors). |
| fpmsyncd/routesendcoalescer.cpp | New coalescer implementation (bounded drains, retry/re-merge, episode tracking, telemetry, liveness assert/exit). |
| fpmsyncd/Makefile.am | Adds routesendcoalescer.cpp to the fpmsyncd build. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
a43dadf to
2336b7e
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
2336b7e to
4157f10
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Hi @venkit-nexthop @prabhataravind, Could you help review this PR. |
|
@deepak-singhal0408 The PR description says "VNET and EVPN route handlers are untouched," but that's only true for VNET ( |
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
986988b to
6a305cd
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
6a305cd to
412cbee
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
@prabhataravind good catch. The PR description was wrong. Fixed it. EVPN was already on the ZMQ path before this PR (when zmq is enabled), so same behavior remains, whereas VNET stays on REDIS path as before. |
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Under a routing burst, FRR/FPM ingest can outrun the ZMQ route path and overflow the send socket, silently dropping routes. Decouple ingest from the ZMQ send: steady-state route and label-route writes are coalesced (last-writer-wins per key) into an in-memory map and drained in chunks by a dedicated send thread, so ingest never blocks on a full ZMQ socket and a failed chunk is retried from the map rather than lost. - RouteSendCoalescer: coalescing map plus send thread, chunked drain under entry and byte caps, retry with backoff, and STATE_DB health telemetry (episode counters, congestion/stall gauges, RECOVERED edge). Each table is drained every cycle so neither starves the other, and liveness is tracked per table so a stall in one is visible while the other flows. - routesync funnels the steady-state ZMQ-table write path through the coalescer. The non-ZMQ path and the offload-reply path are unchanged. - Warm-restart reconciliation writes the route tables directly, so the send thread is parked for its duration, keeping a single writer at any instant. Warm restart re-enters on FPM reconnect, so this is an explicit pause/resume rather than a startup-only assumption. - An entry too large to ever fit in a ZMQ message is dropped and counted instead of being retried until the liveness guard fires. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e1be286-79be-4087-a72b-64836072e65e Signed-off-by: Deepak Singhal <deepsinghal@microsoft.com>
Rework the coalescer in response to review. The net effect is a smaller surface: the pause/resume handshake, three asserts, a drop counter and two size caps are gone, and none of them are replaced by new machinery. Make the ZMQ route path mutually exclusive with warm and fast restart, and enforce it where the path is configured rather than where it is read: - get_route_perf_zmq_enabled() is now a plain read of SYSTEM_DEFAULTS and never throws. validate_route_perf_zmq_supported() holds the check and is called from create_route_perf_zmq_client() and orchdaemon, so an unsupported combination fails at startup instead of mid-run. - With that guarantee, warm-restart reconciliation can no longer overlap the send thread, so the pause/resume handshake is deleted. The send thread is the single writer by construction. This also removes a latent out-of-bounds read: the warm arm of setRouteWithWarmRestart indexes element [1] of the wrapper vector, which only exists in the non-ZMQ layout. Replace the three asserts with a drop plus SWSS_LOG_ERROR. fpmsyncd builds with DBGFLAGS = -g while the mock tests build with -g -DNDEBUG, so the asserts were live exactly where they were never exercised and compiled out where they were. Remove both size caps, which could not fire for a real route entry: - The oversize-entry drop and maxWireBytes discarded entries the wire would have accepted. ZmqClient::sendMsg already throws on a genuinely oversized message, which the coalescer catches, re-merges and retries. - The per-chunk byte cap and its size helper are unreachable behind the 256-entry cap, which bounds a chunk to ~7.5 MiB against a 16 MiB limit. The helper also walked every field and allocated per entry while holding the ingest mutex. No other producer in sonic-swss does byte accounting before a send; bounding by entry count matches the convention. Drop malformed_dropped_total. Malformed input is a bug to fix, not a rate to watch, and the error log already names the offending key. Also address review nits: pass the KCO vector by const reference and add the includes the unit test and header relied on transitively. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Deepak Singhal <deepsinghal@microsoft.com>
SWSS_LOG_THROW was uncaught in every caller, so an unsupported configuration terminated orchagent and fpmsyncd with a core dump instead of refusing to start. Replace the throwing validator with a predicate and check it at the three entry points that configure the path: OrchDaemon::init(), fpmsyncd main() and routeresync main(). Each logs and exits non-zero. In orchagent the check moves to the top of init(). It previously sat after FgNhgOrch, whose constructor configures the path, so the throw from that constructor fired first and the explicit check was unreachable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Deepak Singhal <deepsinghal@microsoft.com>
9b230d0 to
5fc7e22
Compare
|
/azp run |
…ing it The premise behind the startup guard was wrong. ZMQ and warm restart have always worked together: ZmqProducerStateTable is constructed with dbPersistence enabled, so every ZMQ set() mirrors to APPL_DB and the restoration read finds its content, and WarmStartHelper is handed the ZMQ table itself, so reconcile writes back over ZMQ. Refusing the combination regressed a supported configuration. Drop the guard and the lib/orch_zmq_config.h helpers it added, restoring orchagent, routeresync and fpmsyncd to their prior startup paths. The real constraint is narrower: reconcile writes the route tables on the main thread, so the send thread must not be writing them at the same time. fpmsyncd now retires the coalescer when warm restart opens its window and starts it otherwise. stop() already drains the map to empty before joining, so no coalesced write can land after reconcile has decided the delta. Since checkAndStart() runs on every FPM reconnect, this also covers warm restart armed after the process is already coalescing. stop() now reports whether it joined a running thread, which keeps the retire log honest when the coalescer was constructed but never started. Restore the warm-restart branch of setRouteWithWarmRestart to its upstream form. The ZMQ wrapper emits a single SET, so the tuple lives at [0] and the hardcoded [1] this PR had introduced was out of bounds on that path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Deepak Singhal <deepsinghal@microsoft.com>
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
What I did
Decoupled FPM ingest from the ZMQ route send in fpmsyncd.
Route and label-route writes go into an in-memory coalescing map (last-writer-wins per key) that a dedicated send thread drains in chunks. A failed chunk is re-merged and retried, and never overwrites a newer update for the same key. A stall past the liveness bound writes a STATE_DB record and exits.
Scope:
SYSTEM_DEFAULTS|swss_zmqis enabled. Disabled, there is no map and no send thread.checkAndStart()runs on every FPM reconnect, so warm restart armed mid-run is covered too.ProducerStateTableover Redis and bypass the coalescer.ROUTE_TABLEwith unicast, so it coalesces alongside it.Fixes sonic-net/sonic-buildimage#28369
Why I did it
ZmqProducerStateTable::set()is fire-and-forget, so a burst that hits the socket high-water mark drops the update silently after zebra has already been acknowledged. Holding the update until a send succeeds closes that window; coalescing falls out of it. Full problem statement in sonic-net/sonic-buildimage#28369.How I verified it
routesendcoalescer_ut.cpp;tests_fpmsyncd212/212,-Werror.ZmqOrchTest,ZmqRouteOrchTest,ZmqRouteConsumerTest27/27.Details if related
Design: sonic-net/SONiC#2481.
ZmqProducerStateTable's bounded send retry (sonic-swss-common#1233, merged) as the inner blip absorber; this PR adds the outer retry.stop()drains the map to empty before joining, so no coalesced write lands after reconcile has decided the delta.Telemetry is in STATE_DB under the route-stat record: episode counters, stall gauges,
last_success_age_sec,routes_lost_total.