On Fri, Oct 9, 2026 at 3:56 PM Jacob Tanenbaum via dev <
[email protected]> wrote:

> Remove the needs_full_sync fallback for IGMP/MLD and IC-learned service
> monitor flows. lflow_ref_unlink_and_prune() saves the uuids of orphaned
> lflows so that orphaned flows can be deleted without a resync being
> required.
>
> The IGMP/MLD and IC-learned service monitor handlers set
> trk_data.needs_full_sync, which forced dp_group to fall back to a full
> lflow_table_sync_to_sb() instead of the incremental
> lflow_ref_sync_lflows() pass.
>
> The full sync, unlike the incremental sync, does not destroy
> lflow_ref_nodes that are no longer linked on lflows still referenced
> by another lflow_ref.  A retained, already-unlinked node can then be
> unlinked again by a later lflow_ref_unlink_lflows() call, which
> releases its datapath refcnt a second time.  The count can drop below
> the number of lflow_refs that still use the lflow, emptying its
> datapath bitmap so that a later sync deletes an SB row that other
> lflow_refs still require (e.g. a load balancer VIP flow shared by two
> load balancers on the same datapath).
>
> Drop the fallback and keep these flows incremental.
> lflow_ref_unlink_and_prune() already destroys orphaned lflows in
> memory; the incremental sync cannot see them because the lflow and
> its lflow_ref_node are gone.  So record the SB uuid of every
> orphaned lflow that already has an SB row in
> trk_data.orphaned_sb_uuids and have dp_group_lflow_handler() delete
> those rows after the incremental sync pass. This solves a performance
> regression in which ordinary logical switch port changes caused the
> whole logical flow table to recalculate.
>
> This is safe even when an orphaned lflow is re-added by a later build
> (e.g. the per-datapath multicast flood flow): the re-added lflow is a
> fresh in-memory object with a new random sb_uuid, so its freshly
> inserted SB row is never one of the recorded uuids.
>
> Add a northd regression test for the case where two load balancers
> on the same switch share a VIP.  Removing the shared VIP from one
> load balancer while a datapath group full sync is in progress,
> followed by a further change to that load balancer, previously
> double-released the shared lflow's datapath refcnt.  A later full
> sync then pruned the lflow and deleted its SB row even though the
> second load balancer still required the VIP.
>
> The test keeps the two changes that must land in a single engine run
> (remove the VIP from the first LB and change the port address)
> batched into one transaction, so that the multicast node's
> recompute forces the datapath group onto the full sync path while
> the lflow node stays incremental.  It asserts that the shared VIP
> flow is still present after the sequence of changes.
>
>
nit: Missing Reported-at.


> Assisted-by: Qwen3.8-27B-FP8, opencode
> Fixes: b9809453f546 ("northd: Split en_lflow into compute and sync nodes.")
> Signed-off-by: Jacob Tanenbaum <[email protected]>
> ---
>  northd/en-dp-group.c | 13 +++++++---
>  northd/en-lflow.c    | 17 +++++++-----
>  northd/en-lflow.h    |  7 ++++-
>  northd/lflow-mgr.c   | 41 +++++++++++++++++++++++++++--
>  northd/lflow-mgr.h   |  6 ++++-
>  tests/ovn-northd.at  | 61 ++++++++++++++++++++++++++++++++++++++++++++
>  6 files changed, 132 insertions(+), 13 deletions(-)
>
> diff --git a/northd/en-dp-group.c b/northd/en-dp-group.c
> index e33e1f407..3aeed361b 100644
> --- a/northd/en-dp-group.c
> +++ b/northd/en-dp-group.c
> @@ -89,9 +89,7 @@ dp_group_lflow_handler(struct engine_node *node,
>      struct ed_type_global_config *global_config =
>          engine_get_input_data("global_config", node);
>
> -    if (hmapx_is_empty(&lflow_data->trk_data.dirty_lflow_refs) ||
> -        lflow_data->trk_data.needs_full_sync) {
> -
> +    if (hmapx_is_empty(&lflow_data->trk_data.dirty_lflow_refs)) {
>          dp_group_sync_to_sb(node, lflow_data);
>          return EN_HANDLED_UPDATED;
>      }
> @@ -107,6 +105,15 @@ dp_group_lflow_handler(struct engine_node *node,
>              return EN_UNHANDLED;
>          }
>      }
> +
> +    /* Delete the SB rows for lflows orphaned in-memory this run (see
> +     * lflow_table_delete_orphaned_sb_flows()).  This only happens on the
> +     * incremental path: the handlers that record orphaned_sb_uuids also
> +     * mark a lflow_ref dirty, so the full-sync branch above never sees
> +     * non-empty orphaned_sb_uuids. */
> +    lflow_table_delete_orphaned_sb_flows(
> +        sb_flow_table,
> +        &lflow_data->trk_data.orphaned_sb_uuids);
>      return EN_HANDLED_UPDATED;
>  }
>
> diff --git a/northd/en-lflow.c b/northd/en-lflow.c
> index f2ae40ec8..a73d55486 100644
> --- a/northd/en-lflow.c
> +++ b/northd/en-lflow.c
> @@ -221,14 +221,16 @@ lflow_multicast_igmp_handler(struct engine_node
> *node, void *data)
>      lflow_get_input_data(node, &lflow_input);
>
>      lflow_ref_unlink_and_prune(mcast_igmp_data->lflow_ref,
> -                              lflow_data->lflow_table);
> +                               lflow_data->lflow_table,
> +                               &lflow_data->trk_data.orphaned_sb_uuids);
>
>      build_igmp_lflows(&mcast_igmp_data->igmp_groups,
>                        &lflow_input.ls_datapaths->datapaths,
>                        lflow_data->lflow_table,
>                        mcast_igmp_data->lflow_ref);
>
> -    lflow_data->trk_data.needs_full_sync = true;
> +    hmapx_add(&lflow_data->trk_data.dirty_lflow_refs,
> +              mcast_igmp_data->lflow_ref);
>
>      return EN_HANDLED_UPDATED;
>  }
> @@ -297,7 +299,8 @@ lflow_ic_learned_svc_mons_handler(struct engine_node
> *node,
>              ic_learned_svc_monitors_data->lflow_ref);
>
>      lflow_ref_unlink_and_prune(ic_learned_svc_monitors_data->lflow_ref,
> -                               lflow_data->lflow_table);
> +                               lflow_data->lflow_table,
> +                               &lflow_data->trk_data.orphaned_sb_uuids);
>
>      build_lswitch_arp_nd_ic_learned_svc_mon(
>          &svc_mons_data,
> @@ -305,7 +308,8 @@ lflow_ic_learned_svc_mons_handler(struct engine_node
> *node,
>          lflow_input.svc_monitor_mac,
>          lflow_data->lflow_table);
>
> -    lflow_data->trk_data.needs_full_sync = true;
> +    hmapx_add(&lflow_data->trk_data.dirty_lflow_refs,
> +              ic_learned_svc_monitors_data->lflow_ref);
>
>      return EN_HANDLED_UPDATED;
>  }
> @@ -317,7 +321,7 @@ void *en_lflow_init(struct engine_node *node
> OVS_UNUSED,
>      data->lflow_table = lflow_table_alloc();
>      lflow_table_init(data->lflow_table);
>      hmapx_init(&data->trk_data.dirty_lflow_refs);
> -    data->trk_data.needs_full_sync = false;
> +    uuidset_init(&data->trk_data.orphaned_sb_uuids);
>
>      return data;
>  }
> @@ -327,11 +331,12 @@ void en_lflow_cleanup(void *data_)
>      struct lflow_data *data = data_;
>      lflow_table_destroy(data->lflow_table);
>      hmapx_destroy(&data->trk_data.dirty_lflow_refs);
> +    uuidset_destroy(&data->trk_data.orphaned_sb_uuids);
>  }
>
>  void en_lflow_clear_tracked_data(void *data_)
>  {
>      struct lflow_data *data = data_;
>      hmapx_clear(&data->trk_data.dirty_lflow_refs);
> -    data->trk_data.needs_full_sync = false;
> +    uuidset_clear(&data->trk_data.orphaned_sb_uuids);
>  }
> diff --git a/northd/en-lflow.h b/northd/en-lflow.h
> index fd3f2427f..c83ef0e4d 100644
> --- a/northd/en-lflow.h
> +++ b/northd/en-lflow.h
> @@ -9,12 +9,17 @@
>
>  #include "lib/hmapx.h"
>  #include "lib/inc-proc-eng.h"
> +#include "lib/uuidset.h"
>
>  struct lflow_table;
>
>  struct lflow_tracked_data {
>      struct hmapx dirty_lflow_refs; /* lflow_refs changed by handlers. */
> -    bool needs_full_sync;          /* Full lflow_table_sync_to_sb needed.
> */
> +
> +    /* SB Logical_Flow row uuids that were orphaned in-memory by
> +     * lflow_ref_unlink_and_prune() and must be deleted from SB after the
> +     * incremental lflow_ref_sync_lflows() pass. */
> +    struct uuidset orphaned_sb_uuids;
>  };
>
>  struct lflow_data {
> diff --git a/northd/lflow-mgr.c b/northd/lflow-mgr.c
> index deceadb74..195e00e0f 100644
> --- a/northd/lflow-mgr.c
> +++ b/northd/lflow-mgr.c
> @@ -743,10 +743,16 @@ lflow_ref_unlink_lflows(struct lflow_ref *lflow_ref,
>   * whose referenced_by list is empty (no other lflow_ref references it).
>   * Unlike lflow_ref_unlink_lflows (which only clears dp bits and sets
>   * linked=false), this function removes the lrns and orphaned lflows
> - * from the in-memory table entirely, without writing to SB. */
> + * from the in-memory table entirely, without writing to SB.
> + *
> + * Because the orphaned lflows are destroyed here (and so are not visible
> to a
> + * later lflow_ref_sync_lflows() pass), the SB uuid of every orphaned
> lflow
> + * that already has an SB row is recorded in 'orphaned_sb_uuids' so the
> caller
> + * can delete those rows from SB after the incremental sync. */
>  void
>  lflow_ref_unlink_and_prune(struct lflow_ref *lflow_ref,
> -                           struct lflow_table *lflow_table)
> +                           struct lflow_table *lflow_table,
> +                           struct uuidset *orphaned_sb_uuids)
>  {
>      lflow_ref_unlink_lflows(lflow_ref, lflow_table);
>
> @@ -756,6 +762,9 @@ lflow_ref_unlink_and_prune(struct lflow_ref *lflow_ref,
>          lflow_ref_node_destroy(lrn);
>
>          if (ovs_list_is_empty(&lflow->referenced_by)) {
> +            if (!uuid_is_zero(&lflow->sb_uuid)) {
> +                uuidset_insert(orphaned_sb_uuids, &lflow->sb_uuid);
> +            }
>              enum ovn_datapath_type dp_type =
>                  ovn_stage_to_datapath_type(lflow->stage);
>              ovs_assert(dp_type < DP_MAX);
> @@ -767,6 +776,34 @@ lflow_ref_unlink_and_prune(struct lflow_ref
> *lflow_ref,
>      }
>  }
>
> +/* Deletes the SB Logical_Flow rows for lflows that were orphaned in
> memory
> + * by lflow_ref_unlink_and_prune() this run.  lflow_ref_sync_lflows()
> cannot
> + * see them because the lflow (and its lflow_ref_node) has already been
> + * destroyed, so their sb_uuids were recorded in 'orphaned_sb_uuids' and
> are
> + * deleted here.
> + *
> + * This is safe even when an orphaned lflow is re-added by a later build
> + * (e.g. the per-datapath multicast flood flow): the re-added lflow is a
> + * fresh in-memory object with a new random sb_uuid (the old one was
> + * destroyed), so its freshly inserted SB row is never in
> + * 'orphaned_sb_uuids'. */
> +void
> +lflow_table_delete_orphaned_sb_flows(
> +        const struct sbrec_logical_flow_table *sb_flow_table,
> +        struct uuidset *orphaned_sb_uuids)
> +{
> +    struct uuidset_node *node;
> +
> +    UUIDSET_FOR_EACH (node, orphaned_sb_uuids) {
> +        const struct sbrec_logical_flow *sbflow =
> +            sbrec_logical_flow_table_get_for_uuid(sb_flow_table,
> +                                                  &node->uuid);
> +        if (sbflow) {
> +            sbrec_logical_flow_delete(sbflow);
> +        }
> +    }
> +}
> +
>  bool
>  lflow_ref_sync_lflows(struct lflow_ref *lflow_ref,
>                        struct lflow_table *lflow_table,
> diff --git a/northd/lflow-mgr.h b/northd/lflow-mgr.h
> index 678e97214..f4cccb5e1 100644
> --- a/northd/lflow-mgr.h
> +++ b/northd/lflow-mgr.h
> @@ -25,6 +25,7 @@ struct ovsdb_idl_txn;
>  struct ovn_datapath;
>  struct ovsdb_idl_row;
>  struct ovn_lflow;
> +struct uuidset;
>
>  /* lflow map which stores the logical flows. */
>  struct lflow_table {
> @@ -58,7 +59,10 @@ struct lflow_ref *lflow_ref_create(void);
>  void lflow_ref_destroy(struct lflow_ref *);
>  void lflow_ref_clear(struct lflow_ref *lflow_ref);
>  void lflow_ref_unlink_lflows(struct lflow_ref *, struct lflow_table *);
> -void lflow_ref_unlink_and_prune(struct lflow_ref *, struct lflow_table *);
> +void lflow_ref_unlink_and_prune(struct lflow_ref *, struct lflow_table *,
> +                                struct uuidset *);
> +void lflow_table_delete_orphaned_sb_flows(
> +        const struct sbrec_logical_flow_table *, struct uuidset *);
>  bool lflow_ref_sync_lflows(struct lflow_ref *,
>                             struct lflow_table *lflow_table,
>                             struct ovsdb_idl_txn *ovnsb_txn,
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 6866068fa..1097fa998 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -24828,4 +24828,65 @@ CHECK_NO_CHANGE_AFTER_RECOMPUTE
>
>  OVN_CLEANUP_NORTHD
>  AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([check shared LB VIP flow survives datapath group full sync])
> +AT_KEYWORDS([loadbalancer])
> +ovn_start
> +
> +# Two load balancers on the same switch share the VIP 1.1.1.1:80 with the
> +# same backend, so they both reference one logical flow L in ls_in_lb.
> +# L's datapath refcnt for sw1 is therefore 2.  The bug this test guards
> +# against: a full sync of the datapath group retains the lflow_ref_node of
> +# an LB that has already dropped the shared VIP, and a later change to
> that
> +# LB unlinks the retained node again, releasing sw1's datapath refcnt a
> +# second time.  That empties L's datapath bitmap and a later full sync
> +# prunes L and deletes its SB row even though lb2 still requires it.
> +check ovn-nbctl --wait=hv ls-add sw1
> +check ovn-nbctl --wait=hv lsp-add sw1 p1 -- \
> +    lsp-set-addresses p1 "aa:aa:aa:aa:aa:01 192.168.10.2"
> +
> +check ovn-nbctl --wait=hv lb-add lb1 1.1.1.1:80 192.168.10.2:80
> +check ovn-nbctl --wait=hv lb-add lb1 2.2.2.2:80 192.168.10.2:80
> +check ovn-nbctl --wait=hv lb-add lb2 1.1.1.1:80 192.168.10.2:80
> +check ovn-nbctl --wait=hv ls-lb-add sw1 lb1
> +check ovn-nbctl --wait=hv ls-lb-add sw1 lb2
> +
> +# Baseline: the shared VIP flow exists.
> +OVS_WAIT_UNTIL([test 1 -le `ovn-sbctl dump-flows sw1 | grep "ip4.dst ==
> 1.1.1.1" | grep -c .`])
> +
> +# RUN 1: remove 1.1.1.1 from lb1 (lb1 keeps 2.2.2.2) and change p1's
> +# address in the SAME engine run.  The port update makes the multicast
> node
> +# recompute, which forces the datapath group to take the FULL sync path.
> +# Unlike the incremental sync, the full sync does not destroy the
> lflow_ref
> +# nodes that were already unlinked, so lb1's now-unlinked 1.1.1.1 node is
> +# retained.  The two changes must land in a single run, so they are
> batched
> +# into one transaction.
> +check ovn-nbctl --wait=hv lb-del lb1 1.1.1.1:80 -- \
> +    lsp-set-addresses p1 "aa:aa:aa:aa:aa:01 192.168.20.2"
> +# The shared VIP flow is still present (lb2 keeps it).
> +OVS_WAIT_UNTIL([test 1 -le `ovn-sbctl dump-flows sw1 | grep "ip4.dst ==
> 1.1.1.1" | grep -c .`])
> +
> +# RUN 2: change lb1 (append VIP 3.3.3.3).  This re-unlinks lb1's
> lflow_ref.
> +# Without the fix this hits the retained 1.1.1.1 node and double-releases
> +# sw1's datapath count, emptying the shared flow's datapath bitmap.
> +check ovn-nbctl --wait=hv lb-add lb1 3.3.3.3:80 192.168.10.2:80
> +
> +# RUN 3: another port address change -> needs_full_sync -> full sync.
> +# Without the fix this prunes the shared 1.1.1.1 flow (now empty bitmap)
> +# and deletes its SB row.
> +check ovn-nbctl --wait=hv lsp-set-addresses p1 "aa:aa:aa:aa:aa:01
> 192.168.30.2"
> +
> +# REGRESSION ASSERTION: the shared VIP flow must still be present, since
> lb2
> +# still requires 1.1.1.1.  Without the fix it was deleted.
> +OVS_WAIT_UNTIL([test 1 -le `ovn-sbctl dump-flows sw1 | grep "ip4.dst ==
> 1.1.1.1" | grep -c .`])
> +
> +# Sanity: p1's port flows now use the final address 192.168.30.2 (confirms
> +# the port changes in runs 1 and 3 were actually applied).
> +OVS_WAIT_UNTIL([test 1 -le `ovn-sbctl dump-flows sw1 | grep
> "192.168.30.2" | grep -c .`])
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
>
> --
> 2.55.0
>
> _______________________________________________
> dev mailing list
> [email protected]
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
>
>
Thank you Jacob,

applied to main with the nit fixed.

Regards,
Ales
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to