[orchagent]: In-process SAI notification queue for ZMQ mode (Option 3) - #4806
[orchagent]: In-process SAI notification queue for ZMQ mode (Option 3)#4806vpandian-nokia wants to merge 3 commits into
Conversation
|
/azp run |
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
47c2a55 to
d57334f
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Add shared SaiNotificationQueue, dispatcher, and SaiNotificationOrch. Migrate SAI notification handlers to enqueue in ZMQ mode and dispatch via handleNotification() across Fdb, Ports, Bfd, Icmp, Twamp, Dash, MACsec, and HFTel orchs. Signed-off-by: Vijay Pandian <vijayaragavan.pandian@nokia.com>
Signed-off-by: Vijay Pandian <vijayaragavan.pandian@nokia.com>
Remove extra Orch::addExecutor() introduced during rebase conflict resolution in IcmpOrch constructor. Signed-off-by: Vijay Pandian <vijayaragavan.pandian@nokia.com>
d57334f to
8d744a7
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
venkit-nexthop
left a comment
There was a problem hiding this comment.
Reviewed against the HLD in sonic-net/sonic-buildimage#27791. The design is sound and matches Option 3 — the queue/dispatcher/executor split is clean, handler lookup correctly copies under the lock and invokes outside it, and the non-ZMQ Redis path is left untouched. Four issues below, one of which I would treat as blocking.
1. [Major] Busy-spin when a readiness predicate is false
SaiNotificationQueueExecutor::execute() returns without consuming anything when the front op is not ready:
std::string frontOp;
if (!m_queue->peekFrontOp(frontOp) || !m_dispatcher->isReady(frontOp))
{
return;
}But hasCachedData() returns hasData(), i.e. "queue non-empty". In Select::select() (sonic-swss-common common/select.cpp:153):
if (sel->hasCachedData())
{
m_ready.insert(sel); // reinserted; the loop at :137 calls it again immediately
}So with a non-empty queue and a false predicate, execute() returns, hasCachedData() is true, the selectable is reinserted and called again — a tight loop inside a single select() call, burning a core until the predicate flips.
This looks reachable in practice. FdbOrch registers [this]() { return m_portsOrch->allPortsReady(); }, and FDB events arriving during startup are exactly the case where ports are not yet ready. Because the predicate can only become true through progress on the same main thread, this risks livelock rather than a brief spin.
Suggestion: when the front op is not ready, either return false from hasCachedData() until readiness changes, or defer with a short backoff/timer instead of relying on immediate reinsertion.
2. [Major] Readiness is checked only for the front entry, then the whole batch is dispatched
if (!m_queue->peekFrontOp(frontOp) || !m_dispatcher->isReady(frontOp)) return;
std::deque<swss::KeyOpFieldsValuesTuple> entries;
m_queue->pops(entries); // up to popBatchSize (128)
for (auto &entry : entries)
{
m_dispatcher->dispatch(entry); // no per-entry readiness check
}pops() takes up to 128 mixed-op entries, but readiness was validated only for entry 0. If a port_state_change is at the head and an fdb_event is at position 5, the FDB handler runs even though allPortsReady() is false — the exact condition the predicate exists to prevent.
Suggestion: check isReady() per entry inside the dispatch loop (deferring or re-queuing those that are not ready), or stop popping at the first not-ready op.
3. [Minor] The queue's priority is silently discarded
SaiNotificationQueue is constructed with pri = 100, but Select never sees it. Executor is handed a wrapper:
Executor(new SaiNotificationQueueSelectable(queue), orch, name)and the wrapper does not forward the priority:
explicit SaiNotificationQueueSelectable(SaiNotificationQueue *queue) : m_queue(queue) {}swss::Selectable(int pri = 0), so the wrapper registers at priority 0 while the intended 100 sits unused on the inner object. Either forward the priority through the wrapper or drop the parameter so it does not mislead.
Related: I assume the wrapper exists because Executor takes ownership of its Selectable and would otherwise delete the process-global queue. That is a reasonable lifetime shim, but it is not stated anywhere, and the indirection otherwise looks redundant given SaiNotificationQueue is already a Selectable. Worth a comment.
4. [Minor] The queue is unbounded
enqueue() has no cap. m_highWatermark is tracked and exposed but never enforced or logged. The previous Redis path had implicit backpressure; an in-process std::queue has none, so a notification burst against a stalled main loop grows without limit. Combined with issue 1, which can stall the drain indefinitely, these compound.
Suggestion: a maximum depth with a drop policy, plus a rate-limited warning when the watermark crosses a threshold.
Smaller notes
registerHandler()silently overwrites an existing handler for the same op. A warning would catch accidental double-registration during the phased migration.enqueueSaiNotification()drops notifications whengOrchShutdownRequested != 0with no counter or log. Intentional, but invisible if it ever happens mid-operation.getSaiNotificationQueue()/getSaiNotificationDispatcher()leak by design as process-lifetime singletons. That is fine, butstd::call_onceor a function-local static would express it more directly than a mutex plus null check.
What looks right
- The dispatcher copies the handler under the lock and calls it outside, so no handler runs while holding the dispatcher mutex.
pops()re-notifies the eventfd when entries remain, so a partial drain does not stall.- Non-ZMQ mode is genuinely untouched; the
gRedisCommunicationMode == ZMQ_SYNCguard was already present and only the body changed. - The old
handleNotification(NotificationConsumer&, ...)overloads are removed rather than left as dead alternatives, avoiding two live paths.
This is a diff-only review; I have not built or run the change. Issue 1 in particular is worth confirming against Select's actual behaviour before acting on it.
What I did
SaiNotificationQueue, and drained by the existingorchagentmainSelectloop through a queue executor — instead of re-posting toASIC_DB:NOTIFICATIONSand consuming via RedisNotificationConsumer.SaiNotificationQueue,SaiNotificationQueueSelectable,SaiNotificationQueueExecutor, andSaiNotificationDispatcher.SaiNotificationOrchas a thin infrastructure orch (not a feature orch): it owns the shared queue executor and exposesregisterHandler(op, handler, readinessPredicate)so feature orchs attach per-op dispatch callbacks. Feature orchs do not register separate executors for queue-path notifications.notifications.cpp(and ICMP callback) to callenqueueSaiNotification()in ZMQ mode; non-ZMQ mode continues to use the existing Redis notification path unchanged.handleNotification(entry)in:PortsOrch(SAI_SWITCH_NOTIFICATION_NAME_PORT_STATE_CHANGE,SAI_SWITCH_NOTIFICATION_NAME_PORT_HOST_TX_READY)FdbOrch(SAI_SWITCH_NOTIFICATION_NAME_FDB_EVENT)BfdOrch(SAI_SWITCH_NOTIFICATION_NAME_BFD_SESSION_STATE_CHANGE)IcmpOrch(SAI_SWITCH_NOTIFICATION_NAME_ICMP_ECHO_SESSION_STATE_CHANGE)TwampOrch(SAI_SWITCH_NOTIFICATION_NAME_TWAMP_SESSION_EVENT)DashHaOrch(SAI_SWITCH_NOTIFICATION_NAME_HA_SET_EVENT,SAI_SWITCH_NOTIFICATION_NAME_HA_SCOPE_EVENT)DashHaFlowOrch(SAI_SWITCH_NOTIFICATION_NAME_FLOW_BULK_GET_SESSION_EVENT)MACsecOrch(SAI_SWITCH_NOTIFICATION_NAME_SWITCH_MACSEC_POST_STATUS,SAI_SWITCH_NOTIFICATION_NAME_MACSEC_POST_STATUS)HFTelOrch(SAI_SWITCH_NOTIFICATION_NAME_TAM_TEL_TYPE_CONFIG_CHANGE)tests/mock_tests/notifications_ut.cpp.Orch::addExecutor(icmpStateNotifier)introduced during rebase inicmporch.cpp.Why I did it
For ZMQ southbound mode, SAI notifications are delivered in-process. Re-posting them through Redis adds latency and extra serialization. Option 3 routes notifications directly from the SAI callback into an in-process queue and dispatches them to the owning orch, avoiding the Redis round-trip while keeping the existing orch handler logic.
Related:
How I verified it
swssdeb;mock_testspassed, includingnotifications_ut(12/12 ZMQ enqueue tests).handlePortStateChangeNotificationobserved in orchagent syslog with correct oper status and speed.FdbOrchand entry appeared inFDB_TABLE.Details if related
ZMQ-mode dispatch path (Option 3):
enqueueSaiNotification(op, data, values)+ wake queue selectableSaiNotificationQueueExecutor::execute()allPortsReady()for port notifications)SaiNotificationDispatcher→ registered feature-orchhandleNotification(entry)Non-ZMQ mode: unchanged —
syncdpublishes toASIC_DB:NOTIFICATIONS; orchs consume via existingNotificationConsumer/Notifierpath.There is no hybrid Option 2 + Option 3 path for the same notification type in ZMQ mode.