diff mbox series

[ovs-dev,v5] northd: Split en_lflow into compute and sync nodes.

Message ID 20261001200107.2769693-1-jtanenba@redhat.com
State Accepted
Delegated to: Ales Musil
Headers show
Series [ovs-dev,v5] northd: Split en_lflow into compute and sync nodes. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/github-robot-_ovn-kubernetes success github build: passed

Commit Message

Jacob Tanenbaum Oct. 1, 2026, 8:01 p.m. UTC
Split ovn-northd's en_lflow engine node into a compute-only
en_lflow node and a new en_dp_group node that
handles datapath-group resolution and all SB database
writes.  This separation makes en_lflow independent of the
SB database, preparing for future incremental processing
improvements.

en_lflow's handlers now track dirty lflow_refs in a hmapx
instead of calling lflow_ref_sync_lflows directly.
en_dp_group drains the dirty set on its lflow
handler, falling back to a full lflow_table_sync_to_sb when
en_lflow did a full recompute or when the IGMP/MLD handler
set the needs_full_sync flag.

A new lflow_ref_unlink_and_prune() function replaces
lflow_ref_resync_flows() for the IGMP and IC-learned
service monitor handlers.  These handlers use a single
shared lflow_ref whose flows are also built by the
per-datapath pipeline (with lflow_ref = NULL).  The
per-datapath build's dp bits are not tracked by any
lflow_ref, so lflow_ref_unlink_lflows alone cannot clear
them via dp_refcnt.  lflow_ref_unlink_and_prune destroys
all lrns and orphaned lflows in-memory without SB writes,
equivalent to lflow_ref_resync_flows' cleanup.

The lflow_table_add_lflow__ upgrade-to-bitmap logic (for
single lflow_refs that contribute multiple datapaths to the
same lflow) is extended with per-datapath dp_refcnt
accounting during mid-cycle bitmap upgrades, preventing
premature dp bit release when multiple lflow_refs share a
flow across overlapping datapaths.

Two test cases need to have sections removed. These tests relied upon
encountering a stale dp_group would cause all the lflows to be
recalculated. After this patch that is no longer the case and only the
lflows directly related to the transaction get modified. Encountering a
stale dp_group should no longer rebuild all the lflows.

Reported-at: https://redhat.atlassian.net/browse/FDP-2747
Assisted-by: Claude Opus 4.8, Claude Code
Signed-off-by: Jacob Tanenbaum <jtanenba@redhat.com>

---
v5:
    * removed overactive cleaning and formatting issues
v4:
    * rebased to master
v3:
    * rebased to master
v2:
    * renamed the engine form dp-group-resolved to dp-group
    * changed the ordering of includes
    * changed to get all input data before operations
    * fixed a leak found by AI
    * added the dirty_lflow_refs to tracked data
    * added section on why some testing is removed
    * removed redundent calls to hmapx_add() when everything was going
      to recalculate
---
 northd/automake.mk               |   2 +
 northd/en-dp-group.c             | 116 ++++++++++++++
 northd/en-dp-group.h             |  28 ++++
 northd/en-lflow.c                | 145 ++++++------------
 northd/en-lflow.h                |   8 +
 northd/en-northd-output.c        |   4 +-
 northd/en-northd-output.h        |   3 +-
 northd/inc-proc-northd.c         |  29 +++-
 northd/lflow-mgr.c               | 170 +++++++++++++++++----
 northd/lflow-mgr.h               |   8 +-
 northd/northd.c                  | 251 +++++++------------------------
 northd/northd.h                  |  39 +++--
 tests/ovn-inc-proc-graph-dump.at |  12 +-
 tests/ovn-northd.at              |  81 +++++++---
 14 files changed, 504 insertions(+), 392 deletions(-)
 create mode 100644 northd/en-dp-group.c
 create mode 100644 northd/en-dp-group.h

Comments

Ales Musil Oct. 2, 2026, 11:55 a.m. UTC | #1
On Thu, Oct 1, 2026 at 10:01 PM Jacob Tanenbaum <jtanenba@redhat.com> wrote:

> Split ovn-northd's en_lflow engine node into a compute-only
> en_lflow node and a new en_dp_group node that
> handles datapath-group resolution and all SB database
> writes.  This separation makes en_lflow independent of the
> SB database, preparing for future incremental processing
> improvements.
>
> en_lflow's handlers now track dirty lflow_refs in a hmapx
> instead of calling lflow_ref_sync_lflows directly.
> en_dp_group drains the dirty set on its lflow
> handler, falling back to a full lflow_table_sync_to_sb when
> en_lflow did a full recompute or when the IGMP/MLD handler
> set the needs_full_sync flag.
>
> A new lflow_ref_unlink_and_prune() function replaces
> lflow_ref_resync_flows() for the IGMP and IC-learned
> service monitor handlers.  These handlers use a single
> shared lflow_ref whose flows are also built by the
> per-datapath pipeline (with lflow_ref = NULL).  The
> per-datapath build's dp bits are not tracked by any
> lflow_ref, so lflow_ref_unlink_lflows alone cannot clear
> them via dp_refcnt.  lflow_ref_unlink_and_prune destroys
> all lrns and orphaned lflows in-memory without SB writes,
> equivalent to lflow_ref_resync_flows' cleanup.
>
> The lflow_table_add_lflow__ upgrade-to-bitmap logic (for
> single lflow_refs that contribute multiple datapaths to the
> same lflow) is extended with per-datapath dp_refcnt
> accounting during mid-cycle bitmap upgrades, preventing
> premature dp bit release when multiple lflow_refs share a
> flow across overlapping datapaths.
>
> Two test cases need to have sections removed. These tests relied upon
> encountering a stale dp_group would cause all the lflows to be
> recalculated. After this patch that is no longer the case and only the
> lflows directly related to the transaction get modified. Encountering a
> stale dp_group should no longer rebuild all the lflows.
>
> Reported-at: https://redhat.atlassian.net/browse/FDP-2747
> Assisted-by: Claude Opus 4.8, Claude Code
> Signed-off-by: Jacob Tanenbaum <jtanenba@redhat.com>
>
> ---
> v5:
>     * removed overactive cleaning and formatting issues
> v4:
>     * rebased to master
> v3:
>     * rebased to master
> v2:
>     * renamed the engine form dp-group-resolved to dp-group
>     * changed the ordering of includes
>     * changed to get all input data before operations
>     * fixed a leak found by AI
>     * added the dirty_lflow_refs to tracked data
>     * added section on why some testing is removed
>     * removed redundent calls to hmapx_add() when everything was going
>       to recalculate
> ---
>  northd/automake.mk               |   2 +
>  northd/en-dp-group.c             | 116 ++++++++++++++
>  northd/en-dp-group.h             |  28 ++++
>  northd/en-lflow.c                | 145 ++++++------------
>  northd/en-lflow.h                |   8 +
>  northd/en-northd-output.c        |   4 +-
>  northd/en-northd-output.h        |   3 +-
>  northd/inc-proc-northd.c         |  29 +++-
>  northd/lflow-mgr.c               | 170 +++++++++++++++++----
>  northd/lflow-mgr.h               |   8 +-
>  northd/northd.c                  | 251 +++++++------------------------
>  northd/northd.h                  |  39 +++--
>  tests/ovn-inc-proc-graph-dump.at |  12 +-
>  tests/ovn-northd.at              |  81 +++++++---
>  14 files changed, 504 insertions(+), 392 deletions(-)
>  create mode 100644 northd/en-dp-group.c
>  create mode 100644 northd/en-dp-group.h
>
> diff --git a/northd/automake.mk b/northd/automake.mk
> index 45ca0337f..1d3044930 100644
> --- a/northd/automake.mk
> +++ b/northd/automake.mk
> @@ -16,6 +16,8 @@ northd_ovn_northd_SOURCES = \
>         northd/en-datapath-logical-router.h \
>         northd/en-datapath-sync.c \
>         northd/en-datapath-sync.h \
> +       northd/en-dp-group.c \
> +       northd/en-dp-group.h \
>         northd/en-ecmp-nexthop.c \
>         northd/en-ecmp-nexthop.h \
>         northd/en-global-config.c \
> diff --git a/northd/en-dp-group.c b/northd/en-dp-group.c
> new file mode 100644
> index 000000000..e33e1f407
> --- /dev/null
> +++ b/northd/en-dp-group.c
> @@ -0,0 +1,116 @@
> +/*
> + * Copyright (c) 2026, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#include <config.h>
> +
> +#include "en-datapath-sync.h"
> +#include "en-dp-group.h"
> +#include "en-global-config.h"
> +#include "en-lflow.h"
> +#include "lflow-mgr.h"
> +#include "lib/inc-proc-eng.h"
> +#include "lib/stopwatch-names.h"
> +#include "northd.h"
> +#include "openvswitch/vlog.h"
> +#include "stopwatch.h"
> +#include "timeval.h"
> +
> +VLOG_DEFINE_THIS_MODULE(en_dp_group);
> +
> +void *
> +en_dp_group_init(struct engine_node *node OVS_UNUSED,
> +                 struct engine_arg *arg OVS_UNUSED)
> +{
> +    return NULL;
> +}
> +
> +static void
> +dp_group_sync_to_sb(struct engine_node *node,
> +                    struct lflow_data *lflow_data)
> +{
> +    const struct engine_context *eng_ctx = engine_get_context();
> +    struct all_synced_datapaths *all_dps =
> +        engine_get_input_data("datapath_sync", node);
> +    struct ed_type_global_config *global_config =
> +        engine_get_input_data("global_config", node);
> +
> +    const struct sbrec_logical_flow_table *sb_flow_table =
> +        EN_OVSDB_GET(engine_get_input("SB_logical_flow", node));
> +    const struct sbrec_logical_dp_group_table *sb_dpgrp_table =
> +        EN_OVSDB_GET(engine_get_input("SB_logical_dp_group", node));
> +
> +    stopwatch_start(LFLOWS_TO_SB_STOPWATCH_NAME, time_msec());
> +    lflow_table_sync_to_sb(lflow_data->lflow_table,
> +                           eng_ctx->ovnsb_idl_txn,
> +                           all_dps->synced_dps,
> +                           global_config->ovn_internal_version_changed,
> +                           sb_flow_table, sb_dpgrp_table);
> +    stopwatch_stop(LFLOWS_TO_SB_STOPWATCH_NAME, time_msec());
> +}
> +
> +enum engine_node_state
> +en_dp_group_run(struct engine_node *node,
> +                void *data OVS_UNUSED)
> +{
> +    struct lflow_data *lflow_data = engine_get_input_data("lflow", node);
> +
> +    dp_group_sync_to_sb(node, lflow_data);
> +    return EN_UPDATED;
> +}
> +
> +enum engine_input_handler_result
> +dp_group_lflow_handler(struct engine_node *node,
> +                       void *data OVS_UNUSED)
> +{
> +    struct lflow_data *lflow_data = engine_get_input_data("lflow", node);
> +
> +    const struct engine_context *eng_ctx = engine_get_context();
> +    struct all_synced_datapaths *all_dps =
> +        engine_get_input_data("datapath_sync", node);
> +
> +    const struct sbrec_logical_flow_table *sb_flow_table =
> +        EN_OVSDB_GET(engine_get_input("SB_logical_flow", node));
> +    const struct sbrec_logical_dp_group_table *sb_dpgrp_table =
> +        EN_OVSDB_GET(engine_get_input("SB_logical_dp_group", 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) {
> +
> +        dp_group_sync_to_sb(node, lflow_data);
> +        return EN_HANDLED_UPDATED;
> +    }
> +
> +    struct hmapx_node *hmapx_node;
> +    HMAPX_FOR_EACH (hmapx_node, &lflow_data->trk_data.dirty_lflow_refs) {
> +        struct lflow_ref *ref = hmapx_node->data;
> +        if (!lflow_ref_sync_lflows(ref, lflow_data->lflow_table,
> +                                   eng_ctx->ovnsb_idl_txn,
> +                                   all_dps->synced_dps,
> +
>  global_config->ovn_internal_version_changed,
> +                                   sb_flow_table, sb_dpgrp_table)) {
> +            return EN_UNHANDLED;
> +        }
> +    }
> +    return EN_HANDLED_UPDATED;
> +}
> +
> +void
> +en_dp_group_cleanup(void *data OVS_UNUSED)
> +{
> +}
> diff --git a/northd/en-dp-group.h b/northd/en-dp-group.h
> new file mode 100644
> index 000000000..a6422df66
> --- /dev/null
> +++ b/northd/en-dp-group.h
> @@ -0,0 +1,28 @@
> +/*
> + * Copyright (c) 2026, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#ifndef EN_DP_GROUP_H
> +#define EN_DP_GROUP_H 1
> +
> +#include "lib/inc-proc-eng.h"
> +
> +void *en_dp_group_init(struct engine_node *node, struct engine_arg *arg);
> +enum engine_node_state en_dp_group_run(struct engine_node *node, void
> *data);
> +void en_dp_group_cleanup(void *data);
> +enum engine_input_handler_result
> +dp_group_lflow_handler(struct engine_node *node, void *data);
> +
> +#endif /* EN_DP_GROUP_H */
> diff --git a/northd/en-lflow.c b/northd/en-lflow.c
> index e8e6cdaa2..f2ae40ec8 100644
> --- a/northd/en-lflow.c
> +++ b/northd/en-lflow.c
> @@ -68,10 +68,6 @@ lflow_get_input_data(struct engine_node *node,
>      struct all_synced_datapaths *all_dps =
>          engine_get_input_data("datapath_sync", node);
>
> -    lflow_input->sbrec_logical_flow_table =
> -        EN_OVSDB_GET(engine_get_input("SB_logical_flow", node));
> -    lflow_input->sbrec_logical_dp_group_table =
> -        EN_OVSDB_GET(engine_get_input("SB_logical_dp_group", node));
>      lflow_input->sbrec_acl_id_table =
>          EN_OVSDB_GET(engine_get_input("SB_acl_id", node));
>
> @@ -119,8 +115,6 @@ lflow_get_input_data(struct engine_node *node,
>  enum engine_node_state
>  en_lflow_run(struct engine_node *node, void *data)
>  {
> -    const struct engine_context *eng_ctx = engine_get_context();
> -
>      struct lflow_input lflow_input;
>      lflow_get_input_data(node, &lflow_input);
>
> @@ -130,8 +124,7 @@ en_lflow_run(struct engine_node *node, void *data)
>      lflow_reset_northd_refs(&lflow_input);
>      lflow_ref_clear(lflow_input.igmp_lflow_ref);
>
> -    build_lflows(eng_ctx->ovnsb_idl_txn, &lflow_input,
> -                 lflow_data->lflow_table);
> +    build_lflows(&lflow_input, lflow_data->lflow_table);
>
>      return EN_UPDATED;
>  }
> @@ -149,31 +142,25 @@ lflow_northd_handler(struct engine_node *node,
>          return EN_UNHANDLED;
>      }
>
> -    const struct engine_context *eng_ctx = engine_get_context();
>      struct lflow_data *lflow_data = data;
>
>      struct lflow_input lflow_input;
>      lflow_get_input_data(node, &lflow_input);
>
> -    if (!lflow_handle_northd_lr_changes(eng_ctx->ovnsb_idl_txn,
> -
> &northd_data->trk_data.trk_routers,
> -                                        &lflow_input,
> -                                        lflow_data->lflow_table)) {
> -        return EN_UNHANDLED;
> -    }
> +    lflow_handle_northd_lr_changes(&northd_data->trk_data.trk_routers,
> +                                   &lflow_input,
> +                                   lflow_data->lflow_table,
> +
>  &lflow_data->trk_data.dirty_lflow_refs);
>
> -    if (!lflow_handle_northd_port_changes(eng_ctx->ovnsb_idl_txn,
> -                                          &northd_data->trk_data.trk_lsps,
> -                                          &lflow_input,
> -                                          lflow_data->lflow_table)) {
> -        return EN_UNHANDLED;
> -    }
> +    lflow_handle_northd_port_changes(&northd_data->trk_data.trk_lsps,
> +                                     &lflow_input,
> +                                     lflow_data->lflow_table,
> +
>  &lflow_data->trk_data.dirty_lflow_refs);
>
> -    if (!lflow_handle_northd_lb_changes(
> -            eng_ctx->ovnsb_idl_txn, &northd_data->trk_data.trk_lbs,
> -            &lflow_input, lflow_data->lflow_table)) {
> -        return EN_UNHANDLED;
> -    }
> +    lflow_handle_northd_lb_changes(&northd_data->trk_data.trk_lbs,
> +                                   &lflow_input,
> +                                   lflow_data->lflow_table,
> +
>  &lflow_data->trk_data.dirty_lflow_refs);
>
>      return EN_HANDLED_UPDATED;
>  }
> @@ -189,17 +176,14 @@ lflow_lr_stateful_handler(struct engine_node *node,
> void *data)
>          return EN_UNHANDLED;
>      }
>
> -    const struct engine_context *eng_ctx = engine_get_context();
>      struct lflow_data *lflow_data = data;
>      struct lflow_input lflow_input;
>
>      lflow_get_input_data(node, &lflow_input);
> -    if (!lflow_handle_lr_stateful_changes(eng_ctx->ovnsb_idl_txn,
> -                                          &lr_sful_data->trk_data,
> -                                          &lflow_input,
> -                                          lflow_data->lflow_table)) {
> -        return EN_UNHANDLED;
> -    }
> +    lflow_handle_lr_stateful_changes(&lr_sful_data->trk_data,
> +                                     &lflow_input,
> +                                     lflow_data->lflow_table,
> +
>  &lflow_data->trk_data.dirty_lflow_refs);
>
>      return EN_HANDLED_UPDATED;
>  }
> @@ -214,17 +198,14 @@ lflow_ls_stateful_handler(struct engine_node *node,
> void *data)
>          return EN_UNHANDLED;
>      }
>
> -    const struct engine_context *eng_ctx = engine_get_context();
>      struct lflow_data *lflow_data = data;
>      struct lflow_input lflow_input;
>
>      lflow_get_input_data(node, &lflow_input);
> -    if (!lflow_handle_ls_stateful_changes(eng_ctx->ovnsb_idl_txn,
> -                                          &ls_sful_data->trk_data,
> -                                          &lflow_input,
> -                                          lflow_data->lflow_table)) {
> -        return EN_UNHANDLED;
> -    }
> +    lflow_handle_ls_stateful_changes(&ls_sful_data->trk_data,
> +                                     &lflow_input,
> +                                     lflow_data->lflow_table,
> +
>  &lflow_data->trk_data.dirty_lflow_refs);
>
>      return EN_HANDLED_UPDATED;
>  }
> @@ -235,35 +216,19 @@ lflow_multicast_igmp_handler(struct engine_node
> *node, void *data)
>      struct multicast_igmp_data *mcast_igmp_data =
>          engine_get_input_data("multicast_igmp", node);
>
> -    const struct engine_context *eng_ctx = engine_get_context();
>      struct lflow_data *lflow_data = data;
>      struct lflow_input lflow_input;
>      lflow_get_input_data(node, &lflow_input);
>
> -    if (!lflow_ref_resync_flows(mcast_igmp_data->lflow_ref,
> -                                lflow_data->lflow_table,
> -                                eng_ctx->ovnsb_idl_txn,
> -                                lflow_input.dps,
> -                                lflow_input.ovn_internal_version_changed,
> -                                lflow_input.sbrec_logical_flow_table,
> -
> lflow_input.sbrec_logical_dp_group_table)) {
> -        return EN_UNHANDLED;
> -    }
> +    lflow_ref_unlink_and_prune(mcast_igmp_data->lflow_ref,
> +                              lflow_data->lflow_table);
>
>      build_igmp_lflows(&mcast_igmp_data->igmp_groups,
>                        &lflow_input.ls_datapaths->datapaths,
>                        lflow_data->lflow_table,
>                        mcast_igmp_data->lflow_ref);
>
> -    if (!lflow_ref_sync_lflows(mcast_igmp_data->lflow_ref,
> -                               lflow_data->lflow_table,
> -                               eng_ctx->ovnsb_idl_txn,
> -                               lflow_input.dps,
> -                               lflow_input.ovn_internal_version_changed,
> -                               lflow_input.sbrec_logical_flow_table,
> -                               lflow_input.sbrec_logical_dp_group_table))
> {
> -        return EN_UNHANDLED;
> -    }
> +    lflow_data->trk_data.needs_full_sync = true;
>
>      return EN_HANDLED_UPDATED;
>  }
> @@ -280,7 +245,6 @@ lflow_group_ecmp_route_change_handler(struct
> engine_node *node,
>          return EN_UNHANDLED;
>      }
>
> -    const struct engine_context *eng_ctx = engine_get_context();
>      struct lflow_data *lflow_data = data;
>
>      struct lflow_input lflow_input;
> @@ -289,26 +253,16 @@ lflow_group_ecmp_route_change_handler(struct
> engine_node *node,
>      struct group_ecmp_datapath *route_node;
>      struct hmapx_node *hmapx_node;
>
> -    /* We need to handle deletions before additions as they could
> potentially
> -     * overlap. */
>      HMAPX_FOR_EACH (hmapx_node,
>
>  &group_ecmp_route_data->trk_data.deleted_datapath_routes) {
>          route_node = hmapx_node->data;
>          lflow_ref_unlink_lflows(route_node->lflow_ref,
>                                  lflow_data->lflow_table);
>
> -        bool handled = lflow_ref_sync_lflows(
> -            route_node->lflow_ref, lflow_data->lflow_table,
> -            eng_ctx->ovnsb_idl_txn, lflow_input.dps,
> -            lflow_input.ovn_internal_version_changed,
> -            lflow_input.sbrec_logical_flow_table,
> -            lflow_input.sbrec_logical_dp_group_table);
> -        if (!handled) {
> -            return EN_UNHANDLED;
> -        }
> +        hmapx_add(&lflow_data->trk_data.dirty_lflow_refs,
> +                  route_node->lflow_ref);
>      }
>
> -    /* Now we handle created or updated route nodes. */
>      struct hmapx *crupdated_datapath_routes =
>          &group_ecmp_route_data->trk_data.crupdated_datapath_routes;
>      HMAPX_FOR_EACH (hmapx_node, crupdated_datapath_routes) {
> @@ -318,16 +272,8 @@ lflow_group_ecmp_route_change_handler(struct
> engine_node *node,
>          build_route_data_flows_for_lrouter(
>              route_node->od, lflow_data->lflow_table,
>              route_node, lflow_input.bfd_ports);
> -
> -        bool handled = lflow_ref_sync_lflows(
> -            route_node->lflow_ref, lflow_data->lflow_table,
> -            eng_ctx->ovnsb_idl_txn, lflow_input.dps,
> -            lflow_input.ovn_internal_version_changed,
> -            lflow_input.sbrec_logical_flow_table,
> -            lflow_input.sbrec_logical_dp_group_table);
> -        if (!handled) {
> -            return EN_UNHANDLED;
> -        }
> +        hmapx_add(&lflow_data->trk_data.dirty_lflow_refs,
> +                  route_node->lflow_ref);
>      }
>
>      return EN_HANDLED_UPDATED;
> @@ -340,7 +286,6 @@ lflow_ic_learned_svc_mons_handler(struct engine_node
> *node,
>      struct ic_learned_svc_monitors_data *ic_learned_svc_monitors_data =
>          engine_get_input_data("ic_learned_svcs", node);
>
> -    const struct engine_context *eng_ctx = engine_get_context();
>      struct lflow_data *lflow_data = data;
>      struct lflow_input lflow_input;
>      lflow_get_input_data(node, &lflow_input);
> @@ -351,16 +296,8 @@ lflow_ic_learned_svc_mons_handler(struct engine_node
> *node,
>              &ic_learned_svc_monitors_data->ic_learned_svc_monitors_map,
>              ic_learned_svc_monitors_data->lflow_ref);
>
> -    if (!lflow_ref_resync_flows(
> -            ic_learned_svc_monitors_data->lflow_ref,
> -            lflow_data->lflow_table,
> -            eng_ctx->ovnsb_idl_txn,
> -            lflow_input.dps,
> -            lflow_input.ovn_internal_version_changed,
> -            lflow_input.sbrec_logical_flow_table,
> -            lflow_input.sbrec_logical_dp_group_table)) {
> -        return EN_UNHANDLED;
> -    }
> +    lflow_ref_unlink_and_prune(ic_learned_svc_monitors_data->lflow_ref,
> +                               lflow_data->lflow_table);
>
>      build_lswitch_arp_nd_ic_learned_svc_mon(
>          &svc_mons_data,
> @@ -368,16 +305,7 @@ lflow_ic_learned_svc_mons_handler(struct engine_node
> *node,
>          lflow_input.svc_monitor_mac,
>          lflow_data->lflow_table);
>
> -    if (!lflow_ref_sync_lflows(
> -            ic_learned_svc_monitors_data->lflow_ref,
> -            lflow_data->lflow_table,
> -            eng_ctx->ovnsb_idl_txn,
> -            lflow_input.dps,
> -            lflow_input.ovn_internal_version_changed,
> -            lflow_input.sbrec_logical_flow_table,
> -            lflow_input.sbrec_logical_dp_group_table)) {
> -        return EN_UNHANDLED;
> -    }
> +    lflow_data->trk_data.needs_full_sync = true;
>
>      return EN_HANDLED_UPDATED;
>  }
> @@ -388,6 +316,9 @@ void *en_lflow_init(struct engine_node *node
> OVS_UNUSED,
>      struct lflow_data *data = xmalloc(sizeof *data);
>      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;
> +
>      return data;
>  }
>
> @@ -395,4 +326,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);
> +}
> +
> +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;
>  }
> diff --git a/northd/en-lflow.h b/northd/en-lflow.h
> index 99bcfda15..fd3f2427f 100644
> --- a/northd/en-lflow.h
> +++ b/northd/en-lflow.h
> @@ -7,17 +7,25 @@
>  #include <stdlib.h>
>  #include <stdio.h>
>
> +#include "lib/hmapx.h"
>  #include "lib/inc-proc-eng.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.
> */
> +};
> +
>  struct lflow_data {
>      struct lflow_table *lflow_table;
> +    struct lflow_tracked_data trk_data;
>  };
>
>  enum engine_node_state en_lflow_run(struct engine_node *node, void *data);
>  void *en_lflow_init(struct engine_node *node, struct engine_arg *arg);
>  void en_lflow_cleanup(void *data);
> +void en_lflow_clear_tracked_data(void *data);
>  enum engine_input_handler_result lflow_northd_handler(struct engine_node
> *,
>                                                        void *data);
>  enum engine_input_handler_result
> diff --git a/northd/en-northd-output.c b/northd/en-northd-output.c
> index b492a771c..ef42bf9b7 100644
> --- a/northd/en-northd-output.c
> +++ b/northd/en-northd-output.c
> @@ -51,8 +51,8 @@ northd_output_sync_to_sb_handler(struct engine_node
> *node OVS_UNUSED,
>  }
>
>  enum engine_input_handler_result
> -northd_output_lflow_handler(struct engine_node *node OVS_UNUSED,
> -                            void *data OVS_UNUSED)
> +northd_output_dp_group_handler(struct engine_node *node OVS_UNUSED,
> +                               void *data OVS_UNUSED)
>  {
>      return EN_HANDLED_UPDATED;
>  }
> diff --git a/northd/en-northd-output.h b/northd/en-northd-output.h
> index b7053e60c..d8854c2a2 100644
> --- a/northd/en-northd-output.h
> +++ b/northd/en-northd-output.h
> @@ -13,7 +13,8 @@ enum engine_input_handler_result
>  northd_output_sync_to_sb_handler(struct engine_node *node,
>                                   void *data OVS_UNUSED);
>  enum engine_input_handler_result
> -northd_output_lflow_handler(struct engine_node *node, void *data
> OVS_UNUSED);
> +northd_output_dp_group_handler(struct engine_node *node,
> +                               void *data OVS_UNUSED);
>  enum engine_input_handler_result
>  northd_output_mac_binding_aging_handler(struct engine_node *node,
>                                          void *data OVS_UNUSED);
> diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
> index d2f28666c..53604a20a 100644
> --- a/northd/inc-proc-northd.c
> +++ b/northd/inc-proc-northd.c
> @@ -37,6 +37,7 @@
>  #include "en-multicast.h"
>  #include "en-northd.h"
>  #include "en-lflow.h"
> +#include "en-dp-group.h"
>  #include "en-northd-output.h"
>  #include "en-meters.h"
>  #include "en-sampling-app.h"
> @@ -160,7 +161,8 @@ enum sb_engine_node {
>  static ENGINE_NODE(northd, CLEAR_TRACKED_DATA, SB_WRITE);
>  static ENGINE_NODE(sync_from_sb, SB_WRITE);
>  static ENGINE_NODE(sampling_app);
> -static ENGINE_NODE(lflow, SB_WRITE);
> +static ENGINE_NODE(lflow, CLEAR_TRACKED_DATA);
> +static ENGINE_NODE(dp_group, SB_WRITE);
>  static ENGINE_NODE(mac_binding_aging, SB_WRITE);
>  static ENGINE_NODE(mac_binding_aging_waker);
>  static ENGINE_NODE(northd_output);
> @@ -399,9 +401,7 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
>      engine_add_input(&en_multicast_igmp, &en_sb_igmp_group, NULL);
>
>      engine_add_input(&en_lflow, &en_sync_meters, NULL);
> -    engine_add_input(&en_lflow, &en_sb_logical_flow, NULL);
>      engine_add_input(&en_lflow, &en_sb_multicast_group, NULL);
> -    engine_add_input(&en_lflow, &en_sb_logical_dp_group, NULL);
>      engine_add_input(&en_lflow, &en_bfd_sync, NULL);
>      engine_add_input(&en_lflow, &en_route_policies, NULL);
>      /* Route changes are propagated to en_lflow through the
> en_group_ecmp_route
> @@ -436,6 +436,25 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
>      engine_add_input(&en_lflow, &en_ic_learned_svc_monitors,
>                       lflow_ic_learned_svc_mons_handler);
>
> +    engine_add_input(&en_dp_group, &en_lflow,
> +                     dp_group_lflow_handler);
> +    engine_add_input(&en_dp_group, &en_sb_logical_flow, NULL);
> +    engine_add_input(&en_dp_group, &en_sb_logical_dp_group, NULL);
> +    /* dp_group reads synced datapath arrays (dps[DP_MAX]) when
> +     * calling lflow_table_sync_to_sb / lflow_ref_sync_lflows.  A noop
> +     * handler is sufficient because en_lflow already recomputes when
> +     * datapath_sync changes; dp_group just needs access to
> +     * the data. */
> +    engine_add_input(&en_dp_group, &en_datapath_sync,
> +                     engine_noop_handler);
> +    /* dp_group reads ovn_internal_version_changed from
> +     * global_config when syncing flows to SB.  Changes to
> +     * global_config propagate through en_lflow (which has its own
> +     * global_config handler), so a noop handler here avoids a
> +     * redundant recompute. */
> +    engine_add_input(&en_dp_group, &en_global_config,
> +                     engine_noop_handler);
> +
>      engine_add_input(&en_sync_to_sb_addr_set, &en_northd, NULL);
>      engine_add_input(&en_sync_to_sb_addr_set, &en_lr_stateful, NULL);
>      engine_add_input(&en_sync_to_sb_addr_set, &en_sb_address_set, NULL);
> @@ -490,8 +509,8 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
>      engine_add_input(&en_northd_output, &en_sync_from_sb, NULL);
>      engine_add_input(&en_northd_output, &en_sync_to_sb,
>                       northd_output_sync_to_sb_handler);
> -    engine_add_input(&en_northd_output, &en_lflow,
> -                     northd_output_lflow_handler);
> +    engine_add_input(&en_northd_output, &en_dp_group,
> +                     northd_output_dp_group_handler);
>      engine_add_input(&en_northd_output, &en_mac_binding_aging,
>                       northd_output_mac_binding_aging_handler);
>      engine_add_input(&en_northd_output, &en_fdb_aging,
> diff --git a/northd/lflow-mgr.c b/northd/lflow-mgr.c
> index f51df9ce0..deceadb74 100644
> --- a/northd/lflow-mgr.c
> +++ b/northd/lflow-mgr.c
> @@ -261,6 +261,28 @@ lflow_table_set_size(struct lflow_table *lflow_table,
> size_t size)
>      lflow_table->entries.n = size;
>  }
>
> +/* An lflow that applies to no datapaths (its dp group bitmap is empty)
> must
> + * not exist in the SB DB.  Such lflows can be left in the table by
> en_lflow's
> + * incremental handlers, which unlink lflows (clearing their dp bits) and
> defer
> + * the teardown to the dp-group-resolved sync stage.  Release the lflow's
> dp
> + * group and destroy it; its SB row (if any) is removed by the caller.
> Returns
> + * true if the lflow was destroyed. */
> +static bool
> +lflow_prune_if_no_datapaths(struct lflow_table *lflow_table,
> +                            struct ovn_lflow *lflow)
> +{
> +    if (dynamic_bitmap_count1(&lflow->dpg_bitmap)) {
> +        return false;
> +    }
> +
> +    enum ovn_datapath_type dp_type =
> ovn_stage_to_datapath_type(lflow->stage);
> +    ovs_assert(dp_type < DP_MAX);
> +    ovn_dp_group_release(&lflow_table->dp_groups[dp_type], lflow->dpg);
> +    lflow->dpg = NULL;
> +    ovn_lflow_destroy(lflow_table, lflow);
> +    return true;
> +}
> +
>  void
>  lflow_table_sync_to_sb(struct lflow_table *lflow_table,
>                         struct ovsdb_idl_txn *ovnsb_txn,
> @@ -287,6 +309,12 @@ lflow_table_sync_to_sb(struct lflow_table
> *lflow_table,
>              ovn_lflow_destroy(lflow_table, lflow);
>              continue;
>          }
> +        /* An lflow with no datapaths must be removed from the SB DB.  It
> is
> +         * skipped here (not added to 'sb_uuid_set'), so its SB row, if
> any, is
> +         * deleted by the reconciliation loop below. */
> +        if (lflow_prune_if_no_datapaths(lflow_table, lflow)) {
> +            continue;
> +        }
>          sbflow = NULL;
>          if (!uuid_is_zero(&lflow->sb_uuid)) {
>              sbflow = sbrec_logical_flow_table_get_for_uuid(sb_flow_table,
> @@ -372,7 +400,7 @@ lflow_table_sync_to_sb(struct lflow_table *lflow_table,
>              lflows, &stage,
>              sbflow->priority, sbflow->match, sbflow->actions,
>              sbflow->controller_meter, acl_ct_translation, sbflow->hash);
> -        if (lflow) {
> +        if (lflow && !lflow_prune_if_no_datapaths(lflow_table, lflow)) {
>              const struct ovn_synced_datapaths *datapaths;
>              struct hmap *dp_groups;
>              dp_groups = &lflow_table->dp_groups[dp_type];
> @@ -393,6 +421,9 @@ lflow_table_sync_to_sb(struct lflow_table *lflow_table,
>          if (search_mode != LFLOW_TABLE_SEARCH_FIELDS) {
>              break;
>          }
> +        if (lflow_prune_if_no_datapaths(lflow_table, lflow)) {
> +            continue;
> +        }
>          const struct ovn_synced_datapaths *datapaths;
>          struct hmap *dp_groups;
>          enum ovn_datapath_type dp_type =
> @@ -708,20 +739,32 @@ lflow_ref_unlink_lflows(struct lflow_ref *lflow_ref,
>      }
>  }
>
> -bool
> -lflow_ref_resync_flows(struct lflow_ref *lflow_ref,
> -                       struct lflow_table *lflow_table,
> -                       struct ovsdb_idl_txn *ovnsb_txn,
> -                       const struct ovn_synced_datapaths dps[DP_MAX],
> -                       bool ovn_internal_version_changed,
> -                       const struct sbrec_logical_flow_table
> *sbflow_table,
> -                       const struct sbrec_logical_dp_group_table
> *dpgrp_table)
> +/* Unlinks and destroys all lrns in 'lflow_ref', then destroys any lflow
> + * 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. */
> +void
> +lflow_ref_unlink_and_prune(struct lflow_ref *lflow_ref,
> +                           struct lflow_table *lflow_table)
>  {
>      lflow_ref_unlink_lflows(lflow_ref, lflow_table);
> -    return lflow_ref_sync_lflows__(lflow_ref, lflow_table, ovnsb_txn,
> -                                   dps,
> -                                   ovn_internal_version_changed,
> sbflow_table,
> -                                   dpgrp_table);
> +
> +    struct lflow_ref_node *lrn;
> +    HMAP_FOR_EACH_SAFE (lrn, ref_node, &lflow_ref->lflow_ref_nodes) {
> +        struct ovn_lflow *lflow = lrn->lflow;
> +        lflow_ref_node_destroy(lrn);
> +
> +        if (ovs_list_is_empty(&lflow->referenced_by)) {
> +            enum ovn_datapath_type dp_type =
> +                ovn_stage_to_datapath_type(lflow->stage);
> +            ovs_assert(dp_type < DP_MAX);
> +            ovn_dp_group_release(&lflow_table->dp_groups[dp_type],
> +                                 lflow->dpg);
> +            lflow->dpg = NULL;
> +            ovn_lflow_destroy(lflow_table, lflow);
> +        }
> +    }
>  }
>
>  bool
> @@ -808,10 +851,78 @@ lflow_table_add_lflow__(struct lflow_table
> *lflow_table,
>              }
>              ovs_list_insert(&lflow->referenced_by, &lrn->ref_list_node);
>              hmap_insert(&lflow_ref->lflow_ref_nodes, &lrn->ref_node,
> hash);
> +        } else if (sdp) {
> +            /* Single-datapath (re-)add of an existing node. */
> +            if (!lrn->dpgrp_lflow) {
> +                if (!lrn->linked) {
> +                    /* First add of this (re-)link cycle establishes the
> base
> +                     * datapath this reference tracks for the lflow. */
> +                    lrn->dp_index = sdp->index;
> +                } else if (lrn->dp_index != sdp->index) {
> +                    /* The same 'lflow_ref' references this lflow L(M, A)
> for a
> +                     * second datapath in this cycle (e.g. the single
> lflow_ref
> +                     * shared by all IGMP flows).  A single-datapath
> +                     * lflow_ref_node tracks only one datapath index, so
> +                     * lflow_ref_unlink_lflows() would leave the other
> +                     * datapaths set in the lflow's dp group bitmap.
> Upgrade
> +                     * the node to track a datapath bitmap instead, so
> that
> +                     * unlinking clears every datapath this reference
> +                     * contributed. */
> +                    size_t len = sparse_array_len(&sdp->dps->dps_array);
> +                    lrn->dpgrp_bitmap = bitmap_allocate(len);
> +                    lrn->dpgrp_bitmap_len = len;
> +                    bitmap_set1(lrn->dpgrp_bitmap, lrn->dp_index);
> +                    bitmap_set1(lrn->dpgrp_bitmap, sdp->index);
> +                    lrn->dpgrp_lflow = true;
> +
> +                    /* This reference already accounted for
> 'lrn->dp_index'
> +                     * (the first datapath it added this cycle) in the
> block
> +                     * below.  This add contributes a second datapath,
> which
> +                     * the block below will not see because it only runs
> on
> +                     * the first link of the cycle.  Account for it here
> so
> +                     * that a datapath shared with another reference is
> not
> +                     * released prematurely. */
> +                    if (dynamic_bitmap_is_set(&lflow->dpg_bitmap,
> +                                              sdp->index)) {
> +                        dp_refcnt_use(&lflow->dp_refcnts_map, sdp->index);
> +                    }
> +                }
> +            } else {
> +                /* A node previously upgraded to track a datapath bitmap
> is
> +                 * re-linked one datapath at a time. */
> +                if (!lrn->linked) {
> +                    /* First add of this (re-)link cycle: rebuild the
> tracked
> +                     * bitmap from scratch so it reflects the current
> datapath
> +                     * set and the current datapath array length (which
> may
> +                     * have grown or shrunk since the node was last
> linked). */
> +                    size_t len = sparse_array_len(&sdp->dps->dps_array);
> +                    bitmap_free(lrn->dpgrp_bitmap);
> +                    lrn->dpgrp_bitmap = bitmap_allocate(len);
> +                    lrn->dpgrp_bitmap_len = len;
> +                } else if (!bitmap_is_set(lrn->dpgrp_bitmap, sdp->index))
> {
> +                    /* A second or later datapath this reference
> contributes in
> +                     * the same cycle.  The first datapath was accounted
> for by
> +                     * the block below (which only runs on the first link
> of
> +                     * the cycle); account for this one too so that a
> datapath
> +                     * shared with another reference is not released
> +                     * prematurely. */
> +                    if (dynamic_bitmap_is_set(&lflow->dpg_bitmap,
> +                                              sdp->index)) {
> +                        dp_refcnt_use(&lflow->dp_refcnts_map, sdp->index);
> +                    }
> +                }
> +                bitmap_set1(lrn->dpgrp_bitmap, sdp->index);
> +            }
>          }
>
>          if (!lrn->linked) {
> -            if (lrn->dpgrp_lflow) {
> +            /* Allocate a reference counter only if the datapath(s) added
> by
> +             * this reference are already used by the lflow. */
> +            if (sdp) {
> +                if (dynamic_bitmap_is_set(&lflow->dpg_bitmap,
> sdp->index)) {
> +                    dp_refcnt_use(&lflow->dp_refcnts_map, sdp->index);
> +                }
> +            } else {
>                  ovs_assert(lrn->dpgrp_bitmap_len == dp_bitmap_len);
>                  size_t index;
>                  BITMAP_FOR_EACH_1 (index, dp_bitmap_len, dp_bitmap) {
> @@ -820,11 +931,6 @@ lflow_table_add_lflow__(struct lflow_table
> *lflow_table,
>                          dp_refcnt_use(&lflow->dp_refcnts_map, index);
>                      }
>                  }
> -            } else {
> -                /* Allocate a reference counter only if already used. */
> -                if (dynamic_bitmap_is_set(&lflow->dpg_bitmap,
> lrn->dp_index)) {
> -                    dp_refcnt_use(&lflow->dp_refcnts_map, lrn->dp_index);
> -                }
>              }
>          }
>          lrn->linked = true;
> @@ -1258,17 +1364,12 @@ sync_lflow_to_sb(struct ovn_lflow *lflow,
>                  &lflow->dpg->dpg_uuid);
>
>              if (!lflow->dpg->dp_group) {
> -                /* Ideally this should not happen.  But it can still
> happen
> -                 * due to 2 reasons:
> -                 * 1. There is a bug in the dp_group management.  We
> should
> -                 *    perhaps assert here.
> -                 * 2. A User or CMS may delete the logical_dp_groups in
> SB DB
> -                 *    or clear the SB:Logical_flow.logical_dp_groups
> column
> -                 *    (intentionally or accidentally)
> -                 *
> -                 * Because of (2) it is better to return false instead of
> -                 * assert,so that we recover from th inconsistent SB DB.
> -                 */
> +                /* The SB Logical_DP_Group row referenced by this
> in-memory dp
> +                 * group no longer exists.  This can happen if a user or
> CMS
> +                 * deletes the SB:Logical_DP_Group rows or clears the
> +                 * SB:Logical_Flow.logical_dp_group column (intentionally
> or
> +                 * accidentally).  Release the stale dp group and fall
> through
> +                 * to create a fresh one. */
>                  static struct vlog_rate_limit rl =
> VLOG_RATE_LIMIT_INIT(1, 1);
>                  VLOG_WARN_RL(&rl, "SB Logical flow ["UUID_FMT"]'s "
>                              "logical_dp_group column is not set "
> @@ -1276,10 +1377,12 @@ sync_lflow_to_sb(struct ovn_lflow *lflow,
>                              "referencing the dp group ["UUID_FMT"]",
>                              UUID_ARGS(&sbflow->header_.uuid),
>                              UUID_ARGS(&lflow->dpg->dpg_uuid));
> -                lflow->sync_state = LFLOW_STALE;
> -                return false;
> +                ovn_dp_group_release(dp_groups, lflow->dpg);
> +                lflow->dpg = NULL;
> +                pre_sync_dpg = NULL;
>              }
> -        } else {
> +        }
> +        if (!lflow->dpg) {
>              lflow->dpg = ovn_dp_group_create(
>                                  ovnsb_txn, dp_groups, sbrec_dp_group,
>                                  &lflow->dpg_bitmap,
> @@ -1291,6 +1394,7 @@ sync_lflow_to_sb(struct ovn_lflow *lflow,
>
>      if (pre_sync_dpg != lflow->dpg) {
>          ovn_dp_group_use(lflow->dpg);
> +        ovn_dp_group_release(dp_groups, pre_sync_dpg);
>      }
>
>      lflow->sync_state = LFLOW_SYNCED;
> diff --git a/northd/lflow-mgr.h b/northd/lflow-mgr.h
> index 6253be685..678e97214 100644
> --- a/northd/lflow-mgr.h
> +++ b/northd/lflow-mgr.h
> @@ -58,13 +58,7 @@ 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 *);
> -bool lflow_ref_resync_flows(struct lflow_ref *,
> -                            struct lflow_table *lflow_table,
> -                            struct ovsdb_idl_txn *ovnsb_txn,
> -                            const struct ovn_synced_datapaths dps[DP_MAX],
> -                            bool ovn_internal_version_changed,
> -                            const struct sbrec_logical_flow_table *,
> -                            const struct sbrec_logical_dp_group_table *);
> +void lflow_ref_unlink_and_prune(struct lflow_ref *, struct lflow_table *);
>  bool lflow_ref_sync_lflows(struct lflow_ref *,
>                             struct lflow_table *lflow_table,
>                             struct ovsdb_idl_txn *ovnsb_txn,
> diff --git a/northd/northd.c b/northd/northd.c
> index 99954d276..f37040b57 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -21038,10 +21038,9 @@ void run_update_worker_pool(int n_threads)
>      }
>  }
>
> -/* Updates the Logical_Flow and Multicast_Group tables in the OVN_SB
> database,
> - * constructing their contents based on the OVN_NB database. */
> -void build_lflows(struct ovsdb_idl_txn *ovnsb_txn,
> -                  struct lflow_input *input_data,
> +/* Builds the in-memory logical flow table from the OVN_NB database.
> + * The flows are synced to the SB database by en_dp_group. */
> +void build_lflows(struct lflow_input *input_data,
>                    struct lflow_table *lflows)
>  {
>      struct svc_monitors_map_data svc_mons_data =
> @@ -21085,14 +21084,6 @@ void build_lflows(struct ovsdb_idl_txn *ovnsb_txn,
>      /* Parallel build may result in a suboptimal hash. Resize the
>       * lflow map to a correct size before doing lookups */
>      lflow_table_expand(lflows);
> -
> -    stopwatch_start(LFLOWS_TO_SB_STOPWATCH_NAME, time_msec());
> -    lflow_table_sync_to_sb(lflows, ovnsb_txn, input_data->dps,
> -                           input_data->ovn_internal_version_changed,
> -                           input_data->sbrec_logical_flow_table,
> -                           input_data->sbrec_logical_dp_group_table);
> -
> -    stopwatch_stop(LFLOWS_TO_SB_STOPWATCH_NAME, time_msec());
>  }
>
>  void
> @@ -21137,24 +21128,17 @@ lflow_reset_northd_refs(struct lflow_input
> *lflow_input)
>      }
>  }
>
> -bool
> -lflow_handle_northd_lr_changes(struct ovsdb_idl_txn *ovnsb_txn,
> -                                struct tracked_dps *tracked_lrs,
> -                                struct lflow_input *lflow_input,
> -                                struct lflow_table *lflows)
> +void
> +lflow_handle_northd_lr_changes(struct tracked_dps *tracked_lrs,
> +                               struct lflow_input *lflow_input,
> +                               struct lflow_table *lflows,
> +                               struct hmapx *dirty_lflow_refs)
>  {
> -    bool handled = true;
>      struct hmapx_node *hmapx_node;
>      HMAPX_FOR_EACH (hmapx_node, &tracked_lrs->deleted) {
>          struct ovn_datapath *od = hmapx_node->data;
> -        handled = lflow_ref_resync_flows(
> -            od->datapath_lflows, lflows, ovnsb_txn, lflow_input->dps,
> -            lflow_input->ovn_internal_version_changed,
> -            lflow_input->sbrec_logical_flow_table,
> -            lflow_input->sbrec_logical_dp_group_table);
> -        if (!handled) {
> -            return handled;
> -        }
> +        lflow_ref_unlink_lflows(od->datapath_lflows, lflows);
> +        hmapx_add(dirty_lflow_refs, od->datapath_lflows);
>      }
>
>      struct lswitch_flow_build_info lsi = {
> @@ -21172,34 +21156,18 @@ lflow_handle_northd_lr_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>
>          lflow_ref_unlink_lflows(od->datapath_lflows, lflows);
>          build_lswitch_and_lrouter_iterate_by_lr(od, &lsi);
> -    }
> -
> -    /* We need to make sure that all datapath groups are allocated before
> -     * trying to sync logical flows. Otherwise, we would need to recompute
> -     * those datapath groups within those flows over and over again. */
> -    HMAPX_FOR_EACH (hmapx_node, &tracked_lrs->crupdated) {
> -        struct ovn_datapath *od = hmapx_node->data;
> -
> -        handled = lflow_ref_sync_lflows(
> -            od->datapath_lflows, lflows, ovnsb_txn, lflow_input->dps,
> -            lflow_input->ovn_internal_version_changed,
> -            lflow_input->sbrec_logical_flow_table,
> -            lflow_input->sbrec_logical_dp_group_table);
> -        if (!handled) {
> -            break;
> -        }
> +        hmapx_add(dirty_lflow_refs, od->datapath_lflows);
>      }
>
>      ds_destroy(&lsi.actions);
>      ds_destroy(&lsi.match);
> -    return handled;
>  }
>
> -bool
> -lflow_handle_northd_port_changes(struct ovsdb_idl_txn *ovnsb_txn,
> -                                 struct tracked_ovn_ports *trk_lsps,
> +void
> +lflow_handle_northd_port_changes(struct tracked_ovn_ports *trk_lsps,
>                                   struct lflow_input *lflow_input,
> -                                 struct lflow_table *lflows)
> +                                 struct lflow_table *lflows,
> +                                 struct hmapx *dirty_lflow_refs)
>  {
>      struct hmapx_node *hmapx_node;
>      struct ovn_port *op;
> @@ -21208,14 +21176,8 @@ lflow_handle_northd_port_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>          op = hmapx_node->data;
>          /* Make sure 'op' is an lsp and not lrp. */
>          ovs_assert(op->nbsp);
> -        bool handled = lflow_ref_resync_flows(
> -            op->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
> -            lflow_input->ovn_internal_version_changed,
> -            lflow_input->sbrec_logical_flow_table,
> -            lflow_input->sbrec_logical_dp_group_table);
> -        if (!handled) {
> -            return false;
> -        }
> +        lflow_ref_unlink_lflows(op->lflow_ref, lflows);
> +        hmapx_add(dirty_lflow_refs, op->lflow_ref);
>          /* No need to update SB multicast groups, thanks to weak
>           * references. */
>      }
> @@ -21236,33 +21198,17 @@ lflow_handle_northd_port_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>                                                   &match, &actions,
>
> lflow_input->svc_monitor_mac,
>                                                   lflows);
> -        /* Sync the new flows to SB. */
> -        bool handled = lflow_ref_sync_lflows(
> -            op->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
> -            lflow_input->ovn_internal_version_changed,
> -            lflow_input->sbrec_logical_flow_table,
> -            lflow_input->sbrec_logical_dp_group_table);
> -        if (handled) {
> -            /* Now regenerate the stateful lflows for 'op' */
> -            /* Clear old lflows. */
> -            lflow_ref_unlink_lflows(op->stateful_lflow_ref, lflows);
> -            build_lbnat_lflows_iterate_by_lsp(op,
> -
> lflow_input->lr_stateful_table,
> -                                              &match, &actions, lflows);
> -            handled = lflow_ref_sync_lflows(
> -                op->stateful_lflow_ref, lflows, ovnsb_txn,
> -                lflow_input->dps,
> -                lflow_input->ovn_internal_version_changed,
> -                lflow_input->sbrec_logical_flow_table,
> -                lflow_input->sbrec_logical_dp_group_table);
> -        }
> +        hmapx_add(dirty_lflow_refs, op->lflow_ref);
> +
> +        /* Now regenerate the stateful lflows for 'op' */
> +        lflow_ref_unlink_lflows(op->stateful_lflow_ref, lflows);
> +        build_lbnat_lflows_iterate_by_lsp(op,
> +                                          lflow_input->lr_stateful_table,
> +                                          &match, &actions, lflows);
> +        hmapx_add(dirty_lflow_refs, op->stateful_lflow_ref);
>
>          ds_destroy(&match);
>          ds_destroy(&actions);
> -
> -        if (!handled) {
> -            return false;
> -        }
>      }
>
>      HMAPX_FOR_EACH (hmapx_node, &trk_lsps->created) {
> @@ -21278,42 +21224,24 @@ lflow_handle_northd_port_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>                                                   &match, &actions,
>
> lflow_input->svc_monitor_mac,
>                                                   lflows);
> +        hmapx_add(dirty_lflow_refs, op->lflow_ref);
>
> -        /* Sync the newly added flows to SB. */
> -        bool handled = lflow_ref_sync_lflows(
> -            op->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
> -            lflow_input->ovn_internal_version_changed,
> -            lflow_input->sbrec_logical_flow_table,
> -            lflow_input->sbrec_logical_dp_group_table);
> -        if (handled) {
> -            /* Now generate the stateful lflows for 'op' */
> -            build_lbnat_lflows_iterate_by_lsp(op,
> -
> lflow_input->lr_stateful_table,
> -                                              &match, &actions, lflows);
> -            handled = lflow_ref_sync_lflows(
> -                op->stateful_lflow_ref, lflows, ovnsb_txn,
> -                lflow_input->dps,
> -                lflow_input->ovn_internal_version_changed,
> -                lflow_input->sbrec_logical_flow_table,
> -                lflow_input->sbrec_logical_dp_group_table);
> -        }
> +        /* Now generate the stateful lflows for 'op' */
> +        build_lbnat_lflows_iterate_by_lsp(op,
> +                                          lflow_input->lr_stateful_table,
> +                                          &match, &actions, lflows);
> +        hmapx_add(dirty_lflow_refs, op->stateful_lflow_ref);
>
>          ds_destroy(&match);
>          ds_destroy(&actions);
> -
> -        if (!handled) {
> -            return false;
> -        }
>      }
> -
> -    return true;
>  }
>
> -bool
> -lflow_handle_northd_lb_changes(struct ovsdb_idl_txn *ovnsb_txn,
> -                               struct tracked_lbs *trk_lbs,
> +void
> +lflow_handle_northd_lb_changes(struct tracked_lbs *trk_lbs,
>                                 struct lflow_input *lflow_input,
> -                               struct lflow_table *lflows)
> +                               struct lflow_table *lflows,
> +                               struct hmapx *dirty_lflow_refs)
>  {
>      struct ovn_lb_datapaths *lb_dps;
>      struct hmapx_node *hmapx_node;
> @@ -21325,12 +21253,8 @@ lflow_handle_northd_lb_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>
>      HMAPX_FOR_EACH (hmapx_node, &trk_lbs->deleted) {
>          lb_dps = hmapx_node->data;
> -
> -        lflow_ref_resync_flows(
> -            lb_dps->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
> -            lflow_input->ovn_internal_version_changed,
> -            lflow_input->sbrec_logical_flow_table,
> -            lflow_input->sbrec_logical_dp_group_table);
> +        lflow_ref_unlink_lflows(lb_dps->lflow_ref, lflows);
> +        hmapx_add(dirty_lflow_refs, lb_dps->lflow_ref);
>      }
>
>      HMAPX_FOR_EACH (hmapx_node, &trk_lbs->crupdated) {
> @@ -21365,31 +21289,20 @@ lflow_handle_northd_lb_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>          ds_destroy(&match);
>          ds_destroy(&actions);
>
> -        /* Sync the new flows to SB. */
> -        bool handled = lflow_ref_sync_lflows(
> -            lb_dps->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
> -            lflow_input->ovn_internal_version_changed,
> -            lflow_input->sbrec_logical_flow_table,
> -            lflow_input->sbrec_logical_dp_group_table);
> -        if (!handled) {
> -            return false;
> -        }
> +        hmapx_add(dirty_lflow_refs, lb_dps->lflow_ref);
>      }
> -
> -    return true;
>  }
>
> -bool
> -lflow_handle_lr_stateful_changes(struct ovsdb_idl_txn *ovnsb_txn,
> -                                struct lr_stateful_tracked_data *trk_data,
> -                                struct lflow_input *lflow_input,
> -                                struct lflow_table *lflows)
> +void
> +lflow_handle_lr_stateful_changes(struct lr_stateful_tracked_data
> *trk_data,
> +                                 struct lflow_input *lflow_input,
> +                                 struct lflow_table *lflows,
> +                                 struct hmapx *dirty_lflow_refs)
>  {
>      struct lr_stateful_record *lr_stateful_rec;
>      struct ds actions = DS_EMPTY_INITIALIZER;
>      struct ds match = DS_EMPTY_INITIALIZER;
>      struct hmapx_node *hmapx_node;
> -    bool handled = true;
>
>      HMAPX_FOR_EACH (hmapx_node, &trk_data->crupdated) {
>          lr_stateful_rec = hmapx_node->data;
> @@ -21402,17 +21315,7 @@ lflow_handle_lr_stateful_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>                                  &match, &actions,
>                                  lflow_input->meter_groups,
>                                  lflow_input->features);
> -
> -        /* Sync the new flows to SB. */
> -        handled = lflow_ref_sync_lflows(
> -            lr_stateful_rec->lflow_ref, lflows, ovnsb_txn,
> -            lflow_input->dps,
> -            lflow_input->ovn_internal_version_changed,
> -            lflow_input->sbrec_logical_flow_table,
> -            lflow_input->sbrec_logical_dp_group_table);
> -        if (!handled) {
> -            goto exit;
> -        }
> +        hmapx_add(dirty_lflow_refs, lr_stateful_rec->lflow_ref);
>
>          const struct ovn_datapath *od =
>              ovn_datapaths_find_by_index(lflow_input->lr_datapaths,
> @@ -21427,16 +21330,7 @@ lflow_handle_lr_stateful_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>                                                lflow_input->bfd_ports,
>                                                &match, &actions,
>                                                lflows);
> -
> -            handled = lflow_ref_sync_lflows(
> -                op->stateful_lflow_ref, lflows, ovnsb_txn,
> -                lflow_input->dps,
> -                lflow_input->ovn_internal_version_changed,
> -                lflow_input->sbrec_logical_flow_table,
> -                lflow_input->sbrec_logical_dp_group_table);
> -            if (!handled) {
> -                goto exit;
> -            }
> +            hmapx_add(dirty_lflow_refs, op->stateful_lflow_ref);
>
>              if (op->peer && op->peer->nbsp) {
>                  lflow_ref_unlink_lflows(op->peer->stateful_lflow_ref,
> lflows);
> @@ -21444,32 +21338,20 @@ lflow_handle_lr_stateful_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>                  build_lbnat_lflows_iterate_by_lsp(
>                      op->peer, lflow_input->lr_stateful_table, &match,
> &actions,
>                      lflows);
> -
> -                handled = lflow_ref_sync_lflows(
> -                    op->peer->stateful_lflow_ref, lflows, ovnsb_txn,
> -                    lflow_input->dps,
> -                    lflow_input->ovn_internal_version_changed,
> -                    lflow_input->sbrec_logical_flow_table,
> -                    lflow_input->sbrec_logical_dp_group_table);
> -                if (!handled) {
> -                    goto exit;
> -                }
> +                hmapx_add(dirty_lflow_refs, op->peer->stateful_lflow_ref);
>              }
>          }
>      }
>
> -exit:
>      ds_destroy(&match);
>      ds_destroy(&actions);
> -
> -    return handled;
>  }
>
> -bool
> -lflow_handle_ls_stateful_changes(struct ovsdb_idl_txn *ovnsb_txn,
> -                                struct ls_stateful_tracked_data *trk_data,
> -                                struct lflow_input *lflow_input,
> -                                struct lflow_table *lflows)
> +void
> +lflow_handle_ls_stateful_changes(struct ls_stateful_tracked_data
> *trk_data,
> +                                 struct lflow_input *lflow_input,
> +                                 struct lflow_table *lflows,
> +                                 struct hmapx *dirty_lflow_refs)
>  {
>      struct hmapx_node *hmapx_node;
>
> @@ -21494,39 +21376,14 @@ lflow_handle_ls_stateful_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>          build_network_function(od, lflows,
>                                 lflow_input->ls_port_groups,
>                                 ls_stateful_rec->lflow_ref);
> -    }
> -
> -    /* We need to make sure that all datapath groups are allocated before
> -     * trying to sync logical flows. Otherwise, we would need to recompute
> -     * those datapath groups within those flows over and over again. */
> -    HMAPX_FOR_EACH (hmapx_node, &trk_data->crupdated) {
> -        struct ls_stateful_record *ls_stateful_rec = hmapx_node->data;
> -        /* Sync the new flows to SB. */
> -        bool handled = lflow_ref_sync_lflows(
> -            ls_stateful_rec->lflow_ref, lflows, ovnsb_txn,
> -            lflow_input->dps,
> -            lflow_input->ovn_internal_version_changed,
> -            lflow_input->sbrec_logical_flow_table,
> -            lflow_input->sbrec_logical_dp_group_table);
> -        if (!handled) {
> -            return false;
> -        }
> +        hmapx_add(dirty_lflow_refs, ls_stateful_rec->lflow_ref);
>      }
>
>      HMAPX_FOR_EACH (hmapx_node, &trk_data->deleted) {
>          struct ls_stateful_record *ls_stateful_rec = hmapx_node->data;
> -
> -        if (!lflow_ref_resync_flows(
> -                    ls_stateful_rec->lflow_ref, lflows, ovnsb_txn,
> -                    lflow_input->dps,
> -                    lflow_input->ovn_internal_version_changed,
> -                    lflow_input->sbrec_logical_flow_table,
> -                    lflow_input->sbrec_logical_dp_group_table)) {
> -            return false;
> -        }
> +        lflow_ref_unlink_lflows(ls_stateful_rec->lflow_ref, lflows);
> +        hmapx_add(dirty_lflow_refs, ls_stateful_rec->lflow_ref);
>      }
> -
> -    return true;
>  }
>
>  static bool
> diff --git a/northd/northd.h b/northd/northd.h
> index 9a74a4abc..4150157b0 100644
> --- a/northd/northd.h
> +++ b/northd/northd.h
> @@ -260,9 +260,7 @@ struct lflow_ref;
>  struct lr_nat_table;
>
>  struct lflow_input {
> -    /* Southbound table references */
> -    const struct sbrec_logical_flow_table *sbrec_logical_flow_table;
> -    const struct sbrec_logical_dp_group_table
> *sbrec_logical_dp_group_table;
> +    /* Southbound table references (used during lflow computation). */
>      const struct sbrec_acl_id_table *sbrec_acl_id_table;
>
>      /* Indexes */
> @@ -979,8 +977,7 @@ struct lr_stateful_tracked_data;
>  struct ls_stateful_tracked_data;
>  struct group_ecmp_datapath;
>
> -void build_lflows(struct ovsdb_idl_txn *ovnsb_txn,
> -                  struct lflow_input *input_data,
> +void build_lflows(struct lflow_input *input_data,
>                    struct lflow_table *);
>  void lflow_reset_northd_refs(struct lflow_input *);
>  void build_route_data_flows_for_lrouter(
> @@ -988,26 +985,26 @@ void build_route_data_flows_for_lrouter(
>      const struct group_ecmp_datapath *route_node,
>      const struct sset *bfd_ports);
>
> -bool lflow_handle_northd_lr_changes(struct ovsdb_idl_txn *ovnsh_txn,
> -                                     struct tracked_dps *,
> -                                     struct lflow_input *,
> -                                     struct lflow_table *lflows);
> -bool lflow_handle_northd_port_changes(struct ovsdb_idl_txn *ovnsb_txn,
> -                                      struct tracked_ovn_ports *,
> +void lflow_handle_northd_lr_changes(struct tracked_dps *,
> +                                    struct lflow_input *,
> +                                    struct lflow_table *lflows,
> +                                    struct hmapx *dirty_lflow_refs);
> +void lflow_handle_northd_port_changes(struct tracked_ovn_ports *,
>                                        struct lflow_input *,
> -                                      struct lflow_table *lflows);
> -bool lflow_handle_northd_lb_changes(struct ovsdb_idl_txn *ovnsb_txn,
> -                                    struct tracked_lbs *,
> +                                      struct lflow_table *lflows,
> +                                      struct hmapx *dirty_lflow_refs);
> +void lflow_handle_northd_lb_changes(struct tracked_lbs *,
>                                      struct lflow_input *,
> -                                    struct lflow_table *lflows);
> -bool lflow_handle_lr_stateful_changes(struct ovsdb_idl_txn *,
> -                                      struct lr_stateful_tracked_data *,
> +                                    struct lflow_table *lflows,
> +                                    struct hmapx *dirty_lflow_refs);
> +void lflow_handle_lr_stateful_changes(struct lr_stateful_tracked_data *,
>                                        struct lflow_input *,
> -                                      struct lflow_table *lflows);
> -bool lflow_handle_ls_stateful_changes(struct ovsdb_idl_txn *,
> -                                      struct ls_stateful_tracked_data *,
> +                                      struct lflow_table *lflows,
> +                                      struct hmapx *dirty_lflow_refs);
> +void lflow_handle_ls_stateful_changes(struct ls_stateful_tracked_data *,
>                                        struct lflow_input *,
> -                                      struct lflow_table *lflows);
> +                                      struct lflow_table *lflows,
> +                                      struct hmapx *dirty_lflow_refs);
>  bool northd_handle_sb_port_binding_changes(
>      const struct sbrec_port_binding_table *, struct hmap *ls_ports,
>      struct hmap *lr_ports);
> diff --git a/tests/ovn-inc-proc-graph-dump.at b/tests/
> ovn-inc-proc-graph-dump.at
> index b0c29d1b5..8348c3467 100644
> --- a/tests/ovn-inc-proc-graph-dump.at
> +++ b/tests/ovn-inc-proc-graph-dump.at
> @@ -143,7 +143,6 @@ digraph "Incremental-Processing-Engine" {
>         sync_meters -> sync_to_sb [[label=""]];
>         sync_to_sb_lb -> sync_to_sb [[label=""]];
>         sync_to_sb_pb -> sync_to_sb [[label=""]];
> -       SB_logical_flow [[style=filled, shape=box, fillcolor=white,
> label="SB_logical_flow"]];
>         SB_multicast_group [[style=filled, shape=box, fillcolor=white,
> label="SB_multicast_group"]];
>         NB_bfd [[style=filled, shape=box, fillcolor=white,
> label="NB_bfd"]];
>         SB_bfd [[style=filled, shape=box, fillcolor=white,
> label="SB_bfd"]];
> @@ -186,9 +185,7 @@ digraph "Incremental-Processing-Engine" {
>         SB_igmp_group -> multicast_igmp [[label=""]];
>         lflow [[style=filled, shape=box, fillcolor=white, label="lflow"]];
>         sync_meters -> lflow [[label=""]];
> -       SB_logical_flow -> lflow [[label=""]];
>         SB_multicast_group -> lflow [[label=""]];
> -       SB_logical_dp_group -> lflow [[label=""]];
>         bfd_sync -> lflow [[label=""]];
>         route_policies -> lflow [[label=""]];
>         routes -> lflow [[label="engine_noop_handler"]];
> @@ -203,6 +200,13 @@ digraph "Incremental-Processing-Engine" {
>         multicast_igmp -> lflow [[label="lflow_multicast_igmp_handler"]];
>         SB_acl_id -> lflow [[label=""]];
>         ic_learned_svc_monitors -> lflow
> [[label="lflow_ic_learned_svc_mons_handler"]];
> +       SB_logical_flow [[style=filled, shape=box, fillcolor=white,
> label="SB_logical_flow"]];
> +       dp_group [[style=filled, shape=box, fillcolor=white,
> label="dp_group"]];
> +       lflow -> dp_group [[label="dp_group_lflow_handler"]];
> +       SB_logical_flow -> dp_group [[label=""]];
> +       SB_logical_dp_group -> dp_group [[label=""]];
> +       datapath_sync -> dp_group [[label="engine_noop_handler"]];
> +       global_config -> dp_group [[label="engine_noop_handler"]];
>         mac_binding_aging_waker [[style=filled, shape=box,
> fillcolor=white, label="mac_binding_aging_waker"]];
>         mac_binding_aging [[style=filled, shape=box, fillcolor=white,
> label="mac_binding_aging"]];
>         SB_mac_binding -> mac_binding_aging [[label=""]];
> @@ -237,7 +241,7 @@ digraph "Incremental-Processing-Engine" {
>         acl_id -> northd_output [[label="northd_output_acl_id_handler"]];
>         sync_from_sb -> northd_output [[label=""]];
>         sync_to_sb -> northd_output
> [[label="northd_output_sync_to_sb_handler"]];
> -       lflow -> northd_output [[label="northd_output_lflow_handler"]];
> +       dp_group -> northd_output
> [[label="northd_output_dp_group_handler"]];
>         mac_binding_aging -> northd_output
> [[label="northd_output_mac_binding_aging_handler"]];
>         fdb_aging -> northd_output
> [[label="northd_output_fdb_aging_handler"]];
>         ecmp_nexthop -> northd_output
> [[label="northd_output_ecmp_nexthop_handler"]];
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index b8512a462..f8c144918 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -13736,17 +13736,7 @@ check ovn-nbctl --wait=sb set load_balancer lb1
> options:bar=foo
>  check_engine_stats lflow norecompute compute
>  CHECK_NO_CHANGE_AFTER_RECOMPUTE
>
> -# Clear the SB:Logical_Flow.logical_dp_groups column of all the
> -# logical flows and then modify the NB:Load_balancer.  ovn-northd
> -# should resync the logical flows.
> -for l in $(ovn-sbctl --bare --columns _uuid list logical_flow)
> -do
> -    check ovn-sbctl clear logical_flow $l logical_dp_group
> -done
> -
> -check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
>  check ovn-nbctl --wait=sb set load_balancer lb1 options:foo=bar
> -check_engine_stats lflow recompute nocompute
>  CHECK_NO_CHANGE_AFTER_RECOMPUTE
>
>  lb_lflow_uuid=$(fetch_column Logical_flow _uuid match='"ct.new &&
> ip4.dst == 10.0.0.10 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80"')
> @@ -13915,16 +13905,7 @@ AT_CHECK([echo $dpgrp_dps | grep $sw3_uuid], [0],
> [ignore])
>  AT_CHECK([echo $dpgrp_dps | grep $sw4_uuid], [0], [ignore])
>  AT_CHECK([echo $dpgrp_dps | grep $sw5_uuid], [0], [ignore])
>
> -# Clear the SB:Logical_Flow.logical_dp_groups column of all the
> -# logical flows and then modify the NB:Load_balancer.  ovn-northd
> -# should resync the logical flows.
> -for l in $(ovn-sbctl --bare --columns _uuid list logical_flow)
> -do
> -    check ovn-sbctl clear logical_flow $l logical_dp_group
> -done
> -
>  check ovn-nbctl --wait=sb set load_balancer lb2 vips='{"10.0.0.10:80"="
> 10.0.0.3:80,10.0.0.4:80"}'
> -check_engine_stats lflow recompute compute
>  CHECK_NO_CHANGE_AFTER_RECOMPUTE
>
>  lb_lflow_uuid=$(fetch_column Logical_flow _uuid match='"ct.new &&
> ip4.dst == 10.0.0.10 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80"')
> @@ -24576,3 +24557,65 @@ OVN_CLEANUP_NORTHD
>  AT_CLEANUP
>  ])
>
> +AT_SETUP([Datapath group SB writes only when bitmap changes])
> +ovn_start
> +
> +# Create three switches, each with an ACL so they share the
> +# "has ACLs" lflows in the same datapath group {sw0, sw1, sw2}.
> +check ovn-nbctl ls-add sw0
> +check ovn-nbctl ls-add sw1
> +check ovn-nbctl ls-add sw2
> +check ovn-nbctl lsp-add sw0 sw0-p0
> +check ovn-nbctl lsp-add sw1 sw1-p0
> +check ovn-nbctl lsp-add sw2 sw2-p0
> +check ovn-nbctl acl-add sw0 to-lport 1000 ip4 allow
> +check ovn-nbctl acl-add sw1 to-lport 1000 ip6 allow
> +check ovn-nbctl --wait=sb acl-add sw2 to-lport 1000 tcp allow
> +
> +CHECK_NO_CHANGE_AFTER_RECOMPUTE
> +
> +# Record dp_group state (UUIDs + content).
> +ovn-sbctl --bare --columns=_uuid,datapaths list Logical_DP_Group \
> +    | sort > dpg_before
> +
> +# ----------------------------------------------------------
> +# Case 1: Change that triggers incremental lflow compute but
> +#         does NOT alter the dp-group bitmap.
> +#
> +# Setting a port address rebuilds the port's lflows but the
> +# set of datapaths that share the "has ACLs" lflows is still
> +# {sw0, sw1, sw2}.  The dp_group rows should not change.
> +# ----------------------------------------------------------
> +check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
> +check ovn-nbctl --wait=sb lsp-set-addresses sw0-p0 "00:00:00:00:00:01"
> +check_engine_stats lflow norecompute compute
> +
> +ovn-sbctl --bare --columns=_uuid,datapaths list Logical_DP_Group \
> +    | sort > dpg_after
> +AT_CHECK([diff dpg_before dpg_after])
> +
> +# ----------------------------------------------------------
> +# Case 2: Change that removes a datapath from a shared flow,
> +#         altering the dp-group bitmap.
> +#
> +# Move sw0's ACL to tier 1.  The egress "acl action" flows
> +# now differ for sw0, so the {sw0, sw1, sw2} group loses sw0
> +# and a {sw1, sw2} group appears (or the existing one is
> +# updated).  The dp_group rows must change.
> +# ----------------------------------------------------------
> +check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
> +acl0=$(fetch_column nb:ACL _uuid match=ip4)
> +check ovn-nbctl --wait=sb set ACL $acl0 tier=1
> +check_engine_stats lflow norecompute compute
> +
> +ovn-sbctl --bare --columns=_uuid,datapaths list Logical_DP_Group \
> +    | sort > dpg_changed
> +AT_CHECK([diff dpg_before dpg_changed], [1], [ignore])
> +
> +check ovn-nbctl --wait=sb ls-del sw0
> +
> +CHECK_NO_CHANGE_AFTER_RECOMPUTE
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +
> --
> 2.55.0
>
>
Thank you Jacob,
applied to main.

Regards,
Ales
Ilya Maximets Oct. 5, 2026, 11:33 a.m. UTC | #2
On 10/2/26 1:55 PM, Ales Musil via dev wrote:
> On Thu, Oct 1, 2026 at 10:01 PM Jacob Tanenbaum <jtanenba@redhat.com> wrote:
> 
>> Split ovn-northd's en_lflow engine node into a compute-only
>> en_lflow node and a new en_dp_group node that
>> handles datapath-group resolution and all SB database
>> writes.  This separation makes en_lflow independent of the
>> SB database, preparing for future incremental processing
>> improvements.
>>
>> en_lflow's handlers now track dirty lflow_refs in a hmapx
>> instead of calling lflow_ref_sync_lflows directly.
>> en_dp_group drains the dirty set on its lflow
>> handler, falling back to a full lflow_table_sync_to_sb when
>> en_lflow did a full recompute or when the IGMP/MLD handler
>> set the needs_full_sync flag.
>>
>> A new lflow_ref_unlink_and_prune() function replaces
>> lflow_ref_resync_flows() for the IGMP and IC-learned
>> service monitor handlers.  These handlers use a single
>> shared lflow_ref whose flows are also built by the
>> per-datapath pipeline (with lflow_ref = NULL).  The
>> per-datapath build's dp bits are not tracked by any
>> lflow_ref, so lflow_ref_unlink_lflows alone cannot clear
>> them via dp_refcnt.  lflow_ref_unlink_and_prune destroys
>> all lrns and orphaned lflows in-memory without SB writes,
>> equivalent to lflow_ref_resync_flows' cleanup.
>>
>> The lflow_table_add_lflow__ upgrade-to-bitmap logic (for
>> single lflow_refs that contribute multiple datapaths to the
>> same lflow) is extended with per-datapath dp_refcnt
>> accounting during mid-cycle bitmap upgrades, preventing
>> premature dp bit release when multiple lflow_refs share a
>> flow across overlapping datapaths.
>>
>> Two test cases need to have sections removed. These tests relied upon
>> encountering a stale dp_group would cause all the lflows to be
>> recalculated. After this patch that is no longer the case and only the
>> lflows directly related to the transaction get modified. Encountering a
>> stale dp_group should no longer rebuild all the lflows.
>>
>> Reported-at: https://redhat.atlassian.net/browse/FDP-2747
>> Assisted-by: Claude Opus 4.8, Claude Code
>> Signed-off-by: Jacob Tanenbaum <jtanenba@redhat.com>
>>
>> ---
Hi, Jacob and Ales.

It seems like we see a significant performance regression with this
change in ovn-heater 500-node cluster-density scenario.  The average
iteration time went up from 4.0 to 5.7 seconds, which is 42% increase.

I didn't dig too deep into the change myself, but I asked an LLM to
do some analysis and the output seems plausible.  Could you please
take a look and confirm or dismiss?

commit b9809453f5466390f4a71e85c3fdd19a53ee1ea1
("northd: Split en_lflow into compute and sync nodes.")
Author: Jacob Tanenbaum <jtanenba@redhat.com>

This patch separates logical-flow computation from synchronization and
defers dirty-reference synchronization to the datapath-group engine node.

>     lflow_data->trk_data.needs_full_sync = true;
> [...]
>     if (hmapx_is_empty(&lflow_data->trk_data.dirty_lflow_refs) ||
>         lflow_data->trk_data.needs_full_sync) {
>
>         dp_group_sync_to_sb(node, lflow_data);
>         return EN_HANDLED_UPDATED;
>     }

Could setting needs_full_sync in lflow_multicast_igmp_handler() cause
ordinary logical switch port changes to synchronize the entire logical
flow table?

multicast_igmp_northd_handler() returns EN_UNHANDLED whenever a logical
switch port is created, updated, or deleted. The resulting multicast
recompute returns EN_UPDATED, so lflow_multicast_igmp_handler() sets
needs_full_sync even when lflow otherwise handles the port changes
incrementally. This path does not require learned IGMP groups.

The multicast_igmp node also manages non-IGMP multicast groups, including
ordinary flood, unknown-destination, and static groups. Port changes
therefore trigger this path even when IGMP/MLD snooping is disabled and
there are no learned membership groups. build_igmp_lflows() also rebuilds
multicast flood flows for all logical switches.

Before this patch, the multicast handler synchronized its lflow_ref and
the port handlers synchronized the affected port references. With this
change, dp_group_lflow_handler() instead calls lflow_table_sync_to_sb(),
visiting every in-memory flow and every SB Logical_Flow row, including
flows unrelated to the port changes.

In ovn-heater's 500-node cluster-density tests, we see:

> node: dp_group, handler for input lflow took 797ms

That message can include full-table synchronization inside the handler,
rather than only synchronization of dirty references. Although the log
alone does not identify which branch ran, the port-change path above
appears to introduce cluster-wide synchronization for each incremental
port-change batch.

Could the multicast handler retain targeted synchronization, tracking
deleted flows for deferred SB deletion instead of setting needs_full_sync?
Simply adding the rebuilt multicast reference to the dirty set would not
preserve deletions, since lflow_ref_unlink_and_prune() destroys the old
in-memory flows.

Could full rebuilds also be marked explicitly, so an empty dirty-reference
set does not by itself require full-table synchronization?

-----

While checking the performance, I also ran some normal LLM review and
the following findings may also be worth looking at.  The second one
may not be an actual issue, as we do not expect SB data to be manipulated
by external writers, but the fact that there might be a reachable release
of a borrowed reference doesn't sound great.  So, if you can take a look
and confirm or dismiss, that would also be great.

commit b9809453f5466390f4a71e85c3fdd19a53ee1ea1
("northd: Split en_lflow into compute and sync nodes.")
Author: Jacob Tanenbaum <jtanenba@redhat.com>

This patch separates logical-flow computation from synchronization and
defers dirty-reference synchronization to the datapath-group engine node.

>     if (hmapx_is_empty(&lflow_data->trk_data.dirty_lflow_refs) ||
>         lflow_data->trk_data.needs_full_sync) {
>
>         dp_group_sync_to_sb(node, lflow_data);
>         return EN_HANDLED_UPDATED;
>     }
> [...]
>         if (!lflow_ref_sync_lflows(ref, lflow_data->lflow_table,

Can dp_group_lflow_handler() in northd/en-dp-group.c:92-108 retire
unlinked reference nodes even when needs_full_sync is set? Full table
synchronization retains those nodes on shared flows that still have
datapaths, whereas lflow_ref_sync_lflows__() in
northd/lflow-mgr.c:1529-1538 removes them.

With two switch load balancers sharing a VIP and switch membership,
removing that VIP from one balancer while updating an unrelated existing
port's addresses can unlink its reference and select this full-sync path.
If that balancer retains another VIP, a later options change combined with
another port update calls lflow_ref_unlink_lflows() in
northd/lflow-mgr.c:696-715 on the retained, already-unlinked node.

Wouldn't this release the other balancer's remaining datapath counts and
let full synchronization delete its still-required VIP flow? Could the
full-sync path perform the unlinked-node cleanup before clearing the dirty
references?

---

>         lflow->dpg = ovn_dp_group_get(dp_groups, &lflow->dpg_bitmap,
>                                       n_datapaths);
> [...]
>             if (!lflow->dpg->dp_group) {
> [...]
>                 ovn_dp_group_release(dp_groups, lflow->dpg);
>                 lflow->dpg = NULL;
>                 pre_sync_dpg = NULL;

Could sync_lflow_to_sb() in northd/lflow-mgr.c:1358-1382 release only an
owned reference here? ovn_dp_group_get() returns a borrowed cache entry,
and an incrementally rebuilt flow can have pre_sync_dpg == NULL while
unchanged flows still own that entry.

If an authorized Southbound writer clears all logical_dp_group references
to a group, allowing its row to be garbage-collected, rebuilding only some
flows with that bitmap can reach this branch before the remaining owners.
The release then consumes another flow's reference and leaves the cached
group's reference count too low.

When the remaining owners are subsequently updated or synchronized, can
ovn_dp_group_release() in northd/lflow-mgr.c:1429-1444 free the group while
one flow still points to it, then write to freed memory on that flow's
release? Setting pre_sync_dpg to NULL does not restore the reference
consumed by releasing the borrowed lookup.

----

Best regards, Ilya Maximets.
diff mbox series

Patch

diff --git a/northd/automake.mk b/northd/automake.mk
index 45ca0337f..1d3044930 100644
--- a/northd/automake.mk
+++ b/northd/automake.mk
@@ -16,6 +16,8 @@  northd_ovn_northd_SOURCES = \
 	northd/en-datapath-logical-router.h \
 	northd/en-datapath-sync.c \
 	northd/en-datapath-sync.h \
+	northd/en-dp-group.c \
+	northd/en-dp-group.h \
 	northd/en-ecmp-nexthop.c \
 	northd/en-ecmp-nexthop.h \
 	northd/en-global-config.c \
diff --git a/northd/en-dp-group.c b/northd/en-dp-group.c
new file mode 100644
index 000000000..e33e1f407
--- /dev/null
+++ b/northd/en-dp-group.c
@@ -0,0 +1,116 @@ 
+/*
+ * Copyright (c) 2026, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "en-datapath-sync.h"
+#include "en-dp-group.h"
+#include "en-global-config.h"
+#include "en-lflow.h"
+#include "lflow-mgr.h"
+#include "lib/inc-proc-eng.h"
+#include "lib/stopwatch-names.h"
+#include "northd.h"
+#include "openvswitch/vlog.h"
+#include "stopwatch.h"
+#include "timeval.h"
+
+VLOG_DEFINE_THIS_MODULE(en_dp_group);
+
+void *
+en_dp_group_init(struct engine_node *node OVS_UNUSED,
+                 struct engine_arg *arg OVS_UNUSED)
+{
+    return NULL;
+}
+
+static void
+dp_group_sync_to_sb(struct engine_node *node,
+                    struct lflow_data *lflow_data)
+{
+    const struct engine_context *eng_ctx = engine_get_context();
+    struct all_synced_datapaths *all_dps =
+        engine_get_input_data("datapath_sync", node);
+    struct ed_type_global_config *global_config =
+        engine_get_input_data("global_config", node);
+
+    const struct sbrec_logical_flow_table *sb_flow_table =
+        EN_OVSDB_GET(engine_get_input("SB_logical_flow", node));
+    const struct sbrec_logical_dp_group_table *sb_dpgrp_table =
+        EN_OVSDB_GET(engine_get_input("SB_logical_dp_group", node));
+
+    stopwatch_start(LFLOWS_TO_SB_STOPWATCH_NAME, time_msec());
+    lflow_table_sync_to_sb(lflow_data->lflow_table,
+                           eng_ctx->ovnsb_idl_txn,
+                           all_dps->synced_dps,
+                           global_config->ovn_internal_version_changed,
+                           sb_flow_table, sb_dpgrp_table);
+    stopwatch_stop(LFLOWS_TO_SB_STOPWATCH_NAME, time_msec());
+}
+
+enum engine_node_state
+en_dp_group_run(struct engine_node *node,
+                void *data OVS_UNUSED)
+{
+    struct lflow_data *lflow_data = engine_get_input_data("lflow", node);
+
+    dp_group_sync_to_sb(node, lflow_data);
+    return EN_UPDATED;
+}
+
+enum engine_input_handler_result
+dp_group_lflow_handler(struct engine_node *node,
+                       void *data OVS_UNUSED)
+{
+    struct lflow_data *lflow_data = engine_get_input_data("lflow", node);
+
+    const struct engine_context *eng_ctx = engine_get_context();
+    struct all_synced_datapaths *all_dps =
+        engine_get_input_data("datapath_sync", node);
+
+    const struct sbrec_logical_flow_table *sb_flow_table =
+        EN_OVSDB_GET(engine_get_input("SB_logical_flow", node));
+    const struct sbrec_logical_dp_group_table *sb_dpgrp_table =
+        EN_OVSDB_GET(engine_get_input("SB_logical_dp_group", 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) {
+
+        dp_group_sync_to_sb(node, lflow_data);
+        return EN_HANDLED_UPDATED;
+    }
+
+    struct hmapx_node *hmapx_node;
+    HMAPX_FOR_EACH (hmapx_node, &lflow_data->trk_data.dirty_lflow_refs) {
+        struct lflow_ref *ref = hmapx_node->data;
+        if (!lflow_ref_sync_lflows(ref, lflow_data->lflow_table,
+                                   eng_ctx->ovnsb_idl_txn,
+                                   all_dps->synced_dps,
+                                   global_config->ovn_internal_version_changed,
+                                   sb_flow_table, sb_dpgrp_table)) {
+            return EN_UNHANDLED;
+        }
+    }
+    return EN_HANDLED_UPDATED;
+}
+
+void
+en_dp_group_cleanup(void *data OVS_UNUSED)
+{
+}
diff --git a/northd/en-dp-group.h b/northd/en-dp-group.h
new file mode 100644
index 000000000..a6422df66
--- /dev/null
+++ b/northd/en-dp-group.h
@@ -0,0 +1,28 @@ 
+/*
+ * Copyright (c) 2026, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EN_DP_GROUP_H
+#define EN_DP_GROUP_H 1
+
+#include "lib/inc-proc-eng.h"
+
+void *en_dp_group_init(struct engine_node *node, struct engine_arg *arg);
+enum engine_node_state en_dp_group_run(struct engine_node *node, void *data);
+void en_dp_group_cleanup(void *data);
+enum engine_input_handler_result
+dp_group_lflow_handler(struct engine_node *node, void *data);
+
+#endif /* EN_DP_GROUP_H */
diff --git a/northd/en-lflow.c b/northd/en-lflow.c
index e8e6cdaa2..f2ae40ec8 100644
--- a/northd/en-lflow.c
+++ b/northd/en-lflow.c
@@ -68,10 +68,6 @@  lflow_get_input_data(struct engine_node *node,
     struct all_synced_datapaths *all_dps =
         engine_get_input_data("datapath_sync", node);
 
-    lflow_input->sbrec_logical_flow_table =
-        EN_OVSDB_GET(engine_get_input("SB_logical_flow", node));
-    lflow_input->sbrec_logical_dp_group_table =
-        EN_OVSDB_GET(engine_get_input("SB_logical_dp_group", node));
     lflow_input->sbrec_acl_id_table =
         EN_OVSDB_GET(engine_get_input("SB_acl_id", node));
 
@@ -119,8 +115,6 @@  lflow_get_input_data(struct engine_node *node,
 enum engine_node_state
 en_lflow_run(struct engine_node *node, void *data)
 {
-    const struct engine_context *eng_ctx = engine_get_context();
-
     struct lflow_input lflow_input;
     lflow_get_input_data(node, &lflow_input);
 
@@ -130,8 +124,7 @@  en_lflow_run(struct engine_node *node, void *data)
     lflow_reset_northd_refs(&lflow_input);
     lflow_ref_clear(lflow_input.igmp_lflow_ref);
 
-    build_lflows(eng_ctx->ovnsb_idl_txn, &lflow_input,
-                 lflow_data->lflow_table);
+    build_lflows(&lflow_input, lflow_data->lflow_table);
 
     return EN_UPDATED;
 }
@@ -149,31 +142,25 @@  lflow_northd_handler(struct engine_node *node,
         return EN_UNHANDLED;
     }
 
-    const struct engine_context *eng_ctx = engine_get_context();
     struct lflow_data *lflow_data = data;
 
     struct lflow_input lflow_input;
     lflow_get_input_data(node, &lflow_input);
 
-    if (!lflow_handle_northd_lr_changes(eng_ctx->ovnsb_idl_txn,
-                                        &northd_data->trk_data.trk_routers,
-                                        &lflow_input,
-                                        lflow_data->lflow_table)) {
-        return EN_UNHANDLED;
-    }
+    lflow_handle_northd_lr_changes(&northd_data->trk_data.trk_routers,
+                                   &lflow_input,
+                                   lflow_data->lflow_table,
+                                   &lflow_data->trk_data.dirty_lflow_refs);
 
-    if (!lflow_handle_northd_port_changes(eng_ctx->ovnsb_idl_txn,
-                                          &northd_data->trk_data.trk_lsps,
-                                          &lflow_input,
-                                          lflow_data->lflow_table)) {
-        return EN_UNHANDLED;
-    }
+    lflow_handle_northd_port_changes(&northd_data->trk_data.trk_lsps,
+                                     &lflow_input,
+                                     lflow_data->lflow_table,
+                                     &lflow_data->trk_data.dirty_lflow_refs);
 
-    if (!lflow_handle_northd_lb_changes(
-            eng_ctx->ovnsb_idl_txn, &northd_data->trk_data.trk_lbs,
-            &lflow_input, lflow_data->lflow_table)) {
-        return EN_UNHANDLED;
-    }
+    lflow_handle_northd_lb_changes(&northd_data->trk_data.trk_lbs,
+                                   &lflow_input,
+                                   lflow_data->lflow_table,
+                                   &lflow_data->trk_data.dirty_lflow_refs);
 
     return EN_HANDLED_UPDATED;
 }
@@ -189,17 +176,14 @@  lflow_lr_stateful_handler(struct engine_node *node, void *data)
         return EN_UNHANDLED;
     }
 
-    const struct engine_context *eng_ctx = engine_get_context();
     struct lflow_data *lflow_data = data;
     struct lflow_input lflow_input;
 
     lflow_get_input_data(node, &lflow_input);
-    if (!lflow_handle_lr_stateful_changes(eng_ctx->ovnsb_idl_txn,
-                                          &lr_sful_data->trk_data,
-                                          &lflow_input,
-                                          lflow_data->lflow_table)) {
-        return EN_UNHANDLED;
-    }
+    lflow_handle_lr_stateful_changes(&lr_sful_data->trk_data,
+                                     &lflow_input,
+                                     lflow_data->lflow_table,
+                                     &lflow_data->trk_data.dirty_lflow_refs);
 
     return EN_HANDLED_UPDATED;
 }
@@ -214,17 +198,14 @@  lflow_ls_stateful_handler(struct engine_node *node, void *data)
         return EN_UNHANDLED;
     }
 
-    const struct engine_context *eng_ctx = engine_get_context();
     struct lflow_data *lflow_data = data;
     struct lflow_input lflow_input;
 
     lflow_get_input_data(node, &lflow_input);
-    if (!lflow_handle_ls_stateful_changes(eng_ctx->ovnsb_idl_txn,
-                                          &ls_sful_data->trk_data,
-                                          &lflow_input,
-                                          lflow_data->lflow_table)) {
-        return EN_UNHANDLED;
-    }
+    lflow_handle_ls_stateful_changes(&ls_sful_data->trk_data,
+                                     &lflow_input,
+                                     lflow_data->lflow_table,
+                                     &lflow_data->trk_data.dirty_lflow_refs);
 
     return EN_HANDLED_UPDATED;
 }
@@ -235,35 +216,19 @@  lflow_multicast_igmp_handler(struct engine_node *node, void *data)
     struct multicast_igmp_data *mcast_igmp_data =
         engine_get_input_data("multicast_igmp", node);
 
-    const struct engine_context *eng_ctx = engine_get_context();
     struct lflow_data *lflow_data = data;
     struct lflow_input lflow_input;
     lflow_get_input_data(node, &lflow_input);
 
-    if (!lflow_ref_resync_flows(mcast_igmp_data->lflow_ref,
-                                lflow_data->lflow_table,
-                                eng_ctx->ovnsb_idl_txn,
-                                lflow_input.dps,
-                                lflow_input.ovn_internal_version_changed,
-                                lflow_input.sbrec_logical_flow_table,
-                                lflow_input.sbrec_logical_dp_group_table)) {
-        return EN_UNHANDLED;
-    }
+    lflow_ref_unlink_and_prune(mcast_igmp_data->lflow_ref,
+                              lflow_data->lflow_table);
 
     build_igmp_lflows(&mcast_igmp_data->igmp_groups,
                       &lflow_input.ls_datapaths->datapaths,
                       lflow_data->lflow_table,
                       mcast_igmp_data->lflow_ref);
 
-    if (!lflow_ref_sync_lflows(mcast_igmp_data->lflow_ref,
-                               lflow_data->lflow_table,
-                               eng_ctx->ovnsb_idl_txn,
-                               lflow_input.dps,
-                               lflow_input.ovn_internal_version_changed,
-                               lflow_input.sbrec_logical_flow_table,
-                               lflow_input.sbrec_logical_dp_group_table)) {
-        return EN_UNHANDLED;
-    }
+    lflow_data->trk_data.needs_full_sync = true;
 
     return EN_HANDLED_UPDATED;
 }
@@ -280,7 +245,6 @@  lflow_group_ecmp_route_change_handler(struct engine_node *node,
         return EN_UNHANDLED;
     }
 
-    const struct engine_context *eng_ctx = engine_get_context();
     struct lflow_data *lflow_data = data;
 
     struct lflow_input lflow_input;
@@ -289,26 +253,16 @@  lflow_group_ecmp_route_change_handler(struct engine_node *node,
     struct group_ecmp_datapath *route_node;
     struct hmapx_node *hmapx_node;
 
-    /* We need to handle deletions before additions as they could potentially
-     * overlap. */
     HMAPX_FOR_EACH (hmapx_node,
                     &group_ecmp_route_data->trk_data.deleted_datapath_routes) {
         route_node = hmapx_node->data;
         lflow_ref_unlink_lflows(route_node->lflow_ref,
                                 lflow_data->lflow_table);
 
-        bool handled = lflow_ref_sync_lflows(
-            route_node->lflow_ref, lflow_data->lflow_table,
-            eng_ctx->ovnsb_idl_txn, lflow_input.dps,
-            lflow_input.ovn_internal_version_changed,
-            lflow_input.sbrec_logical_flow_table,
-            lflow_input.sbrec_logical_dp_group_table);
-        if (!handled) {
-            return EN_UNHANDLED;
-        }
+        hmapx_add(&lflow_data->trk_data.dirty_lflow_refs,
+                  route_node->lflow_ref);
     }
 
-    /* Now we handle created or updated route nodes. */
     struct hmapx *crupdated_datapath_routes =
         &group_ecmp_route_data->trk_data.crupdated_datapath_routes;
     HMAPX_FOR_EACH (hmapx_node, crupdated_datapath_routes) {
@@ -318,16 +272,8 @@  lflow_group_ecmp_route_change_handler(struct engine_node *node,
         build_route_data_flows_for_lrouter(
             route_node->od, lflow_data->lflow_table,
             route_node, lflow_input.bfd_ports);
-
-        bool handled = lflow_ref_sync_lflows(
-            route_node->lflow_ref, lflow_data->lflow_table,
-            eng_ctx->ovnsb_idl_txn, lflow_input.dps,
-            lflow_input.ovn_internal_version_changed,
-            lflow_input.sbrec_logical_flow_table,
-            lflow_input.sbrec_logical_dp_group_table);
-        if (!handled) {
-            return EN_UNHANDLED;
-        }
+        hmapx_add(&lflow_data->trk_data.dirty_lflow_refs,
+                  route_node->lflow_ref);
     }
 
     return EN_HANDLED_UPDATED;
@@ -340,7 +286,6 @@  lflow_ic_learned_svc_mons_handler(struct engine_node *node,
     struct ic_learned_svc_monitors_data *ic_learned_svc_monitors_data =
         engine_get_input_data("ic_learned_svcs", node);
 
-    const struct engine_context *eng_ctx = engine_get_context();
     struct lflow_data *lflow_data = data;
     struct lflow_input lflow_input;
     lflow_get_input_data(node, &lflow_input);
@@ -351,16 +296,8 @@  lflow_ic_learned_svc_mons_handler(struct engine_node *node,
             &ic_learned_svc_monitors_data->ic_learned_svc_monitors_map,
             ic_learned_svc_monitors_data->lflow_ref);
 
-    if (!lflow_ref_resync_flows(
-            ic_learned_svc_monitors_data->lflow_ref,
-            lflow_data->lflow_table,
-            eng_ctx->ovnsb_idl_txn,
-            lflow_input.dps,
-            lflow_input.ovn_internal_version_changed,
-            lflow_input.sbrec_logical_flow_table,
-            lflow_input.sbrec_logical_dp_group_table)) {
-        return EN_UNHANDLED;
-    }
+    lflow_ref_unlink_and_prune(ic_learned_svc_monitors_data->lflow_ref,
+                               lflow_data->lflow_table);
 
     build_lswitch_arp_nd_ic_learned_svc_mon(
         &svc_mons_data,
@@ -368,16 +305,7 @@  lflow_ic_learned_svc_mons_handler(struct engine_node *node,
         lflow_input.svc_monitor_mac,
         lflow_data->lflow_table);
 
-    if (!lflow_ref_sync_lflows(
-            ic_learned_svc_monitors_data->lflow_ref,
-            lflow_data->lflow_table,
-            eng_ctx->ovnsb_idl_txn,
-            lflow_input.dps,
-            lflow_input.ovn_internal_version_changed,
-            lflow_input.sbrec_logical_flow_table,
-            lflow_input.sbrec_logical_dp_group_table)) {
-        return EN_UNHANDLED;
-    }
+    lflow_data->trk_data.needs_full_sync = true;
 
     return EN_HANDLED_UPDATED;
 }
@@ -388,6 +316,9 @@  void *en_lflow_init(struct engine_node *node OVS_UNUSED,
     struct lflow_data *data = xmalloc(sizeof *data);
     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;
+
     return data;
 }
 
@@ -395,4 +326,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);
+}
+
+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;
 }
diff --git a/northd/en-lflow.h b/northd/en-lflow.h
index 99bcfda15..fd3f2427f 100644
--- a/northd/en-lflow.h
+++ b/northd/en-lflow.h
@@ -7,17 +7,25 @@ 
 #include <stdlib.h>
 #include <stdio.h>
 
+#include "lib/hmapx.h"
 #include "lib/inc-proc-eng.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. */
+};
+
 struct lflow_data {
     struct lflow_table *lflow_table;
+    struct lflow_tracked_data trk_data;
 };
 
 enum engine_node_state en_lflow_run(struct engine_node *node, void *data);
 void *en_lflow_init(struct engine_node *node, struct engine_arg *arg);
 void en_lflow_cleanup(void *data);
+void en_lflow_clear_tracked_data(void *data);
 enum engine_input_handler_result lflow_northd_handler(struct engine_node *,
                                                       void *data);
 enum engine_input_handler_result
diff --git a/northd/en-northd-output.c b/northd/en-northd-output.c
index b492a771c..ef42bf9b7 100644
--- a/northd/en-northd-output.c
+++ b/northd/en-northd-output.c
@@ -51,8 +51,8 @@  northd_output_sync_to_sb_handler(struct engine_node *node OVS_UNUSED,
 }
 
 enum engine_input_handler_result
-northd_output_lflow_handler(struct engine_node *node OVS_UNUSED,
-                            void *data OVS_UNUSED)
+northd_output_dp_group_handler(struct engine_node *node OVS_UNUSED,
+                               void *data OVS_UNUSED)
 {
     return EN_HANDLED_UPDATED;
 }
diff --git a/northd/en-northd-output.h b/northd/en-northd-output.h
index b7053e60c..d8854c2a2 100644
--- a/northd/en-northd-output.h
+++ b/northd/en-northd-output.h
@@ -13,7 +13,8 @@  enum engine_input_handler_result
 northd_output_sync_to_sb_handler(struct engine_node *node,
                                  void *data OVS_UNUSED);
 enum engine_input_handler_result
-northd_output_lflow_handler(struct engine_node *node, void *data OVS_UNUSED);
+northd_output_dp_group_handler(struct engine_node *node,
+                               void *data OVS_UNUSED);
 enum engine_input_handler_result
 northd_output_mac_binding_aging_handler(struct engine_node *node,
                                         void *data OVS_UNUSED);
diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
index d2f28666c..53604a20a 100644
--- a/northd/inc-proc-northd.c
+++ b/northd/inc-proc-northd.c
@@ -37,6 +37,7 @@ 
 #include "en-multicast.h"
 #include "en-northd.h"
 #include "en-lflow.h"
+#include "en-dp-group.h"
 #include "en-northd-output.h"
 #include "en-meters.h"
 #include "en-sampling-app.h"
@@ -160,7 +161,8 @@  enum sb_engine_node {
 static ENGINE_NODE(northd, CLEAR_TRACKED_DATA, SB_WRITE);
 static ENGINE_NODE(sync_from_sb, SB_WRITE);
 static ENGINE_NODE(sampling_app);
-static ENGINE_NODE(lflow, SB_WRITE);
+static ENGINE_NODE(lflow, CLEAR_TRACKED_DATA);
+static ENGINE_NODE(dp_group, SB_WRITE);
 static ENGINE_NODE(mac_binding_aging, SB_WRITE);
 static ENGINE_NODE(mac_binding_aging_waker);
 static ENGINE_NODE(northd_output);
@@ -399,9 +401,7 @@  void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_multicast_igmp, &en_sb_igmp_group, NULL);
 
     engine_add_input(&en_lflow, &en_sync_meters, NULL);
-    engine_add_input(&en_lflow, &en_sb_logical_flow, NULL);
     engine_add_input(&en_lflow, &en_sb_multicast_group, NULL);
-    engine_add_input(&en_lflow, &en_sb_logical_dp_group, NULL);
     engine_add_input(&en_lflow, &en_bfd_sync, NULL);
     engine_add_input(&en_lflow, &en_route_policies, NULL);
     /* Route changes are propagated to en_lflow through the en_group_ecmp_route
@@ -436,6 +436,25 @@  void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_lflow, &en_ic_learned_svc_monitors,
                      lflow_ic_learned_svc_mons_handler);
 
+    engine_add_input(&en_dp_group, &en_lflow,
+                     dp_group_lflow_handler);
+    engine_add_input(&en_dp_group, &en_sb_logical_flow, NULL);
+    engine_add_input(&en_dp_group, &en_sb_logical_dp_group, NULL);
+    /* dp_group reads synced datapath arrays (dps[DP_MAX]) when
+     * calling lflow_table_sync_to_sb / lflow_ref_sync_lflows.  A noop
+     * handler is sufficient because en_lflow already recomputes when
+     * datapath_sync changes; dp_group just needs access to
+     * the data. */
+    engine_add_input(&en_dp_group, &en_datapath_sync,
+                     engine_noop_handler);
+    /* dp_group reads ovn_internal_version_changed from
+     * global_config when syncing flows to SB.  Changes to
+     * global_config propagate through en_lflow (which has its own
+     * global_config handler), so a noop handler here avoids a
+     * redundant recompute. */
+    engine_add_input(&en_dp_group, &en_global_config,
+                     engine_noop_handler);
+
     engine_add_input(&en_sync_to_sb_addr_set, &en_northd, NULL);
     engine_add_input(&en_sync_to_sb_addr_set, &en_lr_stateful, NULL);
     engine_add_input(&en_sync_to_sb_addr_set, &en_sb_address_set, NULL);
@@ -490,8 +509,8 @@  void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_northd_output, &en_sync_from_sb, NULL);
     engine_add_input(&en_northd_output, &en_sync_to_sb,
                      northd_output_sync_to_sb_handler);
-    engine_add_input(&en_northd_output, &en_lflow,
-                     northd_output_lflow_handler);
+    engine_add_input(&en_northd_output, &en_dp_group,
+                     northd_output_dp_group_handler);
     engine_add_input(&en_northd_output, &en_mac_binding_aging,
                      northd_output_mac_binding_aging_handler);
     engine_add_input(&en_northd_output, &en_fdb_aging,
diff --git a/northd/lflow-mgr.c b/northd/lflow-mgr.c
index f51df9ce0..deceadb74 100644
--- a/northd/lflow-mgr.c
+++ b/northd/lflow-mgr.c
@@ -261,6 +261,28 @@  lflow_table_set_size(struct lflow_table *lflow_table, size_t size)
     lflow_table->entries.n = size;
 }
 
+/* An lflow that applies to no datapaths (its dp group bitmap is empty) must
+ * not exist in the SB DB.  Such lflows can be left in the table by en_lflow's
+ * incremental handlers, which unlink lflows (clearing their dp bits) and defer
+ * the teardown to the dp-group-resolved sync stage.  Release the lflow's dp
+ * group and destroy it; its SB row (if any) is removed by the caller.  Returns
+ * true if the lflow was destroyed. */
+static bool
+lflow_prune_if_no_datapaths(struct lflow_table *lflow_table,
+                            struct ovn_lflow *lflow)
+{
+    if (dynamic_bitmap_count1(&lflow->dpg_bitmap)) {
+        return false;
+    }
+
+    enum ovn_datapath_type dp_type = ovn_stage_to_datapath_type(lflow->stage);
+    ovs_assert(dp_type < DP_MAX);
+    ovn_dp_group_release(&lflow_table->dp_groups[dp_type], lflow->dpg);
+    lflow->dpg = NULL;
+    ovn_lflow_destroy(lflow_table, lflow);
+    return true;
+}
+
 void
 lflow_table_sync_to_sb(struct lflow_table *lflow_table,
                        struct ovsdb_idl_txn *ovnsb_txn,
@@ -287,6 +309,12 @@  lflow_table_sync_to_sb(struct lflow_table *lflow_table,
             ovn_lflow_destroy(lflow_table, lflow);
             continue;
         }
+        /* An lflow with no datapaths must be removed from the SB DB.  It is
+         * skipped here (not added to 'sb_uuid_set'), so its SB row, if any, is
+         * deleted by the reconciliation loop below. */
+        if (lflow_prune_if_no_datapaths(lflow_table, lflow)) {
+            continue;
+        }
         sbflow = NULL;
         if (!uuid_is_zero(&lflow->sb_uuid)) {
             sbflow = sbrec_logical_flow_table_get_for_uuid(sb_flow_table,
@@ -372,7 +400,7 @@  lflow_table_sync_to_sb(struct lflow_table *lflow_table,
             lflows, &stage,
             sbflow->priority, sbflow->match, sbflow->actions,
             sbflow->controller_meter, acl_ct_translation, sbflow->hash);
-        if (lflow) {
+        if (lflow && !lflow_prune_if_no_datapaths(lflow_table, lflow)) {
             const struct ovn_synced_datapaths *datapaths;
             struct hmap *dp_groups;
             dp_groups = &lflow_table->dp_groups[dp_type];
@@ -393,6 +421,9 @@  lflow_table_sync_to_sb(struct lflow_table *lflow_table,
         if (search_mode != LFLOW_TABLE_SEARCH_FIELDS) {
             break;
         }
+        if (lflow_prune_if_no_datapaths(lflow_table, lflow)) {
+            continue;
+        }
         const struct ovn_synced_datapaths *datapaths;
         struct hmap *dp_groups;
         enum ovn_datapath_type dp_type =
@@ -708,20 +739,32 @@  lflow_ref_unlink_lflows(struct lflow_ref *lflow_ref,
     }
 }
 
-bool
-lflow_ref_resync_flows(struct lflow_ref *lflow_ref,
-                       struct lflow_table *lflow_table,
-                       struct ovsdb_idl_txn *ovnsb_txn,
-                       const struct ovn_synced_datapaths dps[DP_MAX],
-                       bool ovn_internal_version_changed,
-                       const struct sbrec_logical_flow_table *sbflow_table,
-                       const struct sbrec_logical_dp_group_table *dpgrp_table)
+/* Unlinks and destroys all lrns in 'lflow_ref', then destroys any lflow
+ * 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. */
+void
+lflow_ref_unlink_and_prune(struct lflow_ref *lflow_ref,
+                           struct lflow_table *lflow_table)
 {
     lflow_ref_unlink_lflows(lflow_ref, lflow_table);
-    return lflow_ref_sync_lflows__(lflow_ref, lflow_table, ovnsb_txn,
-                                   dps,
-                                   ovn_internal_version_changed, sbflow_table,
-                                   dpgrp_table);
+
+    struct lflow_ref_node *lrn;
+    HMAP_FOR_EACH_SAFE (lrn, ref_node, &lflow_ref->lflow_ref_nodes) {
+        struct ovn_lflow *lflow = lrn->lflow;
+        lflow_ref_node_destroy(lrn);
+
+        if (ovs_list_is_empty(&lflow->referenced_by)) {
+            enum ovn_datapath_type dp_type =
+                ovn_stage_to_datapath_type(lflow->stage);
+            ovs_assert(dp_type < DP_MAX);
+            ovn_dp_group_release(&lflow_table->dp_groups[dp_type],
+                                 lflow->dpg);
+            lflow->dpg = NULL;
+            ovn_lflow_destroy(lflow_table, lflow);
+        }
+    }
 }
 
 bool
@@ -808,10 +851,78 @@  lflow_table_add_lflow__(struct lflow_table *lflow_table,
             }
             ovs_list_insert(&lflow->referenced_by, &lrn->ref_list_node);
             hmap_insert(&lflow_ref->lflow_ref_nodes, &lrn->ref_node, hash);
+        } else if (sdp) {
+            /* Single-datapath (re-)add of an existing node. */
+            if (!lrn->dpgrp_lflow) {
+                if (!lrn->linked) {
+                    /* First add of this (re-)link cycle establishes the base
+                     * datapath this reference tracks for the lflow. */
+                    lrn->dp_index = sdp->index;
+                } else if (lrn->dp_index != sdp->index) {
+                    /* The same 'lflow_ref' references this lflow L(M, A) for a
+                     * second datapath in this cycle (e.g. the single lflow_ref
+                     * shared by all IGMP flows).  A single-datapath
+                     * lflow_ref_node tracks only one datapath index, so
+                     * lflow_ref_unlink_lflows() would leave the other
+                     * datapaths set in the lflow's dp group bitmap.  Upgrade
+                     * the node to track a datapath bitmap instead, so that
+                     * unlinking clears every datapath this reference
+                     * contributed. */
+                    size_t len = sparse_array_len(&sdp->dps->dps_array);
+                    lrn->dpgrp_bitmap = bitmap_allocate(len);
+                    lrn->dpgrp_bitmap_len = len;
+                    bitmap_set1(lrn->dpgrp_bitmap, lrn->dp_index);
+                    bitmap_set1(lrn->dpgrp_bitmap, sdp->index);
+                    lrn->dpgrp_lflow = true;
+
+                    /* This reference already accounted for 'lrn->dp_index'
+                     * (the first datapath it added this cycle) in the block
+                     * below.  This add contributes a second datapath, which
+                     * the block below will not see because it only runs on
+                     * the first link of the cycle.  Account for it here so
+                     * that a datapath shared with another reference is not
+                     * released prematurely. */
+                    if (dynamic_bitmap_is_set(&lflow->dpg_bitmap,
+                                              sdp->index)) {
+                        dp_refcnt_use(&lflow->dp_refcnts_map, sdp->index);
+                    }
+                }
+            } else {
+                /* A node previously upgraded to track a datapath bitmap is
+                 * re-linked one datapath at a time. */
+                if (!lrn->linked) {
+                    /* First add of this (re-)link cycle: rebuild the tracked
+                     * bitmap from scratch so it reflects the current datapath
+                     * set and the current datapath array length (which may
+                     * have grown or shrunk since the node was last linked). */
+                    size_t len = sparse_array_len(&sdp->dps->dps_array);
+                    bitmap_free(lrn->dpgrp_bitmap);
+                    lrn->dpgrp_bitmap = bitmap_allocate(len);
+                    lrn->dpgrp_bitmap_len = len;
+                } else if (!bitmap_is_set(lrn->dpgrp_bitmap, sdp->index)) {
+                    /* A second or later datapath this reference contributes in
+                     * the same cycle.  The first datapath was accounted for by
+                     * the block below (which only runs on the first link of
+                     * the cycle); account for this one too so that a datapath
+                     * shared with another reference is not released
+                     * prematurely. */
+                    if (dynamic_bitmap_is_set(&lflow->dpg_bitmap,
+                                              sdp->index)) {
+                        dp_refcnt_use(&lflow->dp_refcnts_map, sdp->index);
+                    }
+                }
+                bitmap_set1(lrn->dpgrp_bitmap, sdp->index);
+            }
         }
 
         if (!lrn->linked) {
-            if (lrn->dpgrp_lflow) {
+            /* Allocate a reference counter only if the datapath(s) added by
+             * this reference are already used by the lflow. */
+            if (sdp) {
+                if (dynamic_bitmap_is_set(&lflow->dpg_bitmap, sdp->index)) {
+                    dp_refcnt_use(&lflow->dp_refcnts_map, sdp->index);
+                }
+            } else {
                 ovs_assert(lrn->dpgrp_bitmap_len == dp_bitmap_len);
                 size_t index;
                 BITMAP_FOR_EACH_1 (index, dp_bitmap_len, dp_bitmap) {
@@ -820,11 +931,6 @@  lflow_table_add_lflow__(struct lflow_table *lflow_table,
                         dp_refcnt_use(&lflow->dp_refcnts_map, index);
                     }
                 }
-            } else {
-                /* Allocate a reference counter only if already used. */
-                if (dynamic_bitmap_is_set(&lflow->dpg_bitmap, lrn->dp_index)) {
-                    dp_refcnt_use(&lflow->dp_refcnts_map, lrn->dp_index);
-                }
             }
         }
         lrn->linked = true;
@@ -1258,17 +1364,12 @@  sync_lflow_to_sb(struct ovn_lflow *lflow,
                 &lflow->dpg->dpg_uuid);
 
             if (!lflow->dpg->dp_group) {
-                /* Ideally this should not happen.  But it can still happen
-                 * due to 2 reasons:
-                 * 1. There is a bug in the dp_group management.  We should
-                 *    perhaps assert here.
-                 * 2. A User or CMS may delete the logical_dp_groups in SB DB
-                 *    or clear the SB:Logical_flow.logical_dp_groups column
-                 *    (intentionally or accidentally)
-                 *
-                 * Because of (2) it is better to return false instead of
-                 * assert,so that we recover from th inconsistent SB DB.
-                 */
+                /* The SB Logical_DP_Group row referenced by this in-memory dp
+                 * group no longer exists.  This can happen if a user or CMS
+                 * deletes the SB:Logical_DP_Group rows or clears the
+                 * SB:Logical_Flow.logical_dp_group column (intentionally or
+                 * accidentally).  Release the stale dp group and fall through
+                 * to create a fresh one. */
                 static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
                 VLOG_WARN_RL(&rl, "SB Logical flow ["UUID_FMT"]'s "
                             "logical_dp_group column is not set "
@@ -1276,10 +1377,12 @@  sync_lflow_to_sb(struct ovn_lflow *lflow,
                             "referencing the dp group ["UUID_FMT"]",
                             UUID_ARGS(&sbflow->header_.uuid),
                             UUID_ARGS(&lflow->dpg->dpg_uuid));
-                lflow->sync_state = LFLOW_STALE;
-                return false;
+                ovn_dp_group_release(dp_groups, lflow->dpg);
+                lflow->dpg = NULL;
+                pre_sync_dpg = NULL;
             }
-        } else {
+        }
+        if (!lflow->dpg) {
             lflow->dpg = ovn_dp_group_create(
                                 ovnsb_txn, dp_groups, sbrec_dp_group,
                                 &lflow->dpg_bitmap,
@@ -1291,6 +1394,7 @@  sync_lflow_to_sb(struct ovn_lflow *lflow,
 
     if (pre_sync_dpg != lflow->dpg) {
         ovn_dp_group_use(lflow->dpg);
+        ovn_dp_group_release(dp_groups, pre_sync_dpg);
     }
 
     lflow->sync_state = LFLOW_SYNCED;
diff --git a/northd/lflow-mgr.h b/northd/lflow-mgr.h
index 6253be685..678e97214 100644
--- a/northd/lflow-mgr.h
+++ b/northd/lflow-mgr.h
@@ -58,13 +58,7 @@  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 *);
-bool lflow_ref_resync_flows(struct lflow_ref *,
-                            struct lflow_table *lflow_table,
-                            struct ovsdb_idl_txn *ovnsb_txn,
-                            const struct ovn_synced_datapaths dps[DP_MAX],
-                            bool ovn_internal_version_changed,
-                            const struct sbrec_logical_flow_table *,
-                            const struct sbrec_logical_dp_group_table *);
+void lflow_ref_unlink_and_prune(struct lflow_ref *, struct lflow_table *);
 bool lflow_ref_sync_lflows(struct lflow_ref *,
                            struct lflow_table *lflow_table,
                            struct ovsdb_idl_txn *ovnsb_txn,
diff --git a/northd/northd.c b/northd/northd.c
index 99954d276..f37040b57 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -21038,10 +21038,9 @@  void run_update_worker_pool(int n_threads)
     }
 }
 
-/* Updates the Logical_Flow and Multicast_Group tables in the OVN_SB database,
- * constructing their contents based on the OVN_NB database. */
-void build_lflows(struct ovsdb_idl_txn *ovnsb_txn,
-                  struct lflow_input *input_data,
+/* Builds the in-memory logical flow table from the OVN_NB database.
+ * The flows are synced to the SB database by en_dp_group. */
+void build_lflows(struct lflow_input *input_data,
                   struct lflow_table *lflows)
 {
     struct svc_monitors_map_data svc_mons_data =
@@ -21085,14 +21084,6 @@  void build_lflows(struct ovsdb_idl_txn *ovnsb_txn,
     /* Parallel build may result in a suboptimal hash. Resize the
      * lflow map to a correct size before doing lookups */
     lflow_table_expand(lflows);
-
-    stopwatch_start(LFLOWS_TO_SB_STOPWATCH_NAME, time_msec());
-    lflow_table_sync_to_sb(lflows, ovnsb_txn, input_data->dps,
-                           input_data->ovn_internal_version_changed,
-                           input_data->sbrec_logical_flow_table,
-                           input_data->sbrec_logical_dp_group_table);
-
-    stopwatch_stop(LFLOWS_TO_SB_STOPWATCH_NAME, time_msec());
 }
 
 void
@@ -21137,24 +21128,17 @@  lflow_reset_northd_refs(struct lflow_input *lflow_input)
     }
 }
 
-bool
-lflow_handle_northd_lr_changes(struct ovsdb_idl_txn *ovnsb_txn,
-                                struct tracked_dps *tracked_lrs,
-                                struct lflow_input *lflow_input,
-                                struct lflow_table *lflows)
+void
+lflow_handle_northd_lr_changes(struct tracked_dps *tracked_lrs,
+                               struct lflow_input *lflow_input,
+                               struct lflow_table *lflows,
+                               struct hmapx *dirty_lflow_refs)
 {
-    bool handled = true;
     struct hmapx_node *hmapx_node;
     HMAPX_FOR_EACH (hmapx_node, &tracked_lrs->deleted) {
         struct ovn_datapath *od = hmapx_node->data;
-        handled = lflow_ref_resync_flows(
-            od->datapath_lflows, lflows, ovnsb_txn, lflow_input->dps,
-            lflow_input->ovn_internal_version_changed,
-            lflow_input->sbrec_logical_flow_table,
-            lflow_input->sbrec_logical_dp_group_table);
-        if (!handled) {
-            return handled;
-        }
+        lflow_ref_unlink_lflows(od->datapath_lflows, lflows);
+        hmapx_add(dirty_lflow_refs, od->datapath_lflows);
     }
 
     struct lswitch_flow_build_info lsi = {
@@ -21172,34 +21156,18 @@  lflow_handle_northd_lr_changes(struct ovsdb_idl_txn *ovnsb_txn,
 
         lflow_ref_unlink_lflows(od->datapath_lflows, lflows);
         build_lswitch_and_lrouter_iterate_by_lr(od, &lsi);
-    }
-
-    /* We need to make sure that all datapath groups are allocated before
-     * trying to sync logical flows. Otherwise, we would need to recompute
-     * those datapath groups within those flows over and over again. */
-    HMAPX_FOR_EACH (hmapx_node, &tracked_lrs->crupdated) {
-        struct ovn_datapath *od = hmapx_node->data;
-
-        handled = lflow_ref_sync_lflows(
-            od->datapath_lflows, lflows, ovnsb_txn, lflow_input->dps,
-            lflow_input->ovn_internal_version_changed,
-            lflow_input->sbrec_logical_flow_table,
-            lflow_input->sbrec_logical_dp_group_table);
-        if (!handled) {
-            break;
-        }
+        hmapx_add(dirty_lflow_refs, od->datapath_lflows);
     }
 
     ds_destroy(&lsi.actions);
     ds_destroy(&lsi.match);
-    return handled;
 }
 
-bool
-lflow_handle_northd_port_changes(struct ovsdb_idl_txn *ovnsb_txn,
-                                 struct tracked_ovn_ports *trk_lsps,
+void
+lflow_handle_northd_port_changes(struct tracked_ovn_ports *trk_lsps,
                                  struct lflow_input *lflow_input,
-                                 struct lflow_table *lflows)
+                                 struct lflow_table *lflows,
+                                 struct hmapx *dirty_lflow_refs)
 {
     struct hmapx_node *hmapx_node;
     struct ovn_port *op;
@@ -21208,14 +21176,8 @@  lflow_handle_northd_port_changes(struct ovsdb_idl_txn *ovnsb_txn,
         op = hmapx_node->data;
         /* Make sure 'op' is an lsp and not lrp. */
         ovs_assert(op->nbsp);
-        bool handled = lflow_ref_resync_flows(
-            op->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
-            lflow_input->ovn_internal_version_changed,
-            lflow_input->sbrec_logical_flow_table,
-            lflow_input->sbrec_logical_dp_group_table);
-        if (!handled) {
-            return false;
-        }
+        lflow_ref_unlink_lflows(op->lflow_ref, lflows);
+        hmapx_add(dirty_lflow_refs, op->lflow_ref);
         /* No need to update SB multicast groups, thanks to weak
          * references. */
     }
@@ -21236,33 +21198,17 @@  lflow_handle_northd_port_changes(struct ovsdb_idl_txn *ovnsb_txn,
                                                  &match, &actions,
                                                  lflow_input->svc_monitor_mac,
                                                  lflows);
-        /* Sync the new flows to SB. */
-        bool handled = lflow_ref_sync_lflows(
-            op->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
-            lflow_input->ovn_internal_version_changed,
-            lflow_input->sbrec_logical_flow_table,
-            lflow_input->sbrec_logical_dp_group_table);
-        if (handled) {
-            /* Now regenerate the stateful lflows for 'op' */
-            /* Clear old lflows. */
-            lflow_ref_unlink_lflows(op->stateful_lflow_ref, lflows);
-            build_lbnat_lflows_iterate_by_lsp(op,
-                                              lflow_input->lr_stateful_table,
-                                              &match, &actions, lflows);
-            handled = lflow_ref_sync_lflows(
-                op->stateful_lflow_ref, lflows, ovnsb_txn,
-                lflow_input->dps,
-                lflow_input->ovn_internal_version_changed,
-                lflow_input->sbrec_logical_flow_table,
-                lflow_input->sbrec_logical_dp_group_table);
-        }
+        hmapx_add(dirty_lflow_refs, op->lflow_ref);
+
+        /* Now regenerate the stateful lflows for 'op' */
+        lflow_ref_unlink_lflows(op->stateful_lflow_ref, lflows);
+        build_lbnat_lflows_iterate_by_lsp(op,
+                                          lflow_input->lr_stateful_table,
+                                          &match, &actions, lflows);
+        hmapx_add(dirty_lflow_refs, op->stateful_lflow_ref);
 
         ds_destroy(&match);
         ds_destroy(&actions);
-
-        if (!handled) {
-            return false;
-        }
     }
 
     HMAPX_FOR_EACH (hmapx_node, &trk_lsps->created) {
@@ -21278,42 +21224,24 @@  lflow_handle_northd_port_changes(struct ovsdb_idl_txn *ovnsb_txn,
                                                  &match, &actions,
                                                  lflow_input->svc_monitor_mac,
                                                  lflows);
+        hmapx_add(dirty_lflow_refs, op->lflow_ref);
 
-        /* Sync the newly added flows to SB. */
-        bool handled = lflow_ref_sync_lflows(
-            op->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
-            lflow_input->ovn_internal_version_changed,
-            lflow_input->sbrec_logical_flow_table,
-            lflow_input->sbrec_logical_dp_group_table);
-        if (handled) {
-            /* Now generate the stateful lflows for 'op' */
-            build_lbnat_lflows_iterate_by_lsp(op,
-                                              lflow_input->lr_stateful_table,
-                                              &match, &actions, lflows);
-            handled = lflow_ref_sync_lflows(
-                op->stateful_lflow_ref, lflows, ovnsb_txn,
-                lflow_input->dps,
-                lflow_input->ovn_internal_version_changed,
-                lflow_input->sbrec_logical_flow_table,
-                lflow_input->sbrec_logical_dp_group_table);
-        }
+        /* Now generate the stateful lflows for 'op' */
+        build_lbnat_lflows_iterate_by_lsp(op,
+                                          lflow_input->lr_stateful_table,
+                                          &match, &actions, lflows);
+        hmapx_add(dirty_lflow_refs, op->stateful_lflow_ref);
 
         ds_destroy(&match);
         ds_destroy(&actions);
-
-        if (!handled) {
-            return false;
-        }
     }
-
-    return true;
 }
 
-bool
-lflow_handle_northd_lb_changes(struct ovsdb_idl_txn *ovnsb_txn,
-                               struct tracked_lbs *trk_lbs,
+void
+lflow_handle_northd_lb_changes(struct tracked_lbs *trk_lbs,
                                struct lflow_input *lflow_input,
-                               struct lflow_table *lflows)
+                               struct lflow_table *lflows,
+                               struct hmapx *dirty_lflow_refs)
 {
     struct ovn_lb_datapaths *lb_dps;
     struct hmapx_node *hmapx_node;
@@ -21325,12 +21253,8 @@  lflow_handle_northd_lb_changes(struct ovsdb_idl_txn *ovnsb_txn,
 
     HMAPX_FOR_EACH (hmapx_node, &trk_lbs->deleted) {
         lb_dps = hmapx_node->data;
-
-        lflow_ref_resync_flows(
-            lb_dps->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
-            lflow_input->ovn_internal_version_changed,
-            lflow_input->sbrec_logical_flow_table,
-            lflow_input->sbrec_logical_dp_group_table);
+        lflow_ref_unlink_lflows(lb_dps->lflow_ref, lflows);
+        hmapx_add(dirty_lflow_refs, lb_dps->lflow_ref);
     }
 
     HMAPX_FOR_EACH (hmapx_node, &trk_lbs->crupdated) {
@@ -21365,31 +21289,20 @@  lflow_handle_northd_lb_changes(struct ovsdb_idl_txn *ovnsb_txn,
         ds_destroy(&match);
         ds_destroy(&actions);
 
-        /* Sync the new flows to SB. */
-        bool handled = lflow_ref_sync_lflows(
-            lb_dps->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
-            lflow_input->ovn_internal_version_changed,
-            lflow_input->sbrec_logical_flow_table,
-            lflow_input->sbrec_logical_dp_group_table);
-        if (!handled) {
-            return false;
-        }
+        hmapx_add(dirty_lflow_refs, lb_dps->lflow_ref);
     }
-
-    return true;
 }
 
-bool
-lflow_handle_lr_stateful_changes(struct ovsdb_idl_txn *ovnsb_txn,
-                                struct lr_stateful_tracked_data *trk_data,
-                                struct lflow_input *lflow_input,
-                                struct lflow_table *lflows)
+void
+lflow_handle_lr_stateful_changes(struct lr_stateful_tracked_data *trk_data,
+                                 struct lflow_input *lflow_input,
+                                 struct lflow_table *lflows,
+                                 struct hmapx *dirty_lflow_refs)
 {
     struct lr_stateful_record *lr_stateful_rec;
     struct ds actions = DS_EMPTY_INITIALIZER;
     struct ds match = DS_EMPTY_INITIALIZER;
     struct hmapx_node *hmapx_node;
-    bool handled = true;
 
     HMAPX_FOR_EACH (hmapx_node, &trk_data->crupdated) {
         lr_stateful_rec = hmapx_node->data;
@@ -21402,17 +21315,7 @@  lflow_handle_lr_stateful_changes(struct ovsdb_idl_txn *ovnsb_txn,
                                 &match, &actions,
                                 lflow_input->meter_groups,
                                 lflow_input->features);
-
-        /* Sync the new flows to SB. */
-        handled = lflow_ref_sync_lflows(
-            lr_stateful_rec->lflow_ref, lflows, ovnsb_txn,
-            lflow_input->dps,
-            lflow_input->ovn_internal_version_changed,
-            lflow_input->sbrec_logical_flow_table,
-            lflow_input->sbrec_logical_dp_group_table);
-        if (!handled) {
-            goto exit;
-        }
+        hmapx_add(dirty_lflow_refs, lr_stateful_rec->lflow_ref);
 
         const struct ovn_datapath *od =
             ovn_datapaths_find_by_index(lflow_input->lr_datapaths,
@@ -21427,16 +21330,7 @@  lflow_handle_lr_stateful_changes(struct ovsdb_idl_txn *ovnsb_txn,
                                               lflow_input->bfd_ports,
                                               &match, &actions,
                                               lflows);
-
-            handled = lflow_ref_sync_lflows(
-                op->stateful_lflow_ref, lflows, ovnsb_txn,
-                lflow_input->dps,
-                lflow_input->ovn_internal_version_changed,
-                lflow_input->sbrec_logical_flow_table,
-                lflow_input->sbrec_logical_dp_group_table);
-            if (!handled) {
-                goto exit;
-            }
+            hmapx_add(dirty_lflow_refs, op->stateful_lflow_ref);
 
             if (op->peer && op->peer->nbsp) {
                 lflow_ref_unlink_lflows(op->peer->stateful_lflow_ref, lflows);
@@ -21444,32 +21338,20 @@  lflow_handle_lr_stateful_changes(struct ovsdb_idl_txn *ovnsb_txn,
                 build_lbnat_lflows_iterate_by_lsp(
                     op->peer, lflow_input->lr_stateful_table, &match, &actions,
                     lflows);
-
-                handled = lflow_ref_sync_lflows(
-                    op->peer->stateful_lflow_ref, lflows, ovnsb_txn,
-                    lflow_input->dps,
-                    lflow_input->ovn_internal_version_changed,
-                    lflow_input->sbrec_logical_flow_table,
-                    lflow_input->sbrec_logical_dp_group_table);
-                if (!handled) {
-                    goto exit;
-                }
+                hmapx_add(dirty_lflow_refs, op->peer->stateful_lflow_ref);
             }
         }
     }
 
-exit:
     ds_destroy(&match);
     ds_destroy(&actions);
-
-    return handled;
 }
 
-bool
-lflow_handle_ls_stateful_changes(struct ovsdb_idl_txn *ovnsb_txn,
-                                struct ls_stateful_tracked_data *trk_data,
-                                struct lflow_input *lflow_input,
-                                struct lflow_table *lflows)
+void
+lflow_handle_ls_stateful_changes(struct ls_stateful_tracked_data *trk_data,
+                                 struct lflow_input *lflow_input,
+                                 struct lflow_table *lflows,
+                                 struct hmapx *dirty_lflow_refs)
 {
     struct hmapx_node *hmapx_node;
 
@@ -21494,39 +21376,14 @@  lflow_handle_ls_stateful_changes(struct ovsdb_idl_txn *ovnsb_txn,
         build_network_function(od, lflows,
                                lflow_input->ls_port_groups,
                                ls_stateful_rec->lflow_ref);
-    }
-
-    /* We need to make sure that all datapath groups are allocated before
-     * trying to sync logical flows. Otherwise, we would need to recompute
-     * those datapath groups within those flows over and over again. */
-    HMAPX_FOR_EACH (hmapx_node, &trk_data->crupdated) {
-        struct ls_stateful_record *ls_stateful_rec = hmapx_node->data;
-        /* Sync the new flows to SB. */
-        bool handled = lflow_ref_sync_lflows(
-            ls_stateful_rec->lflow_ref, lflows, ovnsb_txn,
-            lflow_input->dps,
-            lflow_input->ovn_internal_version_changed,
-            lflow_input->sbrec_logical_flow_table,
-            lflow_input->sbrec_logical_dp_group_table);
-        if (!handled) {
-            return false;
-        }
+        hmapx_add(dirty_lflow_refs, ls_stateful_rec->lflow_ref);
     }
 
     HMAPX_FOR_EACH (hmapx_node, &trk_data->deleted) {
         struct ls_stateful_record *ls_stateful_rec = hmapx_node->data;
-
-        if (!lflow_ref_resync_flows(
-                    ls_stateful_rec->lflow_ref, lflows, ovnsb_txn,
-                    lflow_input->dps,
-                    lflow_input->ovn_internal_version_changed,
-                    lflow_input->sbrec_logical_flow_table,
-                    lflow_input->sbrec_logical_dp_group_table)) {
-            return false;
-        }
+        lflow_ref_unlink_lflows(ls_stateful_rec->lflow_ref, lflows);
+        hmapx_add(dirty_lflow_refs, ls_stateful_rec->lflow_ref);
     }
-
-    return true;
 }
 
 static bool
diff --git a/northd/northd.h b/northd/northd.h
index 9a74a4abc..4150157b0 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -260,9 +260,7 @@  struct lflow_ref;
 struct lr_nat_table;
 
 struct lflow_input {
-    /* Southbound table references */
-    const struct sbrec_logical_flow_table *sbrec_logical_flow_table;
-    const struct sbrec_logical_dp_group_table *sbrec_logical_dp_group_table;
+    /* Southbound table references (used during lflow computation). */
     const struct sbrec_acl_id_table *sbrec_acl_id_table;
 
     /* Indexes */
@@ -979,8 +977,7 @@  struct lr_stateful_tracked_data;
 struct ls_stateful_tracked_data;
 struct group_ecmp_datapath;
 
-void build_lflows(struct ovsdb_idl_txn *ovnsb_txn,
-                  struct lflow_input *input_data,
+void build_lflows(struct lflow_input *input_data,
                   struct lflow_table *);
 void lflow_reset_northd_refs(struct lflow_input *);
 void build_route_data_flows_for_lrouter(
@@ -988,26 +985,26 @@  void build_route_data_flows_for_lrouter(
     const struct group_ecmp_datapath *route_node,
     const struct sset *bfd_ports);
 
-bool lflow_handle_northd_lr_changes(struct ovsdb_idl_txn *ovnsh_txn,
-                                     struct tracked_dps *,
-                                     struct lflow_input *,
-                                     struct lflow_table *lflows);
-bool lflow_handle_northd_port_changes(struct ovsdb_idl_txn *ovnsb_txn,
-                                      struct tracked_ovn_ports *,
+void lflow_handle_northd_lr_changes(struct tracked_dps *,
+                                    struct lflow_input *,
+                                    struct lflow_table *lflows,
+                                    struct hmapx *dirty_lflow_refs);
+void lflow_handle_northd_port_changes(struct tracked_ovn_ports *,
                                       struct lflow_input *,
-                                      struct lflow_table *lflows);
-bool lflow_handle_northd_lb_changes(struct ovsdb_idl_txn *ovnsb_txn,
-                                    struct tracked_lbs *,
+                                      struct lflow_table *lflows,
+                                      struct hmapx *dirty_lflow_refs);
+void lflow_handle_northd_lb_changes(struct tracked_lbs *,
                                     struct lflow_input *,
-                                    struct lflow_table *lflows);
-bool lflow_handle_lr_stateful_changes(struct ovsdb_idl_txn *,
-                                      struct lr_stateful_tracked_data *,
+                                    struct lflow_table *lflows,
+                                    struct hmapx *dirty_lflow_refs);
+void lflow_handle_lr_stateful_changes(struct lr_stateful_tracked_data *,
                                       struct lflow_input *,
-                                      struct lflow_table *lflows);
-bool lflow_handle_ls_stateful_changes(struct ovsdb_idl_txn *,
-                                      struct ls_stateful_tracked_data *,
+                                      struct lflow_table *lflows,
+                                      struct hmapx *dirty_lflow_refs);
+void lflow_handle_ls_stateful_changes(struct ls_stateful_tracked_data *,
                                       struct lflow_input *,
-                                      struct lflow_table *lflows);
+                                      struct lflow_table *lflows,
+                                      struct hmapx *dirty_lflow_refs);
 bool northd_handle_sb_port_binding_changes(
     const struct sbrec_port_binding_table *, struct hmap *ls_ports,
     struct hmap *lr_ports);
diff --git a/tests/ovn-inc-proc-graph-dump.at b/tests/ovn-inc-proc-graph-dump.at
index b0c29d1b5..8348c3467 100644
--- a/tests/ovn-inc-proc-graph-dump.at
+++ b/tests/ovn-inc-proc-graph-dump.at
@@ -143,7 +143,6 @@  digraph "Incremental-Processing-Engine" {
 	sync_meters -> sync_to_sb [[label=""]];
 	sync_to_sb_lb -> sync_to_sb [[label=""]];
 	sync_to_sb_pb -> sync_to_sb [[label=""]];
-	SB_logical_flow [[style=filled, shape=box, fillcolor=white, label="SB_logical_flow"]];
 	SB_multicast_group [[style=filled, shape=box, fillcolor=white, label="SB_multicast_group"]];
 	NB_bfd [[style=filled, shape=box, fillcolor=white, label="NB_bfd"]];
 	SB_bfd [[style=filled, shape=box, fillcolor=white, label="SB_bfd"]];
@@ -186,9 +185,7 @@  digraph "Incremental-Processing-Engine" {
 	SB_igmp_group -> multicast_igmp [[label=""]];
 	lflow [[style=filled, shape=box, fillcolor=white, label="lflow"]];
 	sync_meters -> lflow [[label=""]];
-	SB_logical_flow -> lflow [[label=""]];
 	SB_multicast_group -> lflow [[label=""]];
-	SB_logical_dp_group -> lflow [[label=""]];
 	bfd_sync -> lflow [[label=""]];
 	route_policies -> lflow [[label=""]];
 	routes -> lflow [[label="engine_noop_handler"]];
@@ -203,6 +200,13 @@  digraph "Incremental-Processing-Engine" {
 	multicast_igmp -> lflow [[label="lflow_multicast_igmp_handler"]];
 	SB_acl_id -> lflow [[label=""]];
 	ic_learned_svc_monitors -> lflow [[label="lflow_ic_learned_svc_mons_handler"]];
+	SB_logical_flow [[style=filled, shape=box, fillcolor=white, label="SB_logical_flow"]];
+	dp_group [[style=filled, shape=box, fillcolor=white, label="dp_group"]];
+	lflow -> dp_group [[label="dp_group_lflow_handler"]];
+	SB_logical_flow -> dp_group [[label=""]];
+	SB_logical_dp_group -> dp_group [[label=""]];
+	datapath_sync -> dp_group [[label="engine_noop_handler"]];
+	global_config -> dp_group [[label="engine_noop_handler"]];
 	mac_binding_aging_waker [[style=filled, shape=box, fillcolor=white, label="mac_binding_aging_waker"]];
 	mac_binding_aging [[style=filled, shape=box, fillcolor=white, label="mac_binding_aging"]];
 	SB_mac_binding -> mac_binding_aging [[label=""]];
@@ -237,7 +241,7 @@  digraph "Incremental-Processing-Engine" {
 	acl_id -> northd_output [[label="northd_output_acl_id_handler"]];
 	sync_from_sb -> northd_output [[label=""]];
 	sync_to_sb -> northd_output [[label="northd_output_sync_to_sb_handler"]];
-	lflow -> northd_output [[label="northd_output_lflow_handler"]];
+	dp_group -> northd_output [[label="northd_output_dp_group_handler"]];
 	mac_binding_aging -> northd_output [[label="northd_output_mac_binding_aging_handler"]];
 	fdb_aging -> northd_output [[label="northd_output_fdb_aging_handler"]];
 	ecmp_nexthop -> northd_output [[label="northd_output_ecmp_nexthop_handler"]];
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index b8512a462..f8c144918 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -13736,17 +13736,7 @@  check ovn-nbctl --wait=sb set load_balancer lb1 options:bar=foo
 check_engine_stats lflow norecompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
-# Clear the SB:Logical_Flow.logical_dp_groups column of all the
-# logical flows and then modify the NB:Load_balancer.  ovn-northd
-# should resync the logical flows.
-for l in $(ovn-sbctl --bare --columns _uuid list logical_flow)
-do
-    check ovn-sbctl clear logical_flow $l logical_dp_group
-done
-
-check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
 check ovn-nbctl --wait=sb set load_balancer lb1 options:foo=bar
-check_engine_stats lflow recompute nocompute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 lb_lflow_uuid=$(fetch_column Logical_flow _uuid match='"ct.new && ip4.dst == 10.0.0.10 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80"')
@@ -13915,16 +13905,7 @@  AT_CHECK([echo $dpgrp_dps | grep $sw3_uuid], [0], [ignore])
 AT_CHECK([echo $dpgrp_dps | grep $sw4_uuid], [0], [ignore])
 AT_CHECK([echo $dpgrp_dps | grep $sw5_uuid], [0], [ignore])
 
-# Clear the SB:Logical_Flow.logical_dp_groups column of all the
-# logical flows and then modify the NB:Load_balancer.  ovn-northd
-# should resync the logical flows.
-for l in $(ovn-sbctl --bare --columns _uuid list logical_flow)
-do
-    check ovn-sbctl clear logical_flow $l logical_dp_group
-done
-
 check ovn-nbctl --wait=sb set load_balancer lb2 vips='{"10.0.0.10:80"="10.0.0.3:80,10.0.0.4:80"}'
-check_engine_stats lflow recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 lb_lflow_uuid=$(fetch_column Logical_flow _uuid match='"ct.new && ip4.dst == 10.0.0.10 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80"')
@@ -24576,3 +24557,65 @@  OVN_CLEANUP_NORTHD
 AT_CLEANUP
 ])
 
+AT_SETUP([Datapath group SB writes only when bitmap changes])
+ovn_start
+
+# Create three switches, each with an ACL so they share the
+# "has ACLs" lflows in the same datapath group {sw0, sw1, sw2}.
+check ovn-nbctl ls-add sw0
+check ovn-nbctl ls-add sw1
+check ovn-nbctl ls-add sw2
+check ovn-nbctl lsp-add sw0 sw0-p0
+check ovn-nbctl lsp-add sw1 sw1-p0
+check ovn-nbctl lsp-add sw2 sw2-p0
+check ovn-nbctl acl-add sw0 to-lport 1000 ip4 allow
+check ovn-nbctl acl-add sw1 to-lport 1000 ip6 allow
+check ovn-nbctl --wait=sb acl-add sw2 to-lport 1000 tcp allow
+
+CHECK_NO_CHANGE_AFTER_RECOMPUTE
+
+# Record dp_group state (UUIDs + content).
+ovn-sbctl --bare --columns=_uuid,datapaths list Logical_DP_Group \
+    | sort > dpg_before
+
+# ----------------------------------------------------------
+# Case 1: Change that triggers incremental lflow compute but
+#         does NOT alter the dp-group bitmap.
+#
+# Setting a port address rebuilds the port's lflows but the
+# set of datapaths that share the "has ACLs" lflows is still
+# {sw0, sw1, sw2}.  The dp_group rows should not change.
+# ----------------------------------------------------------
+check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
+check ovn-nbctl --wait=sb lsp-set-addresses sw0-p0 "00:00:00:00:00:01"
+check_engine_stats lflow norecompute compute
+
+ovn-sbctl --bare --columns=_uuid,datapaths list Logical_DP_Group \
+    | sort > dpg_after
+AT_CHECK([diff dpg_before dpg_after])
+
+# ----------------------------------------------------------
+# Case 2: Change that removes a datapath from a shared flow,
+#         altering the dp-group bitmap.
+#
+# Move sw0's ACL to tier 1.  The egress "acl action" flows
+# now differ for sw0, so the {sw0, sw1, sw2} group loses sw0
+# and a {sw1, sw2} group appears (or the existing one is
+# updated).  The dp_group rows must change.
+# ----------------------------------------------------------
+check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
+acl0=$(fetch_column nb:ACL _uuid match=ip4)
+check ovn-nbctl --wait=sb set ACL $acl0 tier=1
+check_engine_stats lflow norecompute compute
+
+ovn-sbctl --bare --columns=_uuid,datapaths list Logical_DP_Group \
+    | sort > dpg_changed
+AT_CHECK([diff dpg_before dpg_changed], [1], [ignore])
+
+check ovn-nbctl --wait=sb ls-del sw0
+
+CHECK_NO_CHANGE_AFTER_RECOMPUTE
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+