Replace the en_dp_enum engine node with a new en_tunnel_key node that is the
single owner of IC-SB Datapath_Binding creation, tunnel-key allocation,
VXLAN-range refresh and garbage collection, for both transit switches and
transit routers.

en_dp_enum kept a datapath map and tunnel-key allocator that en_ts and en_tr
mutated (cloning it first, and still risking dangling pointers when they
deleted bindings it referenced).  Now en_ts and en_tr build their own
transit-switch/router datapath maps each run and only maintain the AZ NB
mirrors; concentrating tunnel-key allocation in en_tunnel_key keeps the keys
globally unique across both datapath types without any node mutating another
node's data.

This addresses the review of the en_dp_enum node.

Signed-off-by: Paulo Guilherme Silva <[email protected]>
---
 ic/automake.mk     |   4 +-
 ic/en-dp-enum.c    |  68 --------
 ic/en-dp-enum.h    |  30 ----
 ic/en-tr.c         |  86 ++++++++-
 ic/en-ts.c         |  34 +++-
 ic/en-tunnel-key.c | 426 +++++++++++++++++++++++++++++++++++++++++++++
 ic/en-tunnel-key.h |  46 +++++
 ic/inc-proc-ic.c   |  64 ++++---
 ic/ovn-ic.c        | 341 ++++++++++++++----------------------
 ic/ovn-ic.h        |  36 +++-
 10 files changed, 786 insertions(+), 349 deletions(-)
 delete mode 100644 ic/en-dp-enum.c
 delete mode 100644 ic/en-dp-enum.h
 create mode 100644 ic/en-tunnel-key.c
 create mode 100644 ic/en-tunnel-key.h

diff --git a/ic/automake.mk b/ic/automake.mk
index b730fc578..82338d5dd 100644
--- a/ic/automake.mk
+++ b/ic/automake.mk
@@ -6,14 +6,14 @@ ic_ovn_ic_SOURCES = ic/ovn-ic.c \
        ic/en-ic.h \
        ic/en-az.c \
        ic/en-az.h \
-       ic/en-dp-enum.c \
-       ic/en-dp-enum.h \
        ic/en-gateway.c \
        ic/en-gateway.h \
        ic/en-ts.c \
        ic/en-ts.h \
        ic/en-tr.c \
        ic/en-tr.h \
+       ic/en-tunnel-key.c \
+       ic/en-tunnel-key.h \
        ic/en-port-binding.c \
        ic/en-port-binding.h \
        ic/en-route.c \
diff --git a/ic/en-dp-enum.c b/ic/en-dp-enum.c
deleted file mode 100644
index fd447ab52..000000000
--- a/ic/en-dp-enum.c
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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-dp-enum.h"
-#include "lib/inc-proc-eng.h"
-#include "lib/ovn-util.h"
-#include "openvswitch/hmap.h"
-#include "openvswitch/shash.h"
-#include "openvswitch/vlog.h"
-#include "ovn-ic.h"
-
-VLOG_DEFINE_THIS_MODULE(en_ic_dp_enum);
-
-enum engine_node_state
-en_dp_enum_run(struct engine_node *node OVS_UNUSED, void *data_)
-{
-    const struct engine_context *eng_ctx = engine_get_context();
-    struct ic_context *ctx = eng_ctx->client_ctx;
-    struct ed_type_dp_enum *data = data_;
-
-    /* Clear the previous iteration's state and rebuild from the current IC-SB
-     * datapath bindings. */
-    ovn_destroy_tnlids(&data->dp_tnlids);
-    hmap_init(&data->dp_tnlids);
-    shash_clear(&data->isb_ts_dps);
-    shash_clear(&data->isb_tr_dps);
-
-    enumerate_datapaths(ctx, &data->dp_tnlids, &data->isb_ts_dps,
-                        &data->isb_tr_dps);
-
-    return EN_UPDATED;
-}
-
-void *
-en_dp_enum_init(struct engine_node *node OVS_UNUSED,
-                struct engine_arg *arg OVS_UNUSED)
-{
-    struct ed_type_dp_enum *data = xzalloc(sizeof *data);
-
-    hmap_init(&data->dp_tnlids);
-    shash_init(&data->isb_ts_dps);
-    shash_init(&data->isb_tr_dps);
-
-    return data;
-}
-
-void
-en_dp_enum_cleanup(void *data_)
-{
-    struct ed_type_dp_enum *data = data_;
-
-    ovn_destroy_tnlids(&data->dp_tnlids);
-    shash_destroy(&data->isb_ts_dps);
-    shash_destroy(&data->isb_tr_dps);
-}
diff --git a/ic/en-dp-enum.h b/ic/en-dp-enum.h
deleted file mode 100644
index 206e6423c..000000000
--- a/ic/en-dp-enum.h
+++ /dev/null
@@ -1,30 +0,0 @@
-#ifndef EN_IC_DP_ENUM_H
-#define EN_IC_DP_ENUM_H 1
-
-#include "lib/inc-proc-eng.h"
-#include "openvswitch/hmap.h"
-#include "openvswitch/shash.h"
-
-/* Data maintained by the "dp_enum" engine node: the result of enumerating the
- * IC-SB datapath bindings.  It is the explicit, engine-visible form of the
- * state that used to be built on the stack by enumerate_datapaths() inside
- * ovn_db_run().  The transit-switch (en_ts) and transit-router (en_tr) nodes
- * consume this data instead of rebuilding it themselves, which also keeps the
- * shared 'dp_tnlids' tunnel-key allocator consistent between them. */
-struct ed_type_dp_enum {
-    /* Set of tunnel keys already in use by IC-SB datapath bindings.  Holds
-     * 'struct tnlid_node *' entries owned by this node. */
-    struct hmap dp_tnlids;
-    /* Transit-switch datapath bindings, keyed by transit switch name.  Values
-     * are 'const struct icsbrec_datapath_binding *' owned by the IDL. */
-    struct shash isb_ts_dps;
-    /* Transit-router datapath bindings, keyed by NB IC UUID string.  Values
-     * are 'const struct icsbrec_datapath_binding *' owned by the IDL. */
-    struct shash isb_tr_dps;
-};
-
-enum engine_node_state en_dp_enum_run(struct engine_node *node, void *data);
-void *en_dp_enum_init(struct engine_node *node, struct engine_arg *arg);
-void en_dp_enum_cleanup(void *data);
-
-#endif /* EN_IC_DP_ENUM_H */
diff --git a/ic/en-tr.c b/ic/en-tr.c
index c3b1b60c6..1b5f1db6d 100644
--- a/ic/en-tr.c
+++ b/ic/en-tr.c
@@ -14,21 +14,96 @@
 
 #include <config.h>
 
-#include "en-dp-enum.h"
-#include "en-tr.h"
 #include "en-az.h"
+#include "en-tr.h"
 #include "lib/inc-proc-eng.h"
+#include "lib/ovn-ic-nb-idl.h"
+#include "lib/ovn-ic-sb-idl.h"
+#include "lib/ovn-nb-idl.h"
+#include "openvswitch/shash.h"
 #include "openvswitch/vlog.h"
 #include "ovn-ic.h"
+#include "ovsdb-idl.h"
+#include "smap.h"
+#include "util.h"
+#include "uuid.h"
 
 VLOG_DEFINE_THIS_MODULE(en_ic_tr);
 
+/* Builds 'isb_tr_dps': the committed IC-SB transit-router Datapath_Bindings
+ * keyed by NB IC UUID string.  Read-only; used to copy each committed tunnel
+ * key into its NB Logical_Router mirror's options:requested-tnl-key.  This is
+ * en_tr's own local data, rebuilt each run and never mutated by another node.
+ */
+static void
+collect_tr_datapaths(struct ic_context *ctx, struct shash *isb_tr_dps)
+{
+    shash_init(isb_tr_dps);
+
+    const struct icsbrec_datapath_binding *isb_dp;
+    ICSBREC_DATAPATH_BINDING_FOR_EACH (isb_dp, ctx->ovnisb_idl) {
+        if (ic_dp_get_type(isb_dp) == IC_ROUTER) {
+            char *uuid_str = uuid_to_string(isb_dp->nb_ic_uuid);
+            shash_add(isb_tr_dps, uuid_str, isb_dp);
+            free(uuid_str);
+        }
+    }
+}
+
+/* Syncs transit routers to their AZ NB Logical_Router mirrors: creates the
+ * mirror if missing, and copies each committed IC-SB Datapath_Binding tunnel
+ * key into options:requested-tnl-key.  IC-SB Datapath_Binding creation/keying/
+ * GC is owned by en_tunnel_key. */
+static void
+tr_run(struct ic_context *ctx, struct shash *isb_tr_dps)
+{
+    if (!ctx->ovnnb_txn) {
+        return;
+    }
+
+    const struct nbrec_logical_router *lr;
+    struct shash nb_tres = SHASH_INITIALIZER(&nb_tres);
+    NBREC_LOGICAL_ROUTER_FOR_EACH (lr, ctx->ovnnb_idl) {
+        const char *tr_name = smap_get(&lr->options, "interconn-tr");
+        if (tr_name) {
+            shash_add(&nb_tres, tr_name, lr);
+        }
+    }
+
+    const struct icnbrec_transit_router *tr;
+    ICNBREC_TRANSIT_ROUTER_FOR_EACH (tr, ctx->ovninb_idl) {
+        lr = shash_find_and_delete(&nb_tres, tr->name);
+        if (!lr) {
+            lr = nbrec_logical_router_insert(ctx->ovnnb_txn);
+            nbrec_logical_router_set_name(lr, tr->name);
+            nbrec_logical_router_update_options_setkey(
+                lr, "interconn-tr", tr->name);
+        }
+        char *uuid_str = uuid_to_string(&tr->header_.uuid);
+        const struct icsbrec_datapath_binding *isb_dp =
+            shash_find_data(isb_tr_dps, uuid_str);
+        free(uuid_str);
+
+        if (isb_dp) {
+            char *tnl_key_str = xasprintf("%"PRId64, isb_dp->tunnel_key);
+            nbrec_logical_router_update_options_setkey(
+                lr, "requested-tnl-key", tnl_key_str);
+            free(tnl_key_str);
+        }
+    }
+
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &nb_tres) {
+        nbrec_logical_router_delete(node->data);
+    }
+    shash_destroy(&nb_tres);
+}
+
 enum engine_node_state
 en_tr_run(struct engine_node *node, void *data OVS_UNUSED)
 {
     const struct engine_context *eng_ctx = engine_get_context();
     struct ic_context *ctx = eng_ctx->client_ctx;
-    struct ed_type_dp_enum *dp = engine_get_input_data("dp_enum", node);
     const struct ed_type_az *az = engine_get_input_data("az", node);
 
     /* runned_az is resolved by the upstream en_az node.  Without an AZ there
@@ -37,7 +112,10 @@ en_tr_run(struct engine_node *node, void *data OVS_UNUSED)
         return EN_UNCHANGED;
     }
 
-    tr_run(ctx, &dp->dp_tnlids, &dp->isb_tr_dps);
+    struct shash isb_tr_dps;
+    collect_tr_datapaths(ctx, &isb_tr_dps);
+    tr_run(ctx, &isb_tr_dps);
+    shash_destroy(&isb_tr_dps);
 
     return EN_UPDATED;
 }
diff --git a/ic/en-ts.c b/ic/en-ts.c
index e4ec2a0b7..1f1bb5253 100644
--- a/ic/en-ts.c
+++ b/ic/en-ts.c
@@ -14,21 +14,44 @@
 
 #include <config.h>
 
-#include "en-dp-enum.h"
-#include "en-ts.h"
 #include "en-az.h"
+#include "en-ts.h"
 #include "lib/inc-proc-eng.h"
+#include "lib/ovn-ic-nb-idl.h"
+#include "lib/ovn-ic-sb-idl.h"
+#include "lib/ovn-nb-idl.h"
+#include "openvswitch/shash.h"
 #include "openvswitch/vlog.h"
+#include "ovsdb-idl.h"
+#include "smap.h"
+#include "sset.h"
 #include "ovn-ic.h"
 
 VLOG_DEFINE_THIS_MODULE(en_ic_ts);
 
+/* Builds 'isb_ts_dps': the committed IC-SB transit-switch Datapath_Bindings
+ * keyed by transit-switch name.  Read-only; ts_sync_scope() copies each
+ * committed tunnel key into its NB mirror's requested-tnl-key.  Unlike the
+ * former en_dp_enum map, this is en_ts's own local data, rebuilt each run and
+ * never mutated by another node. */
+static void
+collect_ts_datapaths(struct ic_context *ctx, struct shash *isb_ts_dps)
+{
+    shash_init(isb_ts_dps);
+
+    const struct icsbrec_datapath_binding *isb_dp;
+    ICSBREC_DATAPATH_BINDING_FOR_EACH (isb_dp, ctx->ovnisb_idl) {
+        if (ic_dp_get_type(isb_dp) == IC_SWITCH) {
+            shash_add(isb_ts_dps, isb_dp->transit_switch, isb_dp);
+        }
+    }
+}
+
 enum engine_node_state
 en_ts_run(struct engine_node *node, void *data OVS_UNUSED)
 {
     const struct engine_context *eng_ctx = engine_get_context();
     struct ic_context *ctx = eng_ctx->client_ctx;
-    struct ed_type_dp_enum *dp = engine_get_input_data("dp_enum", node);
     const struct ed_type_az *az = engine_get_input_data("az", node);
 
     /* runned_az is resolved by the upstream en_az node.  Without an AZ there
@@ -37,7 +60,10 @@ en_ts_run(struct engine_node *node, void *data OVS_UNUSED)
         return EN_UNCHANGED;
     }
 
-    ts_run(ctx, &dp->dp_tnlids, &dp->isb_ts_dps);
+    struct shash isb_ts_dps;
+    collect_ts_datapaths(ctx, &isb_ts_dps);
+    ts_sync_scope(ctx, &isb_ts_dps, NULL);
+    shash_destroy(&isb_ts_dps);
 
     return EN_UPDATED;
 }
diff --git a/ic/en-tunnel-key.c b/ic/en-tunnel-key.c
new file mode 100644
index 000000000..42d92f5c3
--- /dev/null
+++ b/ic/en-tunnel-key.c
@@ -0,0 +1,426 @@
+/*
+ * 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-tunnel-key.h"
+#include "lib/inc-proc-eng.h"
+#include "lib/ovn-ic-nb-idl.h"
+#include "lib/ovn-ic-sb-idl.h"
+#include "lib/ovn-nb-idl.h"
+#include "lib/ovn-util.h"
+#include "openvswitch/hmap.h"
+#include "openvswitch/shash.h"
+#include "openvswitch/vlog.h"
+#include "ovn-ic.h"
+#include "ovsdb-idl.h"
+#include "smap.h"
+#include "sset.h"
+#include "util.h"
+#include "uuid.h"
+
+VLOG_DEFINE_THIS_MODULE(en_ic_tunnel_key);
+
+/* Returns true if 'key' falls in the global VXLAN datapath tunnel-key range.
+*/
+static bool
+dp_key_in_vxlan_range(int64_t key)
+{
+    return key >= OVN_MIN_DP_VXLAN_KEY_GLOBAL &&
+           key <= OVN_MAX_DP_VXLAN_KEY_GLOBAL;
+}
+
+/* Rebuilds the tunnel-key allocator 'data->dp_tnlids' and, in 'isb_ts_dps'
+ * (keyed by transit-switch name) / 'isb_tr_dps' (keyed by NB IC UUID string),
+ * every committed IC-SB Datapath_Binding.  Read from the locked IC-SB IDL,
+ * which is the same IDL this node writes to and reads back from, so no
+ * locked/unlocked bridging is needed: the tracked changes (from the unlocked
+ * IDL) are used only to scope which entities to reconcile, never to mutate
+ * state directly. */
+static void
+tunnel_key_build_state(struct ic_context *ctx, struct ed_type_tunnel_key *data,
+                       struct shash *isb_ts_dps, struct shash *isb_tr_dps)
+{
+    ovn_destroy_tnlids(&data->dp_tnlids);
+    hmap_init(&data->dp_tnlids);
+    shash_init(isb_ts_dps);
+    shash_init(isb_tr_dps);
+
+    const struct icsbrec_datapath_binding *isb_dp;
+    ICSBREC_DATAPATH_BINDING_FOR_EACH (isb_dp, ctx->ovnisb_idl) {
+        ovn_add_tnlid(&data->dp_tnlids, isb_dp->tunnel_key);
+        if (ic_dp_get_type(isb_dp) == IC_ROUTER) {
+            char *uuid_str = uuid_to_string(isb_dp->nb_ic_uuid);
+            shash_add(isb_tr_dps, uuid_str, isb_dp);
+            free(uuid_str);
+        } else {
+            shash_add(isb_ts_dps, isb_dp->transit_switch, isb_dp);
+        }
+    }
+}
+
+/* Reconciles one transit switch 'ts's IC-SB Datapath_Binding: creates it (with
+ * a fresh globally-unique tunnel key) when 'isb_dp' is NULL, reallocates its
+ * key if the encap (VXLAN) mode changed its range, and back-fills the type /
+ * nb_ic_uuid columns.  On creation, the freshly-allocated key is published to
+ * the AZ NB Logical_Switch mirror ('nb_ts_mirrors', keyed by name) in this
+ * same iteration to avoid a northd datapath tunnel-key flap. */
+static void
+tunnel_key_reconcile_ts(struct ic_context *ctx,
+                        struct ed_type_tunnel_key *data,
+                        const struct icnbrec_transit_switch *ts,
+                        const struct icsbrec_datapath_binding *isb_dp,
+                        struct shash *nb_ts_mirrors, bool vxlan_mode)
+{
+    if (!isb_dp) {
+        int64_t dp_key = allocate_dp_key(&data->dp_tnlids, vxlan_mode,
+                                         "transit switch datapath");
+        if (!dp_key) {
+            return;
+        }
+        struct icsbrec_datapath_binding *new_dp =
+            icsbrec_datapath_binding_insert(ctx->ovnisb_txn);
+        icsbrec_datapath_binding_set_transit_switch(new_dp, ts->name);
+        icsbrec_datapath_binding_set_tunnel_key(new_dp, dp_key);
+        icsbrec_datapath_binding_set_type(new_dp, "transit-switch");
+        icsbrec_datapath_binding_set_nb_ic_uuid(new_dp, &ts->header_.uuid, 1);
+
+        const struct nbrec_logical_switch *ls =
+            shash_find_data(nb_ts_mirrors, ts->name);
+        if (ls) {
+            char *tnl_key_str = xasprintf("%"PRId64, dp_key);
+            nbrec_logical_switch_update_other_config_setkey(
+                ls, "requested-tnl-key", tnl_key_str);
+            free(tnl_key_str);
+        }
+        return;
+    }
+
+    if (dp_key_in_vxlan_range(isb_dp->tunnel_key) != vxlan_mode) {
+        int64_t dp_key = allocate_dp_key(&data->dp_tnlids, vxlan_mode,
+                                         "transit switch datapath");
+        if (dp_key) {
+            icsbrec_datapath_binding_set_tunnel_key(isb_dp, dp_key);
+        }
+    }
+    if (!isb_dp->type) {
+        icsbrec_datapath_binding_set_type(isb_dp, "transit-switch");
+    }
+    if (!isb_dp->nb_ic_uuid) {
+        icsbrec_datapath_binding_set_nb_ic_uuid(isb_dp, &ts->header_.uuid, 1);
+    }
+}
+
+/* Reconciles one transit router 'tr's IC-SB Datapath_Binding: creates it (with
+ * a fresh globally-unique tunnel key, always from the non-VXLAN range) when
+ * 'isb_dp' is NULL.  Its key reaches the NB Logical_Router mirror on a
+ * follow-up iteration (en_tr copies the committed key back). */
+static void
+tunnel_key_reconcile_tr(struct ic_context *ctx,
+                        struct ed_type_tunnel_key *data,
+                        const struct icnbrec_transit_router *tr,
+                        const struct icsbrec_datapath_binding *isb_dp)
+{
+    if (isb_dp) {
+        return;
+    }
+
+    int64_t dp_key = allocate_dp_key(&data->dp_tnlids, false,
+                                     "transit router datapath");
+    if (!dp_key) {
+        return;
+    }
+    struct icsbrec_datapath_binding *new_dp =
+        icsbrec_datapath_binding_insert(ctx->ovnisb_txn);
+    icsbrec_datapath_binding_set_tunnel_key(new_dp, dp_key);
+    icsbrec_datapath_binding_set_nb_ic_uuid(new_dp, &tr->header_.uuid, 1);
+    icsbrec_datapath_binding_set_type(new_dp, "transit-router");
+}
+
+/* Builds 'nb_ts_mirrors' (keyed by transit-switch name) from the AZ NB
+ * Logical_Switch mirrors.  A table walk (not the by-name index) is used so it
+ * sees the mirrors en_ts just inserted in this transaction, which lets a
+ * brand-new binding publish its key to the mirror in the same iteration. */
+static void
+tunnel_key_collect_nb_mirrors(struct ic_context *ctx,
+                              struct shash *nb_ts_mirrors)
+{
+    shash_init(nb_ts_mirrors);
+    if (!ctx->ovnnb_txn) {
+        return;
+    }
+    const struct nbrec_logical_switch *ls;
+    NBREC_LOGICAL_SWITCH_FOR_EACH (ls, ctx->ovnnb_idl) {
+        const char *ts_name = smap_get(&ls->other_config, "interconn-ts");
+        if (ts_name && !shash_find(nb_ts_mirrors, ts_name)) {
+            shash_add(nb_ts_mirrors, ts_name,
+                      CONST_CAST(struct nbrec_logical_switch *, ls));
+        }
+    }
+}
+
+enum engine_node_state
+en_tunnel_key_run(struct engine_node *node OVS_UNUSED, void *data_)
+{
+    const struct engine_context *eng_ctx = engine_get_context();
+    struct ic_context *ctx = eng_ctx->client_ctx;
+    struct ed_type_tunnel_key *data = data_;
+
+    struct shash isb_ts_dps, isb_tr_dps;
+    tunnel_key_build_state(ctx, data, &isb_ts_dps, &isb_tr_dps);
+
+    /* The IC-SB Datapath_Binding table is the leader's to write; only touch it
+     * (create/refresh/delete) while holding the IC-SB lock. */
+    if (ctx->ovnisb_txn && is_az_leader(ctx->ovnisb_txn)) {
+        bool vxlan_mode = ic_ts_compute_vxlan_mode(ctx);
+        struct shash nb_ts_mirrors;
+        tunnel_key_collect_nb_mirrors(ctx, &nb_ts_mirrors);
+
+        const struct icnbrec_transit_switch *ts;
+        ICNBREC_TRANSIT_SWITCH_FOR_EACH (ts, ctx->ovninb_idl) {
+            const struct icsbrec_datapath_binding *isb_dp =
+                shash_find_and_delete(&isb_ts_dps, ts->name);
+            tunnel_key_reconcile_ts(ctx, data, ts, isb_dp, &nb_ts_mirrors,
+                                    vxlan_mode);
+        }
+
+        const struct icnbrec_transit_router *tr;
+        ICNBREC_TRANSIT_ROUTER_FOR_EACH (tr, ctx->ovninb_idl) {
+            char *uuid_str = uuid_to_string(&tr->header_.uuid);
+            const struct icsbrec_datapath_binding *isb_dp =
+                shash_find_and_delete(&isb_tr_dps, uuid_str);
+            free(uuid_str);
+            tunnel_key_reconcile_tr(ctx, data, tr, isb_dp);
+        }
+
+        /* Delete IC-SB Datapath_Bindings whose IC-NB entity is gone. */
+        struct shash_node *sn;
+        SHASH_FOR_EACH (sn, &isb_ts_dps) {
+            icsbrec_datapath_binding_delete(sn->data);
+        }
+        SHASH_FOR_EACH (sn, &isb_tr_dps) {
+            icsbrec_datapath_binding_delete(sn->data);
+        }
+
+        shash_destroy(&nb_ts_mirrors);
+    }
+
+    shash_destroy(&isb_ts_dps);
+    shash_destroy(&isb_tr_dps);
+
+    return EN_UPDATED;
+}
+
+/* Reconciles only the transit switches named in 'ts_scope' and the transit
+ * routers whose NB IC UUID string is in 'tr_scope': creates a missing binding,
+ * refreshes a mis-ranged key, and deletes a binding whose IC-NB entity is
+ * gone. Returns EN_UNHANDLED (forcing a recompute) when the IC-SB
+ * lock/transaction needed to write the binding is not available yet. */
+static enum engine_input_handler_result
+tunnel_key_sync_scope(struct ic_context *ctx, struct ed_type_tunnel_key *data,
+                      struct sset *ts_scope, struct sset *tr_scope)
+{
+    if (sset_is_empty(ts_scope) && sset_is_empty(tr_scope)) {
+        return EN_HANDLED_UNCHANGED;
+    }
+    if (!ctx->ovnisb_txn || !is_az_leader(ctx->ovnisb_txn)) {
+        return EN_UNHANDLED;
+    }
+
+    bool vxlan_mode = ic_ts_compute_vxlan_mode(ctx);
+    struct shash isb_ts_dps, isb_tr_dps;
+    tunnel_key_build_state(ctx, data, &isb_ts_dps, &isb_tr_dps);
+    struct shash nb_ts_mirrors;
+    tunnel_key_collect_nb_mirrors(ctx, &nb_ts_mirrors);
+
+    /* IC-NB Transit_Switch has no generated get_for_name(); map name -> row so
+     * an in-scope transit switch can be looked up. */
+    struct shash icnb_ts = SHASH_INITIALIZER(&icnb_ts);
+    if (!sset_is_empty(ts_scope)) {
+        const struct icnbrec_transit_switch *ts;
+        ICNBREC_TRANSIT_SWITCH_FOR_EACH (ts, ctx->ovninb_idl) {
+            if (!shash_find(&icnb_ts, ts->name)) {
+                shash_add(&icnb_ts, ts->name,
+                          CONST_CAST(struct icnbrec_transit_switch *, ts));
+            }
+        }
+    }
+
+    const char *name;
+    SSET_FOR_EACH (name, ts_scope) {
+        const struct icnbrec_transit_switch *ts =
+            shash_find_data(&icnb_ts, name);
+        const struct icsbrec_datapath_binding *isb_dp =
+            shash_find_data(&isb_ts_dps, name);
+        if (ts) {
+            tunnel_key_reconcile_ts(ctx, data, ts, isb_dp, &nb_ts_mirrors,
+                                    vxlan_mode);
+        } else if (isb_dp) {
+            icsbrec_datapath_binding_delete(isb_dp);
+        }
+    }
+    shash_destroy(&icnb_ts);
+
+    const char *uuid_str;
+    SSET_FOR_EACH (uuid_str, tr_scope) {
+        struct uuid tr_uuid;
+        if (!uuid_from_string(&tr_uuid, uuid_str)) {
+            continue;
+        }
+        const struct icnbrec_transit_router *tr =
+            icnbrec_transit_router_get_for_uuid(ctx->ovninb_idl, &tr_uuid);
+        const struct icsbrec_datapath_binding *isb_dp =
+            shash_find_data(&isb_tr_dps, uuid_str);
+        if (tr) {
+            tunnel_key_reconcile_tr(ctx, data, tr, isb_dp);
+        } else if (isb_dp) {
+            icsbrec_datapath_binding_delete(isb_dp);
+        }
+    }
+
+    shash_destroy(&nb_ts_mirrors);
+    shash_destroy(&isb_ts_dps);
+    shash_destroy(&isb_tr_dps);
+
+    return EN_HANDLED_UPDATED;
+}
+
+/* IC-NB Transit_Switch: a new transit switch needs its IC-SB Datapath_Binding
+ * created; a deleted one needs it garbage-collected.  Both are scoped by name
+ * and reconciled without a full recompute. */
+enum engine_input_handler_result
+en_tunnel_key_icnb_transit_switch_handler(struct engine_node *node,
+                                          void *data_)
+{
+    struct ed_type_tunnel_key *data = data_;
+    struct ic_context *ctx = engine_get_context()->client_ctx;
+    const struct icnbrec_transit_switch_table *tbl =
+        EN_OVSDB_GET(engine_get_input("ICNB_transit_switch", node));
+    struct sset ts_scope = SSET_INITIALIZER(&ts_scope);
+    struct sset tr_scope = SSET_INITIALIZER(&tr_scope);
+    const struct icnbrec_transit_switch *ts;
+    ICNBREC_TRANSIT_SWITCH_TABLE_FOR_EACH_TRACKED (ts, tbl) {
+        sset_add(&ts_scope, ts->name);
+    }
+
+    enum engine_input_handler_result ret =
+        tunnel_key_sync_scope(ctx, data, &ts_scope, &tr_scope);
+    sset_destroy(&ts_scope);
+    sset_destroy(&tr_scope);
+    return ret;
+}
+
+/* IC-NB Transit_Router: analogous to the transit-switch handler, scoped by the
+ * transit router's NB IC UUID (the binding key). */
+enum engine_input_handler_result
+en_tunnel_key_icnb_transit_router_handler(struct engine_node *node,
+                                          void *data_)
+{
+    struct ed_type_tunnel_key *data = data_;
+    struct ic_context *ctx = engine_get_context()->client_ctx;
+    const struct icnbrec_transit_router_table *tbl =
+        EN_OVSDB_GET(engine_get_input("ICNB_transit_router", node));
+    struct sset ts_scope = SSET_INITIALIZER(&ts_scope);
+    struct sset tr_scope = SSET_INITIALIZER(&tr_scope);
+    const struct icnbrec_transit_router *tr;
+    ICNBREC_TRANSIT_ROUTER_TABLE_FOR_EACH_TRACKED (tr, tbl) {
+        char *uuid_str = uuid_to_string(&tr->header_.uuid);
+        sset_add(&tr_scope, uuid_str);
+        free(uuid_str);
+    }
+
+    enum engine_input_handler_result ret =
+        tunnel_key_sync_scope(ctx, data, &ts_scope, &tr_scope);
+    sset_destroy(&ts_scope);
+    sset_destroy(&tr_scope);
+    return ret;
+}
+
+/* IC-SB Datapath_Binding: react only to a binding *deletion* by re-creating it
+ * for the still-present IC-NB entity (self-healing after an out-of-band
+ * delete).  Inserts and modifies are this node's own writes (or already
+ * consistent), so they are a no-op here - the requested-tnl-key follow-up is
+ * driven by en_ts/en_tr's own handlers on this same table. */
+enum engine_input_handler_result
+en_tunnel_key_icsb_datapath_binding_handler(struct engine_node *node,
+                                            void *data_)
+{
+    struct ed_type_tunnel_key *data = data_;
+    struct ic_context *ctx = engine_get_context()->client_ctx;
+    const struct icsbrec_datapath_binding_table *tbl =
+        EN_OVSDB_GET(engine_get_input("ICSB_datapath_binding", node));
+    struct sset ts_scope = SSET_INITIALIZER(&ts_scope);
+    struct sset tr_scope = SSET_INITIALIZER(&tr_scope);
+    const struct icsbrec_datapath_binding *isb_dp;
+    ICSBREC_DATAPATH_BINDING_TABLE_FOR_EACH_TRACKED (isb_dp, tbl) {
+        if (!icsbrec_datapath_binding_is_deleted(isb_dp)) {
+            continue;
+        }
+        if (ic_dp_get_type(isb_dp) == IC_ROUTER) {
+            char *uuid_str = uuid_to_string(isb_dp->nb_ic_uuid);
+            sset_add(&tr_scope, uuid_str);
+            free(uuid_str);
+        } else {
+            sset_add(&ts_scope, isb_dp->transit_switch);
+        }
+    }
+
+    enum engine_input_handler_result ret =
+        tunnel_key_sync_scope(ctx, data, &ts_scope, &tr_scope);
+    sset_destroy(&ts_scope);
+    sset_destroy(&tr_scope);
+    return ret;
+}
+
+/* Shared change handler for the IC-NB Global input: ignores
+ * nb_ic_cfg/sb_ic_cfg sequence-number-only changes, and forces a recompute
+ * only when the row is created/deleted or its 'options' (e.g. vxlan_mode)
+ * change. */
+enum engine_input_handler_result
+ic_nb_global_options_handler(struct engine_node *node, void *data OVS_UNUSED)
+{
+    const struct icnbrec_ic_nb_global_table *tbl =
+        EN_OVSDB_GET(engine_get_input("ICNB_ic_nb_global", node));
+    const struct icnbrec_ic_nb_global *icnb;
+
+    ICNBREC_IC_NB_GLOBAL_TABLE_FOR_EACH_TRACKED (icnb, tbl) {
+        if (icnbrec_ic_nb_global_is_new(icnb) ||
+            icnbrec_ic_nb_global_is_deleted(icnb) ||
+            ovsdb_idl_track_is_updated(&icnb->header_,
+                                       &icnbrec_ic_nb_global_col_options)) {
+            return EN_UNHANDLED;
+        }
+    }
+
+    return EN_HANDLED_UNCHANGED;
+}
+
+void *
+en_tunnel_key_init(struct engine_node *node OVS_UNUSED,
+                   struct engine_arg *arg OVS_UNUSED)
+{
+    struct ed_type_tunnel_key *data = xzalloc(sizeof *data);
+
+    hmap_init(&data->dp_tnlids);
+
+    return data;
+}
+
+void
+en_tunnel_key_cleanup(void *data_)
+{
+    struct ed_type_tunnel_key *data = data_;
+
+    ovn_destroy_tnlids(&data->dp_tnlids);
+}
diff --git a/ic/en-tunnel-key.h b/ic/en-tunnel-key.h
new file mode 100644
index 000000000..138058fee
--- /dev/null
+++ b/ic/en-tunnel-key.h
@@ -0,0 +1,46 @@
+#ifndef EN_IC_TUNNEL_KEY_H
+#define EN_IC_TUNNEL_KEY_H 1
+
+#include "lib/inc-proc-eng.h"
+#include "openvswitch/hmap.h"
+
+/* Data maintained by the "tunnel_key" engine node: the set of datapath tunnel
+ * keys currently in use by IC-SB Datapath_Bindings.  This node is the single
+ * owner of datapath tunnel-key allocation: it runs after en_ts and en_tr
+ * (which only maintain the AZ NB Logical_Switch/Router mirrors) and
+ * reconciles every transit switch/router's IC-SB Datapath_Binding, allocating
+ * a globally unique tunnel key for each new one.  Concentrating allocation in
+ * one node keeps the keys unique across both datapath types without any node
+ * mutating another node's data. */
+struct ed_type_tunnel_key {
+    /* Set of tunnel keys already in use by IC-SB Datapath_Bindings.  Holds
+     * 'struct tnlid_node *' entries owned by this node. */
+    struct hmap dp_tnlids;
+};
+
+enum engine_node_state en_tunnel_key_run(struct engine_node *node, void *data);
+void *en_tunnel_key_init(struct engine_node *node, struct engine_arg *arg);
+void en_tunnel_key_cleanup(void *data);
+
+/* Scoped incremental handlers: create/refresh/GC the IC-SB Datapath_Binding of
+ * only the transit switches/routers whose IC-NB row or IC-SB binding changed,
+ * without a full recompute. */
+enum engine_input_handler_result
+en_tunnel_key_icnb_transit_switch_handler(struct engine_node *node,
+                                          void *data);
+enum engine_input_handler_result
+en_tunnel_key_icnb_transit_router_handler(struct engine_node *node,
+                                          void *data);
+enum engine_input_handler_result
+en_tunnel_key_icsb_datapath_binding_handler(struct engine_node *node,
+                                            void *data);
+
+/* Shared change handler for the IC-NB Global input of nodes that only care
+ * about its 'options' (e.g. vxlan_mode), not the nb_ic_cfg/sb_ic_cfg
+ * sequence numbers that the sequence-number protocol bumps on every change.
+ * Named without a node prefix because it is shared by en_tunnel_key and
+ * en_ts. */
+enum engine_input_handler_result
+ic_nb_global_options_handler(struct engine_node *node, void *data);
+
+#endif /* EN_IC_TUNNEL_KEY_H */
diff --git a/ic/inc-proc-ic.c b/ic/inc-proc-ic.c
index 66958dbf0..64aef2c82 100644
--- a/ic/inc-proc-ic.c
+++ b/ic/inc-proc-ic.c
@@ -28,10 +28,10 @@
 #include "inc-proc-ic.h"
 #include "en-ic.h"
 #include "en-az.h"
-#include "en-dp-enum.h"
 #include "en-gateway.h"
 #include "en-ts.h"
 #include "en-tr.h"
+#include "en-tunnel-key.h"
 #include "en-port-binding.h"
 #include "en-route.h"
 #include "en-service-monitor.h"
@@ -175,10 +175,10 @@ VLOG_DEFINE_THIS_MODULE(inc_proc_ic);
 /* Define engine nodes for other nodes. They should be defined as static to
  * avoid sparse errors. */
 static ENGINE_NODE(az);
-static ENGINE_NODE(dp_enum);
 static ENGINE_NODE(gateway);
 static ENGINE_NODE(ts);
 static ENGINE_NODE(tr);
+static ENGINE_NODE(tunnel_key);
 static ENGINE_NODE(port_binding);
 static ENGINE_NODE(route);
 static ENGINE_NODE(service_monitor);
@@ -201,21 +201,6 @@ void inc_proc_ic_init(struct ovsdb_idl_loop *nb,
      * splitting the monolithic ovn_db_run() into independently-gated nodes.
      * Change handlers are added incrementally in a later step. */
 
-    /* en_dp_enum: enumerate IC-SB datapath bindings (tunnel-key allocator and
-     * transit switch/router datapath maps shared by en_ts and en_tr).
-     *
-     * en_ts and en_tr allocate datapath tunnel keys from the shared
-     * 'dp_tnlids' set owned by this node, mutating it during their run.  To
-     * keep that allocator correct, en_dp_enum must rebuild it from scratch in
-     * the same iteration as any allocation.  It therefore depends not only on
-     * the IC-SB datapath bindings themselves, but also on every input that can
-     * cause en_ts/en_tr to allocate a key: a new transit switch or router, or
-     * a change of vxlan_mode (which forces a tunnel-key refresh). */
-    engine_add_input(&en_dp_enum, &en_icsb_datapath_binding, NULL);
-    engine_add_input(&en_dp_enum, &en_icnb_transit_switch, NULL);
-    engine_add_input(&en_dp_enum, &en_icnb_transit_router, NULL);
-    engine_add_input(&en_dp_enum, &en_icnb_ic_nb_global, NULL);
-
     /* en_gateway: sync gateways/chassis between SB and IC-SB. */
     engine_add_input(&en_gateway, &en_az, NULL);
     engine_add_input(&en_gateway, &en_icsb_availability_zone, NULL);
@@ -224,20 +209,49 @@ void inc_proc_ic_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_gateway, &en_sb_chassis, NULL);
     engine_add_input(&en_gateway, &en_sb_encap, NULL);
 
-    /* en_ts: sync transit switches to NB and IC-SB datapath bindings. */
+    /* en_ts: sync transit switches to their AZ NB Logical_Switch mirrors.
+     * en_ts builds its own transit-switch IC-SB Datapath_Binding map each run
+     * and only maintains the NB mirror; IC-SB Datapath_Binding creation/keying
+     * is owned by en_tunnel_key (downstream). */
     engine_add_input(&en_ts, &en_az, NULL);
-    engine_add_input(&en_ts, &en_dp_enum, NULL);
+    engine_add_input(&en_ts, &en_icsb_datapath_binding, NULL);
     engine_add_input(&en_ts, &en_icnb_ic_nb_global, NULL);
     engine_add_input(&en_ts, &en_icnb_transit_switch, NULL);
     engine_add_input(&en_ts, &en_nb_logical_switch, NULL);
     engine_add_input(&en_ts, &en_icsb_encap, NULL);
 
-    /* en_tr: sync transit routers to NB and IC-SB datapath bindings. */
+    /* en_tr: sync transit routers to their AZ NB Logical_Router mirrors.
+     * Like en_ts, IC-SB Datapath_Binding creation/keying is owned by
+     * en_tunnel_key. */
     engine_add_input(&en_tr, &en_az, NULL);
-    engine_add_input(&en_tr, &en_dp_enum, NULL);
+    engine_add_input(&en_tr, &en_icsb_datapath_binding, NULL);
     engine_add_input(&en_tr, &en_icnb_transit_router, NULL);
     engine_add_input(&en_tr, &en_nb_logical_router, NULL);
 
+    /* en_tunnel_key: the single owner of IC-SB Datapath_Binding creation,
+     * tunnel-key allocation, VXLAN-range refresh and GC, for both transit
+     * switches and transit routers.  Concentrating allocation in one node
+     * keeps the keys globally unique across both datapath types without any
+     * node mutating another's data.
+     *
+     * It is ordered after en_ts and en_tr (no-op edges) so the AZ NB mirrors
+     * exist before it publishes a brand-new binding's key to them (the
+     * anti-flap early publish in en_tunnel_key_run()).  The IC-NB transit
+     * switch/router and IC-SB Datapath_Binding inputs drive create/GC; the
+     * IC-NB Global (vxlan_mode) and IC-SB Encap inputs drive the VXLAN-range
+     * refresh. */
+    engine_add_input(&en_tunnel_key, &en_ts, engine_noop_handler);
+    engine_add_input(&en_tunnel_key, &en_tr, engine_noop_handler);
+    engine_add_input(&en_tunnel_key, &en_icsb_datapath_binding,
+                     en_tunnel_key_icsb_datapath_binding_handler);
+    engine_add_input(&en_tunnel_key, &en_icnb_transit_switch,
+                     en_tunnel_key_icnb_transit_switch_handler);
+    engine_add_input(&en_tunnel_key, &en_icnb_transit_router,
+                     en_tunnel_key_icnb_transit_router_handler);
+    engine_add_input(&en_tunnel_key, &en_icnb_ic_nb_global,
+                     ic_nb_global_options_handler);
+    engine_add_input(&en_tunnel_key, &en_icsb_encap, NULL);
+
     /* en_port_binding: sync cross-AZ port bindings. */
     engine_add_input(&en_port_binding, &en_az, NULL);
     engine_add_input(&en_port_binding, &en_icsb_availability_zone, NULL);
@@ -290,13 +304,13 @@ void inc_proc_ic_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_address_set, &en_sb_address_set, NULL);
     engine_add_input(&en_address_set, &en_icsb_address_set, NULL);
 
-    /* en_ic: output node aggregating all subsystems.  Order matches the
-     * previous ovn_db_run() call order; in particular en_ts is added before
-     * en_tr so they allocate datapath tunnel keys from the shared en_dp_enum
-     * allocator in the same order as before. */
+    /* en_ic: output node aggregating all subsystems.  en_tunnel_key is added
+     * after en_ts and en_tr, matching its ordering dependency on them (it
+     * publishes a new binding's key to the NB mirror they create). */
     engine_add_input(&en_ic, &en_gateway, NULL);
     engine_add_input(&en_ic, &en_ts, NULL);
     engine_add_input(&en_ic, &en_tr, NULL);
+    engine_add_input(&en_ic, &en_tunnel_key, NULL);
     engine_add_input(&en_ic, &en_port_binding, NULL);
     engine_add_input(&en_ic, &en_route, NULL);
     engine_add_input(&en_ic, &en_service_monitor, NULL);
diff --git a/ic/ovn-ic.c b/ic/ovn-ic.c
index 9c21ef1de..e49ef6a17 100644
--- a/ic/ovn-ic.c
+++ b/ic/ovn-ic.c
@@ -99,7 +99,7 @@ Options:\n\
     stream_usage("database", true, true, false);
 }
 
-static uint32_t
+uint32_t
 allocate_dp_key(struct hmap *dp_tnlids, bool vxlan_mode, const char *name)
 {
     uint32_t hint = vxlan_mode ? OVN_MIN_DP_VXLAN_KEY_GLOBAL
@@ -109,7 +109,7 @@ allocate_dp_key(struct hmap *dp_tnlids, bool vxlan_mode, 
const char *name)
             &hint);
 }
 
-static enum ic_datapath_type
+enum ic_datapath_type
 ic_dp_get_type(const struct icsbrec_datapath_binding *isb_dp)
 {
     if (isb_dp->type && !strcmp(isb_dp->type, "transit-router")) {
@@ -129,250 +129,177 @@ ic_pb_get_type(const struct icsbrec_port_binding 
*isb_pb)
     return IC_SWITCH_PORT;
 }
 
-void
-enumerate_datapaths(struct ic_context *ctx, struct hmap *dp_tnlids,
-                    struct shash *isb_ts_dps, struct shash *isb_tr_dps)
-{
-    const struct icsbrec_datapath_binding *isb_dp;
-    ICSBREC_DATAPATH_BINDING_FOR_EACH (isb_dp, ctx->ovnisb_idl) {
-        ovn_add_tnlid(dp_tnlids, isb_dp->tunnel_key);
-
-        enum ic_datapath_type dp_type = ic_dp_get_type(isb_dp);
-        if (dp_type == IC_ROUTER) {
-            char *uuid_str = uuid_to_string(isb_dp->nb_ic_uuid);
-            shash_add(isb_tr_dps, uuid_str, isb_dp);
-            free(uuid_str);
-        } else {
-            shash_add(isb_ts_dps, isb_dp->transit_switch, isb_dp);
-        }
-    }
-}
-
 /*
  * Check if the AZ is the leader by checking the lock.
  */
-static bool
+bool
 is_az_leader(struct ovsdb_idl_txn *txn)
 {
     struct ovsdb_idl *idl = ovsdb_idl_txn_get_idl(txn);
     return idl && ovsdb_idl_has_lock(idl);
 }
 
-void
-ts_run(struct ic_context *ctx, struct hmap *dp_tnlids,
-       struct shash *isb_ts_dps)
+/* Returns true if transit-switch datapaths must use the VXLAN tunnel-key
+ * range: IC-NB requests vxlan_mode and the IC-SB actually has a VXLAN encap.
+ *
+ * Warning: ovnisb_unlocked should not be used to insert data on IC_SB which
+ * can cause a constraint violation, as an example, inserting data to IC-SB
+ * datapath_binding. */
+bool
+ic_ts_compute_vxlan_mode(struct ic_context *ctx)
 {
-    const struct icnbrec_transit_switch *ts;
-    bool dp_key_refresh = false;
-    bool vxlan_mode = false;
     const struct icnbrec_ic_nb_global *ic_nb =
         icnbrec_ic_nb_global_first(ctx->ovninb_idl);
 
-    /*
-     * Warning: ovnisb_unlocked should not be used to insert data on IC_SB
-     * which can cause a constraint violation, as an example, inserting data to
-     * IC-SB datapath_binding.
-     */
     if (ic_nb && smap_get_bool(&ic_nb->options, "vxlan_mode", false)) {
         const struct icsbrec_encap *encap;
         ICSBREC_ENCAP_FOR_EACH (encap, ctx->ovnisb_unlocked_idl) {
             if (!strcmp(encap->type, "vxlan")) {
-                vxlan_mode = true;
-                break;
+                return true;
             }
         }
     }
+    return false;
+}
 
-    /* Sync INB TS to AZ NB */
-    if (ctx->ovnnb_txn) {
-        struct shash nb_tses = SHASH_INITIALIZER(&nb_tses);
-        const struct nbrec_logical_switch *ls;
-
-        /* Get current NB Logical_Switch with other_config:interconn-ts */
-        NBREC_LOGICAL_SWITCH_FOR_EACH (ls, ctx->ovnnb_idl) {
-            const char *ts_name = smap_get(&ls->other_config, "interconn-ts");
-            if (ts_name) {
-                shash_add(&nb_tses, ts_name, ls);
-            }
-        }
-
-        /* Create/update NB Logical_Switch for each TS */
-        ICNBREC_TRANSIT_SWITCH_FOR_EACH (ts, ctx->ovninb_idl) {
-            ls = shash_find_and_delete(&nb_tses, ts->name);
-            if (!ls) {
-                ls = nbrec_logical_switch_insert(ctx->ovnnb_txn);
-                nbrec_logical_switch_set_name(ls, ts->name);
-                nbrec_logical_switch_update_other_config_setkey(ls,
-                                                                "interconn-ts",
-                                                                ts->name);
-                nbrec_logical_switch_update_other_config_setkey(
-                        ls, "ic-vxlan_mode", vxlan_mode ? "true" : "false");
-            } else {
-                bool _vxlan_mode = smap_get_bool(&ls->other_config,
-                                                 "ic-vxlan_mode", false);
-                if (_vxlan_mode != vxlan_mode) {
-                    dp_key_refresh = true;
-                    nbrec_logical_switch_update_other_config_setkey(
-                            ls, "ic-vxlan_mode",
-                            vxlan_mode ? "true" : "false");
-                }
-            }
-
-            const struct icsbrec_datapath_binding *isb_dp;
-            isb_dp = shash_find_data(isb_ts_dps, ts->name);
-            if (isb_dp) {
-                int64_t nb_tnl_key = smap_get_int(&ls->other_config,
-                                                  "requested-tnl-key",
-                                                  0);
-                if (nb_tnl_key != isb_dp->tunnel_key) {
-                    VLOG_DBG("Set other_config:requested-tnl-key %"PRId64
-                             " for transit switch %s in NB.",
-                             isb_dp->tunnel_key, ts->name);
-                    char *tnl_key_str = xasprintf("%"PRId64,
-                                                  isb_dp->tunnel_key);
-                    nbrec_logical_switch_update_other_config_setkey(
-                        ls, "requested-tnl-key", tnl_key_str);
-                    free(tnl_key_str);
-                }
-            }
-        }
-
-        /* Delete extra NB Logical_Switch with other_config:interconn-ts */
-        struct shash_node *node;
-        SHASH_FOR_EACH (node, &nb_tses) {
-            nbrec_logical_switch_delete(node->data);
-        }
-        shash_destroy(&nb_tses);
-    }
-
-    /* Sync TS between INB and ISB.  This is performed after syncing with AZ
-     * SB, to avoid uncommitted ISB datapath tunnel key to be synced back to
-     * AZ. */
-    if (ctx->ovnisb_txn &&
-        is_az_leader(ctx->ovnisb_txn)) {
-        /* Create ISB Datapath_Binding */
-        ICNBREC_TRANSIT_SWITCH_FOR_EACH (ts, ctx->ovninb_idl) {
-            const struct icsbrec_datapath_binding *isb_dp =
-                shash_find_and_delete(isb_ts_dps, ts->name);
-            if (!isb_dp) {
-                /* Allocate tunnel key */
-                int64_t dp_key = allocate_dp_key(dp_tnlids, vxlan_mode,
-                                                 "transit switch datapath");
-                if (!dp_key) {
-                    continue;
-                }
-
-                isb_dp = icsbrec_datapath_binding_insert(ctx->ovnisb_txn);
-                icsbrec_datapath_binding_set_transit_switch(isb_dp, ts->name);
-                icsbrec_datapath_binding_set_tunnel_key(isb_dp, dp_key);
-            } else if (dp_key_refresh) {
-                /* Refresh tunnel key since encap mode has changed. */
-                int64_t dp_key = allocate_dp_key(dp_tnlids, vxlan_mode,
-                                                 "transit switch datapath");
-                if (dp_key) {
-                    icsbrec_datapath_binding_set_tunnel_key(isb_dp, dp_key);
-                }
-            }
-
-            if (!isb_dp->type) {
-                icsbrec_datapath_binding_set_type(isb_dp, "transit-switch");
-            }
+/* Keep other_config:ic-vxlan_mode on the transit switch's NB Logical_Switch
+ * mirror in sync with the IC VXLAN mode.  northd reads the boolean value with
+ * smap_get_bool() (northd/en-global-config.c), so writing "false" is correct
+ * and does not affect the datapath tunnel-id range.  Only write when the value
+ * actually differs to avoid needless transaction churn. */
+static void
+ts_set_ic_vxlan_mode(const struct nbrec_logical_switch *ls, bool vxlan_mode)
+{
+    const char *want = vxlan_mode ? "true" : "false";
+    const char *cur = smap_get(&ls->other_config, "ic-vxlan_mode");
+    if (!cur || strcmp(cur, want)) {
+        nbrec_logical_switch_update_other_config_setkey(ls, "ic-vxlan_mode",
+                                                        want);
+    }
+}
 
-            if (!isb_dp->nb_ic_uuid) {
-                icsbrec_datapath_binding_set_nb_ic_uuid(isb_dp,
-                                                        &ts->header_.uuid, 1);
-            }
-        }
+/* Reconciles a single transit switch 'ts's AZ NB Logical_Switch mirror:
+ * creates it if missing, keeps other_config:ic-vxlan_mode in sync, and copies
+ * the committed IC-SB Datapath_Binding tunnel key into
+ * other_config:requested-tnl-key.  IC-SB Datapath_Binding creation/keying is
+ * owned by en_tunnel_key.
+ *
+ * 'nb_gc' (keyed by transit-switch name) doubles as a garbage-collection set:
+ * this function removes the mirror it claims, so whatever remains after every
+ * in-scope switch has been reconciled is stale and deleted by the caller.
+ * 'isb_gc' (the IC-SB Datapath_Binding map keyed by transit-switch name) is
+ * read-only here. */
+static void
+ts_sync_one(struct ic_context *ctx, const struct icnbrec_transit_switch *ts,
+            struct shash *isb_gc, struct shash *nb_gc, bool vxlan_mode)
+{
+    if (!nb_gc) {
+        return;
+    }
 
-        struct shash_node *node;
-        SHASH_FOR_EACH (node, isb_ts_dps) {
-            icsbrec_datapath_binding_delete(node->data);
+    const struct nbrec_logical_switch *ls =
+        shash_find_and_delete(nb_gc, ts->name);
+    if (!ls) {
+        ls = nbrec_logical_switch_insert(ctx->ovnnb_txn);
+        nbrec_logical_switch_set_name(ls, ts->name);
+
+        /* Write other_config as a whole column rather than with per-key
+         * partial map updates.  A partial update is only queued as a map
+         * operation and is not reflected in 'ls->other_config' until the
+         * transaction commits, so en_tunnel_key would not recognize this
+         * brand-new row as a transit switch mirror and could not publish the
+         * freshly allocated tunnel key to it in this same iteration.  northd
+         * would then pick its own datapath tunnel key and flap it once
+         * requested-tnl-key finally showed up. */
+        struct smap other_config = SMAP_INITIALIZER(&other_config);
+        smap_add(&other_config, "interconn-ts", ts->name);
+        smap_add(&other_config, "ic-vxlan_mode",
+                 vxlan_mode ? "true" : "false");
+        nbrec_logical_switch_set_other_config(ls, &other_config);
+        smap_destroy(&other_config);
+    } else {
+        ts_set_ic_vxlan_mode(ls, vxlan_mode);
+    }
+
+    const struct icsbrec_datapath_binding *isb_dp =
+        shash_find_data(isb_gc, ts->name);
+    if (isb_dp) {
+        int64_t nb_tnl_key = smap_get_int(&ls->other_config,
+                                          "requested-tnl-key", 0);
+        if (nb_tnl_key != isb_dp->tunnel_key) {
+            VLOG_DBG("Set other_config:requested-tnl-key %"PRId64
+                     " for transit switch %s in NB.",
+                     isb_dp->tunnel_key, ts->name);
+            char *tnl_key_str = xasprintf("%"PRId64, isb_dp->tunnel_key);
+            nbrec_logical_switch_update_other_config_setkey(
+                ls, "requested-tnl-key", tnl_key_str);
+            free(tnl_key_str);
         }
     }
 }
 
+/* Synchronizes transit switches to their AZ NB Logical_Switch mirrors.  When
+ * 'ts_scope' is NULL every transit switch is reconciled (full recompute);
+ * otherwise only the switches named in 'ts_scope' are.  A name still in scope
+ * but no longer present in IC-NB (a deleted switch) is honoured: its mirror
+ * ends up as a garbage-collection leftover and is deleted, matching
+ * full-recompute behaviour.
+ *
+ * 'isb_ts_dps' (the IC-SB Datapath_Binding map keyed by transit-switch name)
+ * is read only, to copy each committed tunnel key into the mirror's
+ * requested-tnl-key.  IC-SB Datapath_Binding creation/keying/GC is owned by
+ * en_tunnel_key, so this function neither allocates keys nor mutates
+ * 'isb_ts_dps'. */
 void
-tr_run(struct ic_context *ctx, struct hmap *dp_tnlids,
-       struct shash *isb_tr_dps)
+ts_sync_scope(struct ic_context *ctx, struct shash *isb_ts_dps,
+              const struct sset *ts_scope)
 {
-    /*
-     * Warning: ovnisb_unlocked should not be used to insert data on IC_SB
-     * which can cause a constraint violation, as an example, inserting data to
-     * IC-SB datapath_binding.
-     */
-    const struct nbrec_logical_router *lr;
+    bool full = !ts_scope;
+    bool vxlan_mode = ic_ts_compute_vxlan_mode(ctx);
 
+    /* Build the NB Logical_Switch mirror GC set, keyed by transit-switch
+     * name.  Only needed when an NB transaction is available. */
+    struct shash nb_ts_mirrors = SHASH_INITIALIZER(&nb_ts_mirrors);
+    struct shash *nb_gc = NULL;
     if (ctx->ovnnb_txn) {
-        struct shash nb_tres = SHASH_INITIALIZER(&nb_tres);
-        NBREC_LOGICAL_ROUTER_FOR_EACH (lr, ctx->ovnnb_idl) {
-            const char *tr_name = smap_get(&lr->options, "interconn-tr");
-            if (tr_name) {
-                shash_add(&nb_tres, tr_name, lr);
-            }
-        }
-
-        const struct icnbrec_transit_router *tr;
-        ICNBREC_TRANSIT_ROUTER_FOR_EACH (tr, ctx->ovninb_idl) {
-            lr = shash_find_and_delete(&nb_tres, tr->name);
-            if (!lr) {
-                lr = nbrec_logical_router_insert(ctx->ovnnb_txn);
-                nbrec_logical_router_set_name(lr, tr->name);
-                nbrec_logical_router_update_options_setkey(
-                    lr, "interconn-tr", tr->name);
+        nb_gc = &nb_ts_mirrors;
+        if (full) {
+            const struct nbrec_logical_switch *ls;
+            NBREC_LOGICAL_SWITCH_FOR_EACH (ls, ctx->ovnnb_idl) {
+                const char *ts_name = smap_get(&ls->other_config,
+                                               "interconn-ts");
+                if (ts_name) {
+                    shash_add(nb_gc, ts_name, ls);
+                }
             }
-            char *uuid_str = uuid_to_string(&tr->header_.uuid);
-            struct icsbrec_datapath_binding *isb_dp = shash_find_data(
-                isb_tr_dps, uuid_str);
-            free(uuid_str);
-
-            if (isb_dp) {
-                char *tnl_key_str = xasprintf("%"PRId64, isb_dp->tunnel_key);
-                nbrec_logical_router_update_options_setkey(
-                    lr, "requested-tnl-key", tnl_key_str);
-                free(tnl_key_str);
+        } else {
+            const char *name;
+            SSET_FOR_EACH (name, ts_scope) {
+                const struct nbrec_logical_switch *ls =
+                    find_ts_in_nb(ctx, CONST_CAST(char *, name));
+                if (ls && !shash_find(nb_gc, name)) {
+                    shash_add(nb_gc, name, ls);
+                }
             }
         }
+    }
 
-        struct shash_node *node;
-        SHASH_FOR_EACH (node, &nb_tres) {
-            nbrec_logical_router_delete(node->data);
-        }
-        shash_destroy(&nb_tres);
-    }
-
-    /* Sync TR between INB and ISB.  This is performed after syncing with AZ
-     * SB, to avoid uncommitted ISB datapath tunnel key to be synced back to
-     * AZ. */
-    if (ctx->ovnisb_txn &&
-        is_az_leader(ctx->ovnisb_txn)) {
-        /* Create ISB Datapath_Binding */
-        const struct icnbrec_transit_router *tr;
-        ICNBREC_TRANSIT_ROUTER_FOR_EACH (tr, ctx->ovninb_idl) {
-            char *uuid_str = uuid_to_string(&tr->header_.uuid);
-            struct icsbrec_datapath_binding *isb_dp =
-                shash_find_and_delete(isb_tr_dps, uuid_str);
-            free(uuid_str);
-
-            if (!isb_dp) {
-                int dp_key = allocate_dp_key(dp_tnlids, false,
-                                             "transit router datapath");
-                if (!dp_key) {
-                    continue;
-                }
-
-                isb_dp = icsbrec_datapath_binding_insert(ctx->ovnisb_txn);
-                icsbrec_datapath_binding_set_tunnel_key(isb_dp, dp_key);
-                icsbrec_datapath_binding_set_nb_ic_uuid(isb_dp,
-                                                        &tr->header_.uuid, 1);
-                icsbrec_datapath_binding_set_type(isb_dp, "transit-router");
-            }
+    const struct icnbrec_transit_switch *ts;
+    ICNBREC_TRANSIT_SWITCH_FOR_EACH (ts, ctx->ovninb_idl) {
+        if (full || sset_contains(ts_scope, ts->name)) {
+            ts_sync_one(ctx, ts, isb_ts_dps, nb_gc, vxlan_mode);
         }
+    }
 
+    /* Delete extra NB Logical_Switch with other_config:interconn-ts. */
+    if (nb_gc) {
         struct shash_node *node;
-        SHASH_FOR_EACH (node, isb_tr_dps) {
-            icsbrec_datapath_binding_delete(node->data);
+        SHASH_FOR_EACH (node, nb_gc) {
+            nbrec_logical_switch_delete(node->data);
         }
     }
+
+    shash_destroy(&nb_ts_mirrors);
 }
 
 /* Returns true if any information in gw and chassis is different. */
@@ -708,7 +635,7 @@ gateway_run(struct ic_context *ctx,
     shash_destroy(&remote_gws);
 }
 
-static const struct nbrec_logical_switch *
+const struct nbrec_logical_switch *
 find_ts_in_nb(struct ic_context *ctx, char *ts_name)
 {
     const struct nbrec_logical_switch *key =
diff --git a/ic/ovn-ic.h b/ic/ovn-ic.h
index c6d5e0a6e..be6bb9f6c 100644
--- a/ic/ovn-ic.h
+++ b/ic/ovn-ic.h
@@ -62,21 +62,31 @@ enum ic_port_binding_type { IC_SWITCH_PORT, IC_ROUTER_PORT, 
IC_PORT_MAX };
 
 struct hmap;
 struct shash;
+struct sset;
 struct icsbrec_availability_zone;
+struct icsbrec_datapath_binding;
+
+enum ic_datapath_type ic_dp_get_type(
+    const struct icsbrec_datapath_binding *isb_dp);
 
-/* Per-subsystem entry points, invoked by the incremental-processing engine
- * nodes (see ic/en-*.c).  Each performs a full recompute of its subsystem and
- * may be invoked independently when its engine inputs change. */
-void enumerate_datapaths(struct ic_context *ctx, struct hmap *dp_tnlids,
-                         struct shash *isb_ts_dps, struct shash *isb_tr_dps);
 void gateway_run(struct ic_context *ctx,
                  const struct icsbrec_availability_zone *runned_az);
 void address_set_run(struct ic_context *ctx,
                      const struct icsbrec_availability_zone *runned_az);
-void ts_run(struct ic_context *ctx, struct hmap *dp_tnlids,
-            struct shash *isb_ts_dps);
-void tr_run(struct ic_context *ctx, struct hmap *dp_tnlids,
-            struct shash *isb_tr_dps);
+
+/* Reconciles the AZ NB Logical_Switch mirrors of the transit switches named in
+ * 'ts_scope' (NULL reconciles every transit switch).  'isb_ts_dps' is the
+ * transit-switch IC-SB Datapath_Binding map (keyed by transit-switch name),
+ * read only to copy each committed tunnel key into the mirror's
+ * requested-tnl-key.  IC-SB Datapath_Binding creation/keying/GC is owned by
+ * the en_tunnel_key node. */
+void ts_sync_scope(struct ic_context *ctx, struct shash *isb_ts_dps,
+                   const struct sset *ts_scope);
+
+/* True if transit-switch datapaths must use the VXLAN tunnel-key range: IC-NB
+ * requests vxlan_mode and the IC-SB actually has a VXLAN encap. */
+bool ic_ts_compute_vxlan_mode(struct ic_context *ctx);
+
 void port_binding_run(struct ic_context *ctx,
                       const struct icsbrec_availability_zone *runned_az);
 void route_run(struct ic_context *ctx,
@@ -84,4 +94,12 @@ void route_run(struct ic_context *ctx,
 void sync_service_monitor(struct ic_context *ctx,
                           const struct icsbrec_availability_zone *runned_az);
 
+/* Shared IC helpers used by more than one engine node. */
+uint32_t
+allocate_dp_key(struct hmap *dp_tnlids, bool vxlan_mode, const char *name);
+const struct nbrec_logical_switch *
+find_ts_in_nb(struct ic_context *ctx, char *ts_name);
+bool
+is_az_leader(struct ovsdb_idl_txn *txn);
+
 #endif /* OVN_IC_H */
-- 
2.34.1


-- 




_'Esta mensagem é direcionada apenas para os endereços constantes no 
cabeçalho inicial. Se você não está listado nos endereços constantes no 
cabeçalho, pedimos-lhe que desconsidere completamente o conteúdo dessa 
mensagem e cuja cópia, encaminhamento e/ou execução das ações citadas estão 
imediatamente anuladas e proibidas'._


* **'Apesar do Magazine Luiza tomar 
todas as precauções razoáveis para assegurar que nenhum vírus esteja 
presente nesse e-mail, a empresa não poderá aceitar a responsabilidade por 
quaisquer perdas ou danos causados por esse e-mail ou por seus anexos'.*



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

Reply via email to