Patch (localnet / L2 gateway) port ofport changes force a full recompute
of the logical flow output through two independent engine edges, even
though logical flows never depend on patch ofports:

  - non_vif_data bundles patch ofports together with tunnel data, and
    en_lflow_output has no change handler for non_vif_data, so any patch
    ofport change invalidates it too.

  - Patch ports never carry an iface-id, so their ofport changes also hit
    the "unhandled" branch of binding_handle_ovs_interface_changes(),
    forcing a full recompute of runtime_data and, transitively, of
    lflow_output again.

This is hit on the datapath every time a bridged / L2 logical port is
bound or moved between chassis, since the peer patch port is (re)created
and gets a new ofport.  In deployments with many logical flows each
recompute can take several seconds, dominating the port's dataplane
downtime.

Fix both edges:

  - Move patch ofports into their own engine node (en_patch_port_data)
    that feeds only the physical flow output.  en_non_vif_data keeps
    feeding en_lflow_output, but now only changes on tunnel ofport
    changes.

  - Recognize patch-type OVS interfaces in
    binding_handle_ovs_interface_changes() and skip them instead of
    treating them as unhandled: their ofport is already tracked by
    en_patch_port_data.

Tunnel (geneve/vxlan) interfaces are deliberately left alone in the
latter fix.  runtime_data's 'active_tunnels' (BFD-derived reachability,
used for ECMP / gateway-chassis flow selection) has no incremental
tracking and is only recalculated on a full recompute; skipping tunnel
ofport changes there would silently stop refreshing it on BFD status
changes.

Recognizing patch ports in binding_handle_ovs_interface_changes() also
exposes a pre-existing gap: unlike binding_handle_port_binding_changes(),
it never re-scans a newly-local datapath's sibling ports (localnet /
external / vtep / multichassis) after calling add_local_datapath().  This
used to be masked by the very recompute this patch removes.  Extract the
existing catch-up logic into catch_up_new_local_datapaths() and call it
from both incremental handlers.

Assisted-by: Claude Opus 4.8, Claude Code
Signed-off-by: Jun Gu <[email protected]>
---
 controller/binding.c             | 122 +++++++++++++++------
 controller/local_data.c          | 180 ++++++++++++++++++++-----------
 controller/local_data.h          |  22 ++--
 controller/ovn-controller.c      |  94 +++++++++++++---
 tests/ovn-controller.at          |  81 ++++++++++++++
 tests/ovn-inc-proc-graph-dump.at |   5 +
 6 files changed, 387 insertions(+), 117 deletions(-)

diff --git a/controller/binding.c b/controller/binding.c
index de51be823..a91193aa0 100644
--- a/controller/binding.c
+++ b/controller/binding.c
@@ -2706,6 +2706,30 @@ is_iface_vif(const struct ovsrec_interface *iface_rec)
     return true;
 }
 
+/* Patch (localnet / L2 gateway) ports are OVS interfaces that
+ * ovn-controller itself creates and manages.  They never carry an iface-id
+ * and are of no interest to port binding processing: their ofport changes
+ * are already tracked by the patch_port_data engine node.  Recognizing them
+ * here lets binding_handle_ovs_interface_changes() skip them instead of
+ * treating them as an unhandled change and forcing a full recompute of
+ * runtime_data (and, transitively, of lflow_output) on every patch port
+ * bind / migration.
+ *
+ * Tunnel (geneve/vxlan) interfaces are deliberately NOT included here even
+ * though their ofport changes are tracked by the non_vif_data engine node:
+ * runtime_data's 'active_tunnels' (BFD-derived tunnel reachability, used by
+ * ECMP/gateway-chassis logical flow selection) is only ever recalculated
+ * during a full recompute of runtime_data, and currently has no incremental
+ * tracking of its own (see the engine dependency table above
+ * en_runtime_data_run()).  Treating tunnel interface changes as "handled"
+ * here would silently stop refreshing 'active_tunnels' on BFD status
+ * changes. */
+static bool
+is_iface_patch(const struct ovsrec_interface *iface_rec)
+{
+    return iface_rec->type && !strcmp(iface_rec->type, "patch");
+}
+
 bool
 is_iface_in_int_bridge(const struct ovsrec_interface *iface,
                        const struct ovsrec_bridge *br_int)
@@ -2786,6 +2810,58 @@ ovs_interface_change_need_handle(const struct 
ovsrec_interface *iface_rec,
     return false;
 }
 
+/* A datapath can become local (added to 'b_ctx_out->local_datapaths' and
+ * marked TRACKED_RESOURCE_NEW in 'b_ctx_out->tracked_dp_bindings') from more
+ * than one incremental handler: e.g. a VIF getting claimed by
+ * binding_handle_ovs_interface_changes(), or a port_binding update handled
+ * by binding_handle_port_binding_changes().  'tracked_dp_bindings' is reset
+ * on every engine iteration, but sibling ports of the newly-local datapath
+ * (localnet/external/vtep/multichassis) may already be present in the SB
+ * IDL and therefore never show up again as their own tracked change.  So
+ * every incremental handler that can call add_local_datapath() for a
+ * previously-unknown datapath must run this catch-up scan afterwards,
+ * otherwise those sibling ports are silently never registered until the
+ * next full recompute. */
+static void
+catch_up_new_local_datapaths(struct binding_ctx_in *b_ctx_in,
+                             struct binding_ctx_out *b_ctx_out)
+{
+    struct shash bridge_mappings = SHASH_INITIALIZER(&bridge_mappings);
+    add_ovs_bridge_mappings(b_ctx_in->ovs_table, b_ctx_in->bridge_table,
+                            &bridge_mappings);
+
+    struct tracked_datapath *t_dp;
+    HMAP_FOR_EACH (t_dp, node, b_ctx_out->tracked_dp_bindings) {
+        if (t_dp->tracked_type != TRACKED_RESOURCE_NEW) {
+            continue;
+        }
+        struct sbrec_port_binding *target =
+            sbrec_port_binding_index_init_row(
+                b_ctx_in->sbrec_port_binding_by_datapath);
+        sbrec_port_binding_index_set_datapath(target, t_dp->dp);
+
+        const struct sbrec_port_binding *pb;
+        SBREC_PORT_BINDING_FOR_EACH_EQUAL (pb, target,
+            b_ctx_in->sbrec_port_binding_by_datapath) {
+            enum en_lport_type lport_type = get_lport_type(pb);
+            if (lport_type == LP_LOCALNET) {
+                consider_localnet_lport(pb, b_ctx_out);
+                update_ld_localnet_port(pb, &bridge_mappings,
+                                        b_ctx_out->local_datapaths);
+            } else if (lport_type == LP_EXTERNAL) {
+                update_ld_external_ports(pb, b_ctx_out->local_datapaths);
+            } else if (lport_type == LP_VTEP) {
+                update_ld_vtep_port(pb, b_ctx_out->local_datapaths);
+            } else if (pb->n_additional_chassis) {
+                update_ld_multichassis_ports(pb, b_ctx_out->local_datapaths);
+            }
+        }
+        sbrec_port_binding_index_destroy_row(target);
+    }
+
+    shash_destroy(&bridge_mappings);
+}
+
 /* Returns true if the ovs interface changes were handled successfully,
  * false otherwise.
  */
@@ -2824,6 +2900,10 @@ binding_handle_ovs_interface_changes(struct 
binding_ctx_in *b_ctx_in,
         const char *old_iface_id = smap_get(b_ctx_out->local_iface_ids,
                                             iface_rec->name);
         if (!iface_id && !old_iface_id && !is_iface_vif(iface_rec)) {
+            if (is_iface_patch(iface_rec)) {
+                /* Not a port binding concern, handled elsewhere. */
+                continue;
+            }
             /* Right now we are not handling ovs_interface changes if the
              * interface doesn't have iface-id or didn't have it
              * previously. */
@@ -2904,6 +2984,12 @@ binding_handle_ovs_interface_changes(struct 
binding_ctx_in *b_ctx_in,
         }
     }
 
+    if (handled) {
+        /* consider_iface_claim() above may have called add_local_datapath()
+         * for a datapath that was not local before. */
+        catch_up_new_local_datapaths(b_ctx_in, b_ctx_out);
+    }
+
     return handled;
 }
 
@@ -3471,41 +3557,7 @@ delete_done:
         /* There may be new local datapaths added by the above handling, so go
          * through each port_binding of newly added local datapaths to update
          * related local_datapaths if needed. */
-        struct shash bridge_mappings =
-            SHASH_INITIALIZER(&bridge_mappings);
-        add_ovs_bridge_mappings(b_ctx_in->ovs_table,
-                                b_ctx_in->bridge_table,
-                                &bridge_mappings);
-        struct tracked_datapath *t_dp;
-        HMAP_FOR_EACH (t_dp, node, b_ctx_out->tracked_dp_bindings) {
-            if (t_dp->tracked_type != TRACKED_RESOURCE_NEW) {
-                continue;
-            }
-            struct sbrec_port_binding *target =
-                sbrec_port_binding_index_init_row(
-                    b_ctx_in->sbrec_port_binding_by_datapath);
-            sbrec_port_binding_index_set_datapath(target, t_dp->dp);
-
-            SBREC_PORT_BINDING_FOR_EACH_EQUAL (pb, target,
-                b_ctx_in->sbrec_port_binding_by_datapath) {
-                enum en_lport_type lport_type = get_lport_type(pb);
-                if (lport_type == LP_LOCALNET) {
-                    consider_localnet_lport(pb, b_ctx_out);
-                    update_ld_localnet_port(pb, &bridge_mappings,
-                                            b_ctx_out->local_datapaths);
-                } else if (lport_type == LP_EXTERNAL) {
-                    update_ld_external_ports(pb, b_ctx_out->local_datapaths);
-                } else if (lport_type == LP_VTEP) {
-                    update_ld_vtep_port(pb, b_ctx_out->local_datapaths);
-                } else if (pb->n_additional_chassis) {
-                    update_ld_multichassis_ports(pb,
-                                                 b_ctx_out->local_datapaths);
-                }
-            }
-            sbrec_port_binding_index_destroy_row(target);
-        }
-
-        shash_destroy(&bridge_mappings);
+        catch_up_new_local_datapaths(b_ctx_in, b_ctx_out);
     }
 
     return handled;
diff --git a/controller/local_data.c b/controller/local_data.c
index af6c75b40..451eb9d35 100644
--- a/controller/local_data.c
+++ b/controller/local_data.c
@@ -452,14 +452,57 @@ tracked_datapaths_destroy(struct hmap *tracked_datapaths)
     hmap_destroy(tracked_datapaths);
 }
 
-/* Iterates the br_int ports and build the simap of patch to ofports
- * and chassis tunnels. */
+/* Iterates the br_int ports and builds the simap of patch port to ofport. */
 void
-local_nonvif_data_run(const struct ovsrec_bridge *br_int,
-                      const struct sbrec_chassis *chassis_rec,
-                      struct simap *patch_ofports,
-                      struct hmap *chassis_tunnels,
-                      struct flow_based_tunnel *flow_tunnels)
+local_patch_ports_run(const struct ovsrec_bridge *br_int,
+                      struct simap *patch_ofports)
+{
+    for (size_t i = 0; i < br_int->n_ports; i++) {
+        const struct ovsrec_port *port_rec = br_int->ports[i];
+        if (!strcmp(port_rec->name, br_int->name)) {
+            continue;
+        }
+
+        const char *localnet = smap_get(&port_rec->external_ids,
+                                        "ovn-localnet-port");
+        const char *l2gateway = smap_get(&port_rec->external_ids,
+                                        "ovn-l2gateway-port");
+        if (!localnet && !l2gateway) {
+            continue;
+        }
+
+        for (size_t j = 0; j < port_rec->n_interfaces; j++) {
+            const struct ovsrec_interface *iface_rec = port_rec->interfaces[j];
+
+            /* Get OpenFlow port number. */
+            if (!iface_rec->n_ofport) {
+                continue;
+            }
+            int64_t ofport = iface_rec->ofport[0];
+            if (ofport < 1 || ofport > ofp_to_u16(OFPP_MAX)) {
+                continue;
+            }
+
+            if (strcmp(iface_rec->type, "patch")) {
+                continue;
+            }
+            if (localnet) {
+                simap_put(patch_ofports, localnet, ofport);
+                break;
+            } else if (l2gateway) {
+                /* L2 gateway patch ports can be handled just like VIFs. */
+                simap_put(patch_ofports, l2gateway, ofport);
+                break;
+            }
+        }
+    }
+}
+
+void
+local_tunnels_run(const struct ovsrec_bridge *br_int,
+                  const struct sbrec_chassis *chassis_rec,
+                  struct hmap *chassis_tunnels,
+                  struct flow_based_tunnel *flow_tunnels)
 {
     for (int i = 0; i < br_int->n_ports; i++) {
         const struct ovsrec_port *port_rec = br_int->ports[i];
@@ -477,10 +520,9 @@ local_nonvif_data_run(const struct ovsrec_bridge *br_int,
 
         track_flow_based_tunnel(port_rec, chassis_rec, flow_tunnels);
 
-        const char *localnet = smap_get(&port_rec->external_ids,
-                                        "ovn-localnet-port");
-        const char *l2gateway = smap_get(&port_rec->external_ids,
-                                        "ovn-l2gateway-port");
+        if (!tunnel_id) {
+            continue;
+        }
 
         for (int j = 0; j < port_rec->n_interfaces; j++) {
             const struct ovsrec_interface *iface_rec = port_rec->interfaces[j];
@@ -494,67 +536,63 @@ local_nonvif_data_run(const struct ovsrec_bridge *br_int,
                 continue;
             }
 
-            bool is_patch = !strcmp(iface_rec->type, "patch");
-            if (is_patch && localnet) {
-                simap_put(patch_ofports, localnet, ofport);
-                break;
-            } else if (is_patch && l2gateway) {
-                /* L2 gateway patch ports can be handled just like VIFs. */
-                simap_put(patch_ofports, l2gateway, ofport);
-                break;
-            } else if (tunnel_id) {
-                enum chassis_tunnel_type tunnel_type;
-                if (!strcmp(iface_rec->type, "geneve")) {
-                    tunnel_type = GENEVE;
-                } else if (!strcmp(iface_rec->type, "vxlan")) {
-                    tunnel_type = VXLAN;
-                } else {
-                    continue;
-                }
-
-                /* We split the tunnel_id to get the chassis-id
-                 * and hash the tunnel list on the chassis-id. The
-                 * reason to use the chassis-id alone is because
-                 * there might be cases (multicast, gateway chassis)
-                 * where we need to tunnel to the chassis, but won't
-                 * have the encap-ip specifically.
-                 */
-                char *hash_id = NULL;
-                char *ip = NULL;
-
-                if (!encaps_tunnel_id_parse(tunnel_id, &hash_id, &ip, NULL)) {
-                    continue;
-                }
-                struct chassis_tunnel *tun = xmalloc(sizeof *tun);
-                hmap_insert(chassis_tunnels, &tun->hmap_node,
-                            hash_string(hash_id, 0));
-                tun->chassis_id = xstrdup(tunnel_id);
-                tun->ofport = u16_to_ofp(ofport);
-                tun->type = tunnel_type;
-                tun->is_ipv6 = ip ? addr_is_ipv6(ip) : false;
-                tun->is_ramp_tunnel = is_ramp_tunnel(&iface_rec->other_config);
-
-                free(hash_id);
-                free(ip);
-                break;
+            enum chassis_tunnel_type tunnel_type;
+            if (!strcmp(iface_rec->type, "geneve")) {
+                tunnel_type = GENEVE;
+            } else if (!strcmp(iface_rec->type, "vxlan")) {
+                tunnel_type = VXLAN;
+            } else {
+                continue;
+            }
+
+            /* We split the tunnel_id to get the chassis-id
+             * and hash the tunnel list on the chassis-id. The
+             * reason to use the chassis-id alone is because
+             * there might be cases (multicast, gateway chassis)
+             * where we need to tunnel to the chassis, but won't
+             * have the encap-ip specifically.
+             */
+            char *hash_id = NULL;
+            char *ip = NULL;
+
+            if (!encaps_tunnel_id_parse(tunnel_id, &hash_id, &ip, NULL)) {
+                continue;
             }
+            struct chassis_tunnel *tun = xmalloc(sizeof *tun);
+            hmap_insert(chassis_tunnels, &tun->hmap_node,
+                        hash_string(hash_id, 0));
+            tun->chassis_id = xstrdup(tunnel_id);
+            tun->ofport = u16_to_ofp(ofport);
+            tun->type = tunnel_type;
+            tun->is_ipv6 = ip ? addr_is_ipv6(ip) : false;
+            tun->is_ramp_tunnel = is_ramp_tunnel(&iface_rec->other_config);
+
+            free(hash_id);
+            free(ip);
+            break;
         }
     }
 }
 
-bool
-local_nonvif_data_handle_ovs_iface_changes(
-    const struct ovsrec_interface_table *iface_table)
+/* Returns false (i.e. a recompute is required) if any tracked OVS interface of
+ * one of the given 'types' had its ofport added, removed or changed. */
+static bool
+nonvif_iface_ofport_unchanged(const struct ovsrec_interface_table *iface_table,
+                              const char *const *types, size_t n_types)
 {
     const struct ovsrec_interface *iface_rec;
     OVSREC_INTERFACE_TABLE_FOR_EACH_TRACKED (iface_rec, iface_table) {
-        /* Check only patch ports or tunnels. */
-        if (strcmp(iface_rec->type, "geneve") &&
-            strcmp(iface_rec->type, "patch") &&
-            strcmp(iface_rec->type, "vxlan")) {
+        bool type_match = false;
+        for (size_t i = 0; i < n_types; i++) {
+            if (!strcmp(iface_rec->type, types[i])) {
+                type_match = true;
+                break;
+            }
+        }
+        if (!type_match) {
             continue;
         }
-        /* We are interested only in ofport changes for this handler. */
+        /* We are interested only in ofport changes for these handlers. */
         if (ovsrec_interface_is_new(iface_rec) ||
             ovsrec_interface_is_deleted(iface_rec) ||
             ovsrec_interface_is_updated(iface_rec,
@@ -566,6 +604,24 @@ local_nonvif_data_handle_ovs_iface_changes(
     return true;
 }
 
+bool
+local_patch_ports_handle_ovs_iface_changes(
+    const struct ovsrec_interface_table *iface_table)
+{
+    static const char *const types[] = { "patch" };
+    return nonvif_iface_ofport_unchanged(iface_table, types,
+                                         ARRAY_SIZE(types));
+}
+
+bool
+local_tunnels_handle_ovs_iface_changes(
+    const struct ovsrec_interface_table *iface_table)
+{
+    static const char *const types[] = { "geneve", "vxlan" };
+    return nonvif_iface_ofport_unchanged(iface_table, types,
+                                         ARRAY_SIZE(types));
+}
+
 bool
 get_chassis_tunnel_ofport(const struct hmap *chassis_tunnels,
                           const char *chassis_name, ofp_port_t *ofport)
diff --git a/controller/local_data.h b/controller/local_data.h
index cbb8899eb..2e0fee0ca 100644
--- a/controller/local_data.h
+++ b/controller/local_data.h
@@ -159,13 +159,23 @@ struct flow_based_tunnel {
 };
 
 
-void local_nonvif_data_run(const struct ovsrec_bridge *br_int,
-                           const struct sbrec_chassis *chassis,
-                           struct simap *patch_ofports,
-                           struct hmap *chassis_tunnels,
-                           struct flow_based_tunnel *flow_tunnels);
+/* Patch (localnet / L2 gateway) OVS ports.  These are consumed only by the
+ * physical flow output, so they are tracked in their own engine node to avoid
+ * invalidating the logical flow output on every patch port change. */
+void local_patch_ports_run(const struct ovsrec_bridge *br_int,
+                           struct simap *patch_ofports);
 
-bool local_nonvif_data_handle_ovs_iface_changes(
+bool local_patch_ports_handle_ovs_iface_changes(
+    const struct ovsrec_interface_table *);
+
+/* Tunnel OVS ports and the related chassis information.  These are consumed by
+ * both the physical and the logical flow output. */
+void local_tunnels_run(const struct ovsrec_bridge *br_int,
+                       const struct sbrec_chassis *chassis,
+                       struct hmap *chassis_tunnels,
+                       struct flow_based_tunnel *flow_tunnels);
+
+bool local_tunnels_handle_ovs_iface_changes(
     const struct ovsrec_interface_table *);
 
 struct chassis_tunnel *chassis_tunnel_find(const struct hmap *chassis_tunnels,
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index 552e87b53..f4b60f0e8 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -3637,12 +3637,72 @@ en_dns_cache_cleanup(void *data OVS_UNUSED)
 }
 
 
-/* Engine node which is used to handle the Non VIF data like
- *   - OVS patch ports
- *   - Tunnel ports and the related chassis information.
+/* Engine node which handles the OVS patch ports (localnet / L2 gateway).
+ *
+ * Kept separate from the tunnel data (en_non_vif_data) because patch ofports
+ * are consumed only by the physical flow output.  Bundling them with the
+ * tunnel data would force a full recompute of the logical flow output (which
+ * has no handler for its non_vif_data input) on every patch port change, e.g.
+ * whenever a bridged/L2 logical port is bound or moved between chassis.
  */
-struct ed_type_non_vif_data {
+struct ed_type_patch_port_data {
     struct simap patch_ofports; /* simap of patch ovs ports. */
+};
+
+static void *
+en_patch_port_data_init(struct engine_node *node OVS_UNUSED,
+                        struct engine_arg *arg OVS_UNUSED)
+{
+    struct ed_type_patch_port_data *data = xzalloc(sizeof *data);
+    simap_init(&data->patch_ofports);
+    return data;
+}
+
+static void
+en_patch_port_data_cleanup(void *data OVS_UNUSED)
+{
+    struct ed_type_patch_port_data *ed_patch_port_data = data;
+    simap_destroy(&ed_patch_port_data->patch_ofports);
+}
+
+static enum engine_node_state
+en_patch_port_data_run(struct engine_node *node, void *data)
+{
+    struct ed_type_patch_port_data *ed_patch_port_data = data;
+    simap_destroy(&ed_patch_port_data->patch_ofports);
+    simap_init(&ed_patch_port_data->patch_ofports);
+
+    const struct ovsrec_open_vswitch_table *ovs_table =
+        EN_OVSDB_GET(engine_get_input("OVS_open_vswitch", node));
+    const struct ovsrec_bridge_table *bridge_table =
+        EN_OVSDB_GET(engine_get_input("OVS_bridge", node));
+
+    const struct ovsrec_bridge *br_int = get_br_int(bridge_table, ovs_table);
+    ovs_assert(br_int);
+
+    local_patch_ports_run(br_int, &ed_patch_port_data->patch_ofports);
+
+    return EN_UPDATED;
+}
+
+static enum engine_input_handler_result
+patch_port_data_ovs_iface_handler(struct engine_node *node,
+                                  void *data OVS_UNUSED)
+{
+    const struct ovsrec_interface_table *iface_table =
+        EN_OVSDB_GET(engine_get_input("OVS_interface", node));
+
+    if (local_patch_ports_handle_ovs_iface_changes(iface_table)) {
+        return EN_HANDLED_UNCHANGED;
+    } else {
+        return EN_UNHANDLED;
+    }
+}
+
+/* Engine node which handles the tunnel ports and the related chassis
+ * information.  Consumed by both the physical and the logical flow output.
+ */
+struct ed_type_non_vif_data {
     struct hmap chassis_tunnels; /* hmap of 'struct chassis_tunnel' from the
                                   * tunnel OVS ports. */
     struct flow_based_tunnel flow_tunnels[TUNNEL_TYPE_MAX];
@@ -3656,7 +3716,6 @@ en_non_vif_data_init(struct engine_node *node OVS_UNUSED,
                      struct engine_arg *arg OVS_UNUSED)
 {
     struct ed_type_non_vif_data *data = xzalloc(sizeof *data);
-    simap_init(&data->patch_ofports);
     hmap_init(&data->chassis_tunnels);
     flow_based_tunnels_init(data->flow_tunnels);
     data->use_flow_based_tunnels = false;
@@ -3667,7 +3726,6 @@ static void
 en_non_vif_data_cleanup(void *data OVS_UNUSED)
 {
     struct ed_type_non_vif_data *ed_non_vif_data = data;
-    simap_destroy(&ed_non_vif_data->patch_ofports);
     chassis_tunnels_destroy(&ed_non_vif_data->chassis_tunnels);
     flow_based_tunnels_destroy(ed_non_vif_data->flow_tunnels);
 }
@@ -3676,11 +3734,9 @@ static enum engine_node_state
 en_non_vif_data_run(struct engine_node *node, void *data)
 {
     struct ed_type_non_vif_data *ed_non_vif_data = data;
-    simap_destroy(&ed_non_vif_data->patch_ofports);
     chassis_tunnels_destroy(&ed_non_vif_data->chassis_tunnels);
     flow_based_tunnels_destroy(ed_non_vif_data->flow_tunnels);
 
-    simap_init(&ed_non_vif_data->patch_ofports);
     hmap_init(&ed_non_vif_data->chassis_tunnels);
     flow_based_tunnels_init(ed_non_vif_data->flow_tunnels);
 
@@ -3705,10 +3761,9 @@ en_non_vif_data_run(struct engine_node *node, void *data)
     ed_non_vif_data->use_flow_based_tunnels =
         is_flow_based_tunnels_enabled(ovs_table, chassis);
 
-    local_nonvif_data_run(br_int, chassis,
-                          &ed_non_vif_data->patch_ofports,
-                          &ed_non_vif_data->chassis_tunnels,
-                          ed_non_vif_data->flow_tunnels);
+    local_tunnels_run(br_int, chassis,
+                      &ed_non_vif_data->chassis_tunnels,
+                      ed_non_vif_data->flow_tunnels);
 
     return EN_UPDATED;
 }
@@ -3719,7 +3774,7 @@ non_vif_data_ovs_iface_handler(struct engine_node *node, 
void *data OVS_UNUSED)
     const struct ovsrec_interface_table *iface_table =
         EN_OVSDB_GET(engine_get_input("OVS_interface", node));
 
-    if (local_nonvif_data_handle_ovs_iface_changes(iface_table)) {
+    if (local_tunnels_handle_ovs_iface_changes(iface_table)) {
         return EN_HANDLED_UNCHANGED;
     } else {
         return EN_UNHANDLED;
@@ -4740,6 +4795,9 @@ static void init_physical_ctx(struct engine_node *node,
     const struct ed_type_mff_ovn_geneve *ed_mff_ovn_geneve =
         engine_get_input_data("mff_ovn_geneve", node);
 
+    struct ed_type_patch_port_data *patch_port_data =
+        engine_get_input_data("patch_port_data", node);
+
     const struct ovsrec_interface_table *ovs_interface_table =
         EN_OVSDB_GET(engine_get_input("if_status_mgr", node));
 
@@ -4791,7 +4849,7 @@ static void init_physical_ctx(struct engine_node *node,
     p_ctx->ct_zones = ct_zones;
     p_ctx->mff_ovn_geneve = ed_mff_ovn_geneve->mff_ovn_geneve;
     p_ctx->local_bindings = &rt_data->lbinding_data.bindings;
-    p_ctx->patch_ofports = &non_vif_data->patch_ofports;
+    p_ctx->patch_ofports = &patch_port_data->patch_ofports;
     p_ctx->chassis_tunnels = &non_vif_data->chassis_tunnels;
     p_ctx->flow_tunnels = non_vif_data->flow_tunnels;
     p_ctx->use_flow_based_tunnels = non_vif_data->use_flow_based_tunnels;
@@ -6922,6 +6980,7 @@ static ENGINE_NODE(template_vars, CLEAR_TRACKED_DATA);
 static ENGINE_NODE(ct_zones, CLEAR_TRACKED_DATA, IS_VALID);
 static ENGINE_NODE(ovs_interface_shadow, CLEAR_TRACKED_DATA);
 static ENGINE_NODE(runtime_data, CLEAR_TRACKED_DATA, SB_WRITE);
+static ENGINE_NODE(patch_port_data);
 static ENGINE_NODE(non_vif_data);
 static ENGINE_NODE(mff_ovn_geneve);
 static ENGINE_NODE(ofctrl_is_connected);
@@ -7015,6 +7074,11 @@ inc_proc_ovn_controller_init(
     engine_add_input(&en_port_groups, &en_runtime_data,
                      port_groups_runtime_data_handler);
 
+    engine_add_input(&en_patch_port_data, &en_ovs_open_vswitch, NULL);
+    engine_add_input(&en_patch_port_data, &en_ovs_bridge, NULL);
+    engine_add_input(&en_patch_port_data, &en_ovs_interface,
+                     patch_port_data_ovs_iface_handler);
+
     engine_add_input(&en_non_vif_data, &en_ovs_open_vswitch, NULL);
     engine_add_input(&en_non_vif_data, &en_ovs_bridge, NULL);
     engine_add_input(&en_non_vif_data, &en_sb_chassis, NULL);
@@ -7030,6 +7094,8 @@ inc_proc_ovn_controller_init(
     /* Note: The order of inputs is important, all OVS interface changes must
      * be handled before any ct_zone changes.
      */
+    engine_add_input(&en_pflow_output, &en_patch_port_data,
+                     NULL);
     engine_add_input(&en_pflow_output, &en_non_vif_data,
                      NULL);
     engine_add_input(&en_pflow_output, &en_northd_options, NULL);
diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
index e17ebea76..7459db3e9 100644
--- a/tests/ovn-controller.at
+++ b/tests/ovn-controller.at
@@ -1120,6 +1120,87 @@ OVN_CLEANUP([hv1])
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn-controller - patch and tunnel ofports tracked separately])
+AT_KEYWORDS([ovn-patch-port-data])
+
+ovn_start
+net_add n1
+
+sim_add hv1
+as hv1
+check ovs-vsctl add-br br-phys
+check ovs-vsctl add-br br-eth0
+check ovs-vsctl set open . external-ids:ovn-bridge-mappings=physnet1:br-eth0
+ovn_attach n1 br-phys 192.168.0.1
+
+sim_add hv2
+as hv2
+check ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+
+check ovn-nbctl ls-add ls0 \
+    -- lsp-add ls0 ln0 \
+    -- lsp-set-type ln0 localnet \
+    -- lsp-set-addresses ln0 unknown \
+    -- lsp-set-options ln0 network_name=physnet1 \
+    -- lsp-add ls0 lsp0 \
+    -- lsp-set-addresses lsp0 "00:00:00:00:00:01 10.0.0.1"
+
+as hv1
+check ovs-vsctl add-port br-int vif0 \
+    -- set Interface vif0 external_ids:iface-id=lsp0
+
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+# The localnet port of a locally bound datapath gets its OVS patch ports and
+# the remote chassis gets its tunnel port.  Wait for both to settle before
+# looking at the engine statistics.
+OVS_WAIT_UNTIL([test 1 -le $(ovs-vsctl get Interface patch-br-int-to-ln0 \
+                             ofport)])
+OVS_WAIT_UNTIL([test 1 -le $(ovs-vsctl get Interface ovn-hv2-0 ofport)])
+
+# A patch ofport change must be handled by en_patch_port_data only.  The tunnel
+# data (en_non_vif_data), which is also consumed by the logical flow output,
+# must not be invalidated by it.
+check as hv1 ovn-appctl -t ovn-controller inc-engine/clear-stats
+check ovs-vsctl set Interface patch-br-int-to-ln0 ofport_request=4242
+OVS_WAIT_UNTIL([test 4242 = $(ovs-vsctl get Interface patch-br-int-to-ln0 \
+                              ofport)])
+OVS_WAIT_UNTIL([test 1 -le $(ovs-ofctl dump-flows br-int | \
+                             grep -c 'in_port=4242')])
+check_controller_engine_stats hv1 patch_port_data recompute nocompute
+check_controller_engine_stats hv1 non_vif_data norecompute compute
+check_controller_engine_stats hv1 pflow_output recompute nocompute
+check_controller_engine_stats hv1 runtime_data norecompute compute
+check_controller_engine_stats hv1 lflow_output norecompute nocompute
+
+# Conversely, a tunnel ofport change must be handled by en_non_vif_data only
+# and must leave en_patch_port_data alone.  Tunnel interfaces are
+# deliberately NOT recognized by binding_handle_ovs_interface_changes() (see
+# is_iface_patch()), because runtime_data's 'active_tunnels' (BFD-derived
+# reachability, used by ECMP/gateway-chassis logical flow selection) has no
+# incremental tracking of its own and is only ever recalculated on a full
+# recompute of runtime_data.  So a tunnel ofport change is still expected to
+# force a full recompute of both runtime_data and (transitively, since
+# en_lflow_output has no change handler for its en_non_vif_data input)
+# lflow_output.
+check as hv1 ovn-appctl -t ovn-controller inc-engine/clear-stats
+check ovs-vsctl set Interface ovn-hv2-0 ofport_request=4243
+OVS_WAIT_UNTIL([test 4243 = $(ovs-vsctl get Interface ovn-hv2-0 ofport)])
+OVS_WAIT_UNTIL([test 1 -le $(ovs-ofctl dump-flows br-int | \
+                             grep -c 'in_port=4243')])
+check_controller_engine_stats hv1 non_vif_data recompute nocompute
+check_controller_engine_stats hv1 patch_port_data norecompute compute
+check_controller_engine_stats hv1 pflow_output recompute nocompute
+check_controller_engine_stats hv1 runtime_data recompute nocompute
+check_controller_engine_stats hv1 lflow_output recompute nocompute
+
+OVN_CLEANUP([hv1], [hv2])
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([ovn-controller - localnet port change and chassisredirect bridged 
redirect])
 AT_KEYWORDS([ovn-localnet-cr-bridged])
diff --git a/tests/ovn-inc-proc-graph-dump.at b/tests/ovn-inc-proc-graph-dump.at
index 269766ded..b53ae3988 100644
--- a/tests/ovn-inc-proc-graph-dump.at
+++ b/tests/ovn-inc-proc-graph-dump.at
@@ -373,6 +373,10 @@ digraph "Incremental-Processing-Engine" {
        lb_data -> lflow_output [[label="lflow_output_lb_data_handler"]];
        SB_fdb -> lflow_output [[label="lflow_output_sb_fdb_handler"]];
        SB_meter -> lflow_output [[label="lflow_output_sb_meter_handler"]];
+       patch_port_data [[style=filled, shape=box, fillcolor=white, 
label="patch_port_data"]];
+       OVS_open_vswitch -> patch_port_data [[label=""]];
+       OVS_bridge -> patch_port_data [[label=""]];
+       OVS_interface -> patch_port_data 
[[label="patch_port_data_ovs_iface_handler"]];
        SB_sb_global [[style=filled, shape=box, fillcolor=white, 
label="SB_sb_global"]];
        northd_options [[style=filled, shape=box, fillcolor=white, 
label="northd_options"]];
        SB_sb_global -> northd_options 
[[label="en_northd_options_sb_sb_global_handler"]];
@@ -419,6 +423,7 @@ digraph "Incremental-Processing-Engine" {
        neighbor_exchange -> evpn_arp [[label=""]];
        evpn_vtep_binding -> evpn_arp [[label="evpn_arp_vtep_binding_handler"]];
        pflow_output [[style=filled, shape=box, fillcolor=white, 
label="pflow_output"]];
+       patch_port_data -> pflow_output [[label=""]];
        non_vif_data -> pflow_output [[label=""]];
        northd_options -> pflow_output [[label=""]];
        ct_zones -> pflow_output [[label="pflow_output_ct_zones_handler"]];
-- 
2.34.1

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

Reply via email to