[{"id":3675921,"web_url":"http://patchwork.ozlabs.org/comment/3675921/","msgid":"<3957ba70-10d6-4f4b-b2ed-d3b148cce66d@redhat.com>","list_archive_url":null,"date":"2026-04-10T15:05:53","subject":"Re: [ovs-dev] [PATCH ovn 4/6] controller: Consolidate the netlink\n notifiers.","submitter":{"id":76591,"url":"http://patchwork.ozlabs.org/api/people/76591/","name":"Dumitru Ceara","email":"dceara@redhat.com"},"content":"On 4/2/26 10:25 AM, Ales Musil wrote:\n> The netlink notifiers used a lot of code that was more or less\n> identical to each other. Extract the common code into separate module\n> which allows the definition of listeners and their specific data.\n> This should make it easier to add any new notifier, which will be the\n> case in the future. It should also make it slightly easier to track\n> individual updates and changes that could be processed incrementally\n> instead of full recompute when there is any change.\n> \n> Signed-off-by: Ales Musil <amusil@redhat.com>\n> ---\n\nHi Ales,\n\n> -dnl Should NOT notify if an entry is added to a bridge that's not monitored by\n> +dnl Should notify if an entry is removed from a bridge that's monitored by\n>  dnl OVN.\n> -check ovstest test-ovn-netlink neighbor-table-notify br-test $br_if_index \\\n> +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> +    'ip neigh del 10.10.10.10 lladdr 00:00:00:00:10:00 \\\n> +        dev br-test' | sort], [0], [dnl\n> +Add neighbor ifindex=$br_if_index vlan=0 eth=00:00:00:00:00:00 dst=10.10.10.10 port=0\n> +Delete neighbor ifindex=$br_if_index vlan=0 eth=00:00:00:00:00:00 dst=10.10.10.10 port=0\n> +])\n> +\n> +dnl Should NOT notify if an noarp entry is added to a bridge port\n\nTypo: a noarp\n\nWith that addressed:\n\nAcked-by: Dumitru Ceara <dceara@redhat.com>\n\nRegards,\nDumitru","headers":{"Return-Path":"<ovs-dev-bounces@openvswitch.org>","X-Original-To":["incoming@patchwork.ozlabs.org","dev@openvswitch.org"],"Delivered-To":["patchwork-incoming@legolas.ozlabs.org","ovs-dev@lists.linuxfoundation.org"],"Authentication-Results":["legolas.ozlabs.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=Xj1oH6zj;\n\tdkim-atps=neutral","legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org\n (client-ip=140.211.166.137; helo=smtp4.osuosl.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org)","smtp4.osuosl.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key)\n header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=Xj1oH6zj","smtp2.osuosl.org; dmarc=pass (p=quarantine dis=none)\n header.from=redhat.com","smtp2.osuosl.org;\n dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com\n header.a=rsa-sha256 header.s=mimecast20190719 header.b=Xj1oH6zj"],"Received":["from smtp4.osuosl.org (smtp4.osuosl.org [140.211.166.137])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fsg9X0VLhz1yGS\n\tfor <incoming@patchwork.ozlabs.org>; Sat, 11 Apr 2026 01:06:08 +1000 (AEST)","from localhost (localhost [127.0.0.1])\n\tby smtp4.osuosl.org (Postfix) with ESMTP id 1530840A74;\n\tFri, 10 Apr 2026 15:06:06 +0000 (UTC)","from smtp4.osuosl.org ([127.0.0.1])\n by localhost (smtp4.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id RqfkVX8blmGE; Fri, 10 Apr 2026 15:06:04 +0000 (UTC)","from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56])\n\tby smtp4.osuosl.org (Postfix) with ESMTPS id 9611640A5B;\n\tFri, 10 Apr 2026 15:06:04 +0000 (UTC)","from lf-lists.osuosl.org (localhost [127.0.0.1])\n\tby lists.linuxfoundation.org (Postfix) with ESMTP id 6FCFCC054A;\n\tFri, 10 Apr 2026 15:06:04 +0000 (UTC)","from smtp2.osuosl.org (smtp2.osuosl.org [140.211.166.133])\n by lists.linuxfoundation.org (Postfix) with ESMTP id A9FB1C0549\n for <dev@openvswitch.org>; Fri, 10 Apr 2026 15:06:03 +0000 (UTC)","from localhost (localhost [127.0.0.1])\n by smtp2.osuosl.org (Postfix) with ESMTP id 8FEDF4084B\n for <dev@openvswitch.org>; Fri, 10 Apr 2026 15:06:03 +0000 (UTC)","from smtp2.osuosl.org ([127.0.0.1])\n by localhost (smtp2.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id 5MxM3o4iwxlr for <dev@openvswitch.org>;\n Fri, 10 Apr 2026 15:06:02 +0000 (UTC)","from us-smtp-delivery-124.mimecast.com\n (us-smtp-delivery-124.mimecast.com [170.10.133.124])\n by smtp2.osuosl.org (Postfix) with ESMTPS id 70C4340849\n for <dev@openvswitch.org>; Fri, 10 Apr 2026 15:06:01 +0000 (UTC)","from mail-wm1-f72.google.com (mail-wm1-f72.google.com\n [209.85.128.72]) by relay.mimecast.com with ESMTP with STARTTLS\n (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id\n us-mta-184-eXx_vq0mO-qIPA6PkGCGYA-1; Fri, 10 Apr 2026 11:05:58 -0400","by mail-wm1-f72.google.com with SMTP id\n 5b1f17b1804b1-4836abfc742so15803375e9.0\n for <dev@openvswitch.org>; Fri, 10 Apr 2026 08:05:58 -0700 (PDT)","from ?IPV6:2001:1c05:1417:d800:d1ef:9817:2a26:625d?\n (2001-1c05-1417-d800-d1ef-9817-2a26-625d.cable.dynamic.v6.ziggo.nl.\n [2001:1c05:1417:d800:d1ef:9817:2a26:625d])\n by smtp.gmail.com with ESMTPSA id\n 5b1f17b1804b1-488d5dbcb66sm23449845e9.9.2026.04.10.08.05.54\n (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128);\n Fri, 10 Apr 2026 08:05:54 -0700 (PDT)"],"X-Virus-Scanned":["amavis at osuosl.org","amavis at osuosl.org"],"X-Comment":"SPF check N/A for local connections - client-ip=140.211.9.56;\n helo=lists.linuxfoundation.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=<UNKNOWN> ","DKIM-Filter":["OpenDKIM Filter v2.11.0 smtp4.osuosl.org 9611640A5B","OpenDKIM Filter v2.11.0 smtp2.osuosl.org 70C4340849"],"Received-SPF":"Pass (mailfrom) identity=mailfrom; client-ip=170.10.133.124;\n helo=us-smtp-delivery-124.mimecast.com; envelope-from=dceara@redhat.com;\n receiver=<UNKNOWN>","DMARC-Filter":"OpenDMARC Filter v1.4.2 smtp2.osuosl.org 70C4340849","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1775833561;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:mime-version:mime-version:content-type:content-type:\n content-transfer-encoding:content-transfer-encoding:\n in-reply-to:in-reply-to:references:references;\n bh=2WfpbnFxS3ZRYOhLmM9IGLYURQS/l0afI9jIyRLXtGI=;\n b=Xj1oH6zjYaG/C+kNeQ3zN17XjbvtTP2gca6s2u8SInOMScHic1aEPGgW3sSncSkVneNX0A\n 1Cgxn19z/Nq5e/JC1Nmet021Izp8wbeqnmrE/DnKrg18ejrnknDQBXaVIqUKh55gmv1H76\n UifTBcJiNsgSY2Q+yls7yN67q8ak2hM=","X-MC-Unique":"eXx_vq0mO-qIPA6PkGCGYA-1","X-Mimecast-MFC-AGG-ID":"eXx_vq0mO-qIPA6PkGCGYA_1775833557","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1775833556; x=1776438356;\n h=content-transfer-encoding:in-reply-to:content-language:from\n :references:to:subject:user-agent:mime-version:date:message-id\n :x-gm-gg:x-gm-message-state:from:to:cc:subject:date:message-id\n :reply-to;\n bh=2WfpbnFxS3ZRYOhLmM9IGLYURQS/l0afI9jIyRLXtGI=;\n b=VKiQUTFn/ux1sbD9jtFSggRKyX9u0LIinJT7Uu6VBlvYe2UIe0l4yLNXFK806Z+r/f\n P1BZZ2rqkgs5EIdY5cmtgzhou2P+5vqIyhOINaLlJwOuYrmy4kbrUBsNOOZoBMtXvjil\n FGExroqF8lBxVrWZMAI3luY8dx7MVPUW2HqfWxmQ4snRIpu9hbsA/vtnqN3qDYfewI8/\n kvxHj2mZWq6YTfXBQNET0vCV9t59KFwXhdazPMDzTRvAKtviZN7UeXwFU4TW4TJmdR8+\n EArZdl4ludVdUTGBKuIRi2XR5xwH7WIa3PEpCUktQ/d25+fJTwHMTaJ9ohLwBzSXBYze\n cAjw==","X-Forwarded-Encrypted":"i=1;\n AJvYcCVgc4SoFDLDX8EewW1rCkKFv53hn0ZZYCk4x91mDBB9/Nv0/DbLvwlHwZ0O6JXHobfbsXk=@openvswitch.org","X-Gm-Message-State":"AOJu0YyGbxyJorUNeJP3pI8Pi5/AYSG82CMNB4tIM+7OXzWu3w4qFN5h\n NCagrNmHPgGL0k4dYELPjleFzKNN0SKgV7wfUN07e6phTXnUu2u9V+ZHAe2nEqu/6WCtPR1ItJR\n fkmw1PG14ze/xILZpLGCau94KPK2K5K63IZtNxLF2gTk5OLvAzJdCDkuG11XtyQ==","X-Gm-Gg":"AeBDietpyUWmtPQecrC6m12cpUPJCSMeplR219l68E/PpN/cUz0rsie6zh9MsfBIDsO\n Fhm2k7eCrwXFw3SiT/EL13mosM2s2tUo531OCyEhFuJatBmKskEUHcmO+KWnwBznl6UxU9X6UFP\n vabboqvVhZgef/6jOEJcj1hmwNeknQ9au/vx+lbbnUY6sI3p8D2nh4U1sc0HZY6QlqMrsrGcGlF\n eRwMGXw2xwZLBPeWdsgG2TfdBBFNtCmNCVfoWpyoMHY2XAFN0Ol++7sEy36nTmtZaBJvKQ4RMiD\n trVo9S+XwHk1On958Uo0ox5r91i4LUvw5KS/brO/oRdmT/ArE7uRl2qA0EuW1B1w2YuGIdIXND2\n 9kh17l7XBk8oQ1p9nR9JnpIhU4owbCN5b11cFH6mY7us0KsB1FRRq0/6hdhnkjXW7i9d3AtmVWh\n laqjHGCowlov/bqMjldlXJDPbKAdkgJUtFUS+nyEjpbxJcL9LadMqkLwgSHIC9rEc+HhhocjHm","X-Received":["by 2002:a05:600c:c173:b0:485:39d1:b4dd with SMTP id\n 5b1f17b1804b1-488d684b024mr47170465e9.10.1775833555878;\n Fri, 10 Apr 2026 08:05:55 -0700 (PDT)","by 2002:a05:600c:c173:b0:485:39d1:b4dd with SMTP id\n 5b1f17b1804b1-488d684b024mr47169845e9.10.1775833555410;\n Fri, 10 Apr 2026 08:05:55 -0700 (PDT)"],"Message-ID":"<3957ba70-10d6-4f4b-b2ed-d3b148cce66d@redhat.com>","Date":"Fri, 10 Apr 2026 17:05:53 +0200","MIME-Version":"1.0","User-Agent":"Mozilla Thunderbird","To":"Ales Musil <amusil@redhat.com>, dev@openvswitch.org","References":"<20260402082510.1417440-1-amusil@redhat.com>\n <20260402082510.1417440-5-amusil@redhat.com>","In-Reply-To":"<20260402082510.1417440-5-amusil@redhat.com>","X-Mimecast-Spam-Score":"0","X-Mimecast-MFC-PROC-ID":"U7sDIkh68atfZzvBbg0tq2YsqK85qwgPGRWxLgLDTSE_1775833557","X-Mimecast-Originator":"redhat.com","Content-Language":"en-US","Subject":"Re: [ovs-dev] [PATCH ovn 4/6] controller: Consolidate the netlink\n notifiers.","X-BeenThere":"ovs-dev@openvswitch.org","X-Mailman-Version":"2.1.30","Precedence":"list","List-Id":"<ovs-dev.openvswitch.org>","List-Unsubscribe":"<https://mail.openvswitch.org/mailman/options/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=unsubscribe>","List-Archive":"<http://mail.openvswitch.org/pipermail/ovs-dev/>","List-Post":"<mailto:ovs-dev@openvswitch.org>","List-Help":"<mailto:ovs-dev-request@openvswitch.org?subject=help>","List-Subscribe":"<https://mail.openvswitch.org/mailman/listinfo/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=subscribe>","From":"Dumitru Ceara via dev <ovs-dev@openvswitch.org>","Reply-To":"Dumitru Ceara <dceara@redhat.com>","Content-Type":"text/plain; charset=\"us-ascii\"","Content-Transfer-Encoding":"7bit","Errors-To":"ovs-dev-bounces@openvswitch.org","Sender":"\"dev\" <ovs-dev-bounces@openvswitch.org>"}},{"id":3675987,"web_url":"http://patchwork.ozlabs.org/comment/3675987/","msgid":"<adkpP1n5DQX4QAis@lore-desk>","list_archive_url":null,"date":"2026-04-10T16:45:51","subject":"Re: [ovs-dev] [PATCH ovn 4/6] controller: Consolidate the netlink\n notifiers.","submitter":{"id":73083,"url":"http://patchwork.ozlabs.org/api/people/73083/","name":"Lorenzo Bianconi","email":"lorenzo.bianconi@redhat.com"},"content":"> The netlink notifiers used a lot of code that was more or less\n> identical to each other. Extract the common code into separate module\n> which allows the definition of listeners and their specific data.\n> This should make it easier to add any new notifier, which will be the\n> case in the future. It should also make it slightly easier to track\n> individual updates and changes that could be processed incrementally\n> instead of full recompute when there is any change.\n> \n> Signed-off-by: Ales Musil <amusil@redhat.com>\n\nAcked-by: Lorenzo Bianconi <lorenzo.bianconi@redhat.com>\n\n> ---\n>  controller/automake.mk                        |  13 +-\n>  controller/neighbor-exchange.c                |   4 +-\n>  controller/neighbor-exchange.h                |   4 +-\n>  controller/neighbor-table-notify.c            | 244 -----------------\n>  controller/neighbor-table-notify.h            |  45 ----\n>  controller/ovn-controller.c                   | 169 ++++++++----\n>  ...ify-stub.c => ovn-netlink-notifier-stub.c} |  35 ++-\n>  controller/ovn-netlink-notifier.c             | 251 ++++++++++++++++++\n>  controller/ovn-netlink-notifier.h             |  38 +++\n>  controller/route-exchange-netlink.h           |   1 +\n>  controller/route-exchange.c                   |   4 +-\n>  controller/route-exchange.h                   |   2 +-\n>  controller/route-table-notify-stub.c          |  55 ----\n>  controller/route-table-notify.c               | 238 -----------------\n>  controller/route-table-notify.h               |  44 ---\n>  tests/automake.mk                             |   4 +-\n>  tests/system-ovn-netlink.at                   |  59 ++--\n>  tests/test-ovn-netlink.c                      |  55 ++--\n>  18 files changed, 502 insertions(+), 763 deletions(-)\n>  delete mode 100644 controller/neighbor-table-notify.c\n>  delete mode 100644 controller/neighbor-table-notify.h\n>  rename controller/{neighbor-table-notify-stub.c => ovn-netlink-notifier-stub.c} (51%)\n>  create mode 100644 controller/ovn-netlink-notifier.c\n>  create mode 100644 controller/ovn-netlink-notifier.h\n>  delete mode 100644 controller/route-table-notify-stub.c\n>  delete mode 100644 controller/route-table-notify.c\n>  delete mode 100644 controller/route-table-notify.h\n> \n> diff --git a/controller/automake.mk b/controller/automake.mk\n> index d6809df10..c37b89b6c 100644\n> --- a/controller/automake.mk\n> +++ b/controller/automake.mk\n> @@ -32,6 +32,7 @@ controller_ovn_controller_SOURCES = \\\n>  \tcontroller/lport.h \\\n>  \tcontroller/ofctrl.c \\\n>  \tcontroller/ofctrl.h \\\n> +\tcontroller/ovn-netlink-notifier.h \\\n>  \tcontroller/neighbor.c \\\n>  \tcontroller/neighbor.h \\\n>  \tcontroller/neighbor-of.c \\\n> @@ -63,33 +64,29 @@ controller_ovn_controller_SOURCES = \\\n>  \tcontroller/ecmp-next-hop-monitor.h \\\n>  \tcontroller/ecmp-next-hop-monitor.c \\\n>  \tcontroller/route-exchange.h \\\n> -\tcontroller/route-table-notify.h \\\n>  \tcontroller/route.h \\\n>  \tcontroller/route.c \\\n>  \tcontroller/garp_rarp.h \\\n>  \tcontroller/garp_rarp.c \\\n>  \tcontroller/neighbor-exchange.h \\\n> -\tcontroller/neighbor-table-notify.h \\\n>  \tcontroller/host-if-monitor.h\n>  \n>  if HAVE_NETLINK\n>  controller_ovn_controller_SOURCES += \\\n>  \tcontroller/host-if-monitor.c \\\n> +\tcontroller/ovn-netlink-notifier.c \\\n>  \tcontroller/neighbor-exchange-netlink.h \\\n>  \tcontroller/neighbor-exchange-netlink.c \\\n>  \tcontroller/neighbor-exchange.c \\\n> -\tcontroller/neighbor-table-notify.c \\\n>  \tcontroller/route-exchange-netlink.h \\\n>  \tcontroller/route-exchange-netlink.c \\\n> -\tcontroller/route-exchange.c \\\n> -\tcontroller/route-table-notify.c\n> +\tcontroller/route-exchange.c\n>  else\n>  controller_ovn_controller_SOURCES += \\\n>  \tcontroller/host-if-monitor-stub.c \\\n> +\tcontroller/ovn-netlink-notifier-stub.c \\\n>  \tcontroller/neighbor-exchange-stub.c \\\n> -\tcontroller/neighbor-table-notify-stub.c \\\n> -\tcontroller/route-exchange-stub.c \\\n> -\tcontroller/route-table-notify-stub.c\n> +\tcontroller/route-exchange-stub.c\n>  endif\n>  \n>  controller_ovn_controller_LDADD = lib/libovn.la $(OVS_LIBDIR)/libopenvswitch.la\n> diff --git a/controller/neighbor-exchange.c b/controller/neighbor-exchange.c\n> index e40f39e24..47e757712 100644\n> --- a/controller/neighbor-exchange.c\n> +++ b/controller/neighbor-exchange.c\n> @@ -21,7 +21,6 @@\n>  #include \"neighbor.h\"\n>  #include \"neighbor-exchange.h\"\n>  #include \"neighbor-exchange-netlink.h\"\n> -#include \"neighbor-table-notify.h\"\n>  #include \"openvswitch/poll-loop.h\"\n>  #include \"openvswitch/vlog.h\"\n>  #include \"ovn-util.h\"\n> @@ -136,8 +135,7 @@ neighbor_exchange_run(const struct neighbor_exchange_ctx_in *n_ctx_in,\n>              break;\n>          }\n>  \n> -        neighbor_table_add_watch_request(&n_ctx_out->neighbor_table_watches,\n> -                                         if_index, nim->if_name);\n> +        vector_push(n_ctx_out->neighbor_table_watches, &if_index);\n>          vector_destroy(&received_neighbors);\n>      }\n>  }\n> diff --git a/controller/neighbor-exchange.h b/controller/neighbor-exchange.h\n> index b4257f14c..32c87a8ab 100644\n> --- a/controller/neighbor-exchange.h\n> +++ b/controller/neighbor-exchange.h\n> @@ -30,8 +30,8 @@ struct neighbor_exchange_ctx_in {\n>  };\n>  \n>  struct neighbor_exchange_ctx_out {\n> -    /* Contains struct neighbor_table_watch_request. */\n> -    struct hmap neighbor_table_watches;\n> +    /* Contains int32_t representing if_index. */\n> +    struct vector *neighbor_table_watches;\n>      /* Contains 'struct evpn_remote_vtep'. */\n>      struct hmap *remote_vteps;\n>      /* Contains 'struct evpn_static_entry', remote FDB entries learned through\n> diff --git a/controller/neighbor-table-notify.c b/controller/neighbor-table-notify.c\n> deleted file mode 100644\n> index 04caa21df..000000000\n> --- a/controller/neighbor-table-notify.c\n> +++ /dev/null\n> @@ -1,244 +0,0 @@\n> -/* Copyright (c) 2025, Red Hat, Inc.\n> - *\n> - * Licensed under the Apache License, Version 2.0 (the \"License\");\n> - * you may not use this file except in compliance with the License.\n> - * You may obtain a copy of the License at:\n> - *\n> - *     http://www.apache.org/licenses/LICENSE-2.0\n> - *\n> - * Unless required by applicable law or agreed to in writing, software\n> - * distributed under the License is distributed on an \"AS IS\" BASIS,\n> - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n> - * See the License for the specific language governing permissions and\n> - * limitations under the License.\n> - */\n> -\n> -#include <config.h>\n> -\n> -#include <linux/rtnetlink.h>\n> -#include <net/if.h>\n> -\n> -#include \"hash.h\"\n> -#include \"hmapx.h\"\n> -#include \"lib/util.h\"\n> -#include \"netlink-notifier.h\"\n> -#include \"openvswitch/vlog.h\"\n> -\n> -#include \"neighbor-exchange-netlink.h\"\n> -#include \"neighbor-table-notify.h\"\n> -\n> -VLOG_DEFINE_THIS_MODULE(neighbor_table_notify);\n> -\n> -struct neighbor_table_watch_request {\n> -    struct hmap_node node;\n> -    int32_t if_index;\n> -    char if_name[IFNAMSIZ + 1];\n> -};\n> -\n> -struct neighbor_table_watch_entry {\n> -    struct hmap_node node;\n> -    int32_t if_index;\n> -    char if_name[IFNAMSIZ + 1];\n> -};\n> -\n> -static struct hmap watches = HMAP_INITIALIZER(&watches);\n> -static bool any_neighbor_table_changed;\n> -static struct ne_table_msg nln_nmsg_change;\n> -\n> -static struct nln *nl_neighbor_handle;\n> -static struct nln_notifier *nl_neighbor_notifier;\n> -\n> -static void neighbor_table_change(const void *change_, void *aux);\n> -\n> -static void\n> -neighbor_table_register_notifiers(void)\n> -{\n> -    VLOG_INFO(\"Adding neighbor table watchers.\");\n> -    ovs_assert(!nl_neighbor_handle);\n> -\n> -    nl_neighbor_handle = nln_create(NETLINK_ROUTE, ne_table_parse,\n> -                                    &nln_nmsg_change);\n> -    ovs_assert(nl_neighbor_handle);\n> -\n> -    nl_neighbor_notifier =\n> -        nln_notifier_create(nl_neighbor_handle, RTNLGRP_NEIGH,\n> -                            neighbor_table_change, NULL);\n> -    if (!nl_neighbor_notifier) {\n> -        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);\n> -        VLOG_WARN_RL(&rl, \"Failed to create neighbor table watcher.\");\n> -    }\n> -}\n> -\n> -static void\n> -neighbor_table_deregister_notifiers(void)\n> -{\n> -    VLOG_INFO(\"Removing neighbor table watchers.\");\n> -    ovs_assert(nl_neighbor_handle);\n> -\n> -    nln_notifier_destroy(nl_neighbor_notifier);\n> -    nln_destroy(nl_neighbor_handle);\n> -    nl_neighbor_notifier = NULL;\n> -    nl_neighbor_handle = NULL;\n> -}\n> -\n> -static uint32_t\n> -neighbor_table_notify_hash_watch(int32_t if_index)\n> -{\n> -    /* To allow lookups triggered by netlink messages, don't include the\n> -     * if_name in the hash.  The netlink updates only include if_index. */\n> -    return hash_int(if_index, 0);\n> -}\n> -\n> -static void\n> -add_watch_entry(int32_t if_index, const char *if_name)\n> -{\n> -   VLOG_DBG(\"Registering new neighbor table watcher \"\n> -            \"for interface %s (%\"PRId32\").\",\n> -            if_name, if_index);\n> -\n> -    struct neighbor_table_watch_entry *we;\n> -    uint32_t hash = neighbor_table_notify_hash_watch(if_index);\n> -    we = xzalloc(sizeof *we);\n> -    we->if_index = if_index;\n> -    ovs_strzcpy(we->if_name, if_name, sizeof we->if_name);\n> -    hmap_insert(&watches, &we->node, hash);\n> -\n> -    if (!nl_neighbor_handle) {\n> -        neighbor_table_register_notifiers();\n> -    }\n> -}\n> -\n> -static void\n> -remove_watch_entry(struct neighbor_table_watch_entry *we)\n> -{\n> -    VLOG_DBG(\"Removing neighbor table watcher for interface %s (%\"PRId32\").\",\n> -             we->if_name, we->if_index);\n> -    hmap_remove(&watches, &we->node);\n> -    free(we);\n> -\n> -    if (hmap_is_empty(&watches)) {\n> -        neighbor_table_deregister_notifiers();\n> -    }\n> -}\n> -\n> -bool\n> -neighbor_table_notify_run(void)\n> -{\n> -    any_neighbor_table_changed = false;\n> -\n> -    if (nl_neighbor_handle) {\n> -        nln_run(nl_neighbor_handle);\n> -    }\n> -\n> -    return any_neighbor_table_changed;\n> -}\n> -\n> -void\n> -neighbor_table_notify_wait(void)\n> -{\n> -    if (nl_neighbor_handle) {\n> -        nln_wait(nl_neighbor_handle);\n> -    }\n> -}\n> -\n> -void\n> -neighbor_table_add_watch_request(struct hmap *neighbor_table_watches,\n> -                                 int32_t if_index, const char *if_name)\n> -{\n> -    struct neighbor_table_watch_request *wr = xzalloc(sizeof *wr);\n> -\n> -    wr->if_index = if_index;\n> -    ovs_strzcpy(wr->if_name, if_name, sizeof wr->if_name);\n> -    hmap_insert(neighbor_table_watches, &wr->node,\n> -                neighbor_table_notify_hash_watch(wr->if_index));\n> -}\n> -\n> -void\n> -neighbor_table_watch_request_cleanup(struct hmap *neighbor_table_watches)\n> -{\n> -    struct neighbor_table_watch_request *wr;\n> -    HMAP_FOR_EACH_POP (wr, node, neighbor_table_watches) {\n> -        free(wr);\n> -    }\n> -}\n> -\n> -static struct neighbor_table_watch_entry *\n> -find_watch_entry(int32_t if_index, const char *if_name)\n> -{\n> -    struct neighbor_table_watch_entry *we;\n> -    uint32_t hash = neighbor_table_notify_hash_watch(if_index);\n> -    HMAP_FOR_EACH_WITH_HASH (we, node, hash, &watches) {\n> -        if (if_index == we->if_index && !strcmp(if_name, we->if_name)) {\n> -            return we;\n> -        }\n> -    }\n> -    return NULL;\n> -}\n> -\n> -static struct neighbor_table_watch_entry *\n> -find_watch_entry_by_if_index(int32_t if_index)\n> -{\n> -    struct neighbor_table_watch_entry *we;\n> -    uint32_t hash = neighbor_table_notify_hash_watch(if_index);\n> -    HMAP_FOR_EACH_WITH_HASH (we, node, hash, &watches) {\n> -        if (if_index == we->if_index) {\n> -            return we;\n> -        }\n> -    }\n> -    return NULL;\n> -}\n> -\n> -void\n> -neighbor_table_notify_update_watches(const struct hmap *neighbor_table_watches)\n> -{\n> -    struct hmapx sync_watches = HMAPX_INITIALIZER(&sync_watches);\n> -    struct neighbor_table_watch_entry *we;\n> -    HMAP_FOR_EACH (we, node, &watches) {\n> -        hmapx_add(&sync_watches, we);\n> -    }\n> -\n> -    struct neighbor_table_watch_request *wr;\n> -    HMAP_FOR_EACH (wr, node, neighbor_table_watches) {\n> -        we = find_watch_entry(wr->if_index, wr->if_name);\n> -        if (we) {\n> -            hmapx_find_and_delete(&sync_watches, we);\n> -        } else {\n> -            add_watch_entry(wr->if_index, wr->if_name);\n> -        }\n> -    }\n> -\n> -    struct hmapx_node *node;\n> -    HMAPX_FOR_EACH (node, &sync_watches) {\n> -        remove_watch_entry(node->data);\n> -    }\n> -\n> -    hmapx_destroy(&sync_watches);\n> -}\n> -\n> -void\n> -neighbor_table_notify_destroy(void)\n> -{\n> -    struct neighbor_table_watch_entry *we;\n> -    HMAP_FOR_EACH_SAFE (we, node, &watches) {\n> -        remove_watch_entry(we);\n> -    }\n> -}\n> -\n> -static void\n> -neighbor_table_change(const void *change_, void *aux OVS_UNUSED)\n> -{\n> -    /* We currently track whether at least one recent neighbor table change\n> -     * was detected.  If that's the case already there's no need to\n> -     * continue. */\n> -    if (any_neighbor_table_changed) {\n> -        return;\n> -    }\n> -\n> -    const struct ne_table_msg *change = change_;\n> -\n> -    if (change && !ne_is_ovn_owned(&change->nd)) {\n> -        if (find_watch_entry_by_if_index(change->nd.if_index)) {\n> -            any_neighbor_table_changed = true;\n> -        }\n> -    }\n> -}\n> diff --git a/controller/neighbor-table-notify.h b/controller/neighbor-table-notify.h\n> deleted file mode 100644\n> index 9f21271cc..000000000\n> --- a/controller/neighbor-table-notify.h\n> +++ /dev/null\n> @@ -1,45 +0,0 @@\n> -/* Copyright (c) 2025, Red Hat, Inc.\n> - *\n> - * Licensed under the Apache License, Version 2.0 (the \"License\");\n> - * you may not use this file except in compliance with the License.\n> - * You may obtain a copy of the License at:\n> - *\n> - *     http://www.apache.org/licenses/LICENSE-2.0\n> - *\n> - * Unless required by applicable law or agreed to in writing, software\n> - * distributed under the License is distributed on an \"AS IS\" BASIS,\n> - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n> - * See the License for the specific language governing permissions and\n> - * limitations under the License.\n> - */\n> -\n> -#ifndef NEIGHBOR_TABLE_NOTIFY_H\n> -#define NEIGHBOR_TABLE_NOTIFY_H 1\n> -\n> -#include <stdbool.h>\n> -#include \"openvswitch/hmap.h\"\n> -\n> -/* Returns true if any neighbor table has changed enough that we need\n> - * to learn new neighbor entries. */\n> -bool neighbor_table_notify_run(void);\n> -void neighbor_table_notify_wait(void);\n> -\n> -/* Add a watch request to the hmap. The hmap should later be passed to\n> - * neighbor_table_notify_update_watches*/\n> -void neighbor_table_add_watch_request(struct hmap *neighbor_table_watches,\n> -                                      int32_t if_index, const char *if_name);\n> -\n> -/* Cleanup all watch request in the provided hmap that where added using\n> - * neighbor_table_add_watch_request. */\n> -void neighbor_table_watch_request_cleanup(\n> -    struct hmap *neighbor_table_watches);\n> -\n> -/* Updates the list of neighbor table watches that are currently active.\n> - * hmap should contain struct neighbor_table_watch_request */\n> -void neighbor_table_notify_update_watches(\n> -    const struct hmap *neighbor_table_watches);\n> -\n> -/* Cleans up all neighbor table watches. */\n> -void neighbor_table_notify_destroy(void);\n> -\n> -#endif /* NEIGHBOR_TABLE_NOTIFY_H */\n> diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c\n> index 5b7eb3014..41abd3bae 100644\n> --- a/controller/ovn-controller.c\n> +++ b/controller/ovn-controller.c\n> @@ -53,6 +53,7 @@\n>  #include \"openvswitch/vlog.h\"\n>  #include \"ovn/actions.h\"\n>  #include \"ovn/features.h\"\n> +#include \"ovn-netlink-notifier.h\"\n>  #include \"lib/chassis-index.h\"\n>  #include \"lib/extend-table.h\"\n>  #include \"lib/ip-mcast-index.h\"\n> @@ -92,12 +93,12 @@\n>  #include \"acl-ids.h\"\n>  #include \"route.h\"\n>  #include \"route-exchange.h\"\n> -#include \"route-table-notify.h\"\n> +#include \"route-table.h\"\n>  #include \"garp_rarp.h\"\n>  #include \"host-if-monitor.h\"\n>  #include \"neighbor.h\"\n>  #include \"neighbor-exchange.h\"\n> -#include \"neighbor-table-notify.h\"\n> +#include \"neighbor-exchange-netlink.h\"\n>  #include \"evpn-arp.h\"\n>  #include \"evpn-binding.h\"\n>  #include \"evpn-fdb.h\"\n> @@ -5610,6 +5611,30 @@ route_sb_datapath_binding_handler(struct engine_node *node,\n>      return EN_HANDLED_UNCHANGED;\n>  }\n>  \n> +static int\n> +table_id_cmp(const void *a_, const void *b_)\n> +{\n> +    const uint32_t *a = a_;\n> +    const uint32_t *b = b_;\n> +\n> +    return *a < *b ? -1 : *a > *b;\n> +}\n> +\n> +static void\n> +route_table_notify_update(struct vector *watches)\n> +{\n> +    vector_qsort(watches, table_id_cmp);\n> +\n> +    bool enabled = !vector_is_empty(watches);\n> +    ovn_netlink_update_notifier(OVN_NL_NOTIFIER_ROUTE_V4, enabled);\n> +    ovn_netlink_update_notifier(OVN_NL_NOTIFIER_ROUTE_V6, enabled);\n> +}\n> +\n> +struct ed_type_route_table_notify {\n> +    /* Vector of ordered 'uint32_t' representing table_ids. */\n> +    struct vector watches;\n> +};\n> +\n>  struct ed_type_route_exchange {\n>      /* We need the idl to check if the Learned_Route table exists. */\n>      struct ovsdb_idl *sb_idl;\n> @@ -5635,6 +5660,8 @@ en_route_exchange_run(struct engine_node *node, void *data)\n>  \n>      struct ed_type_route *route_data =\n>          engine_get_input_data(\"route\", node);\n> +    struct ed_type_route_table_notify *rt_notify =\n> +        engine_get_input_data(\"route_table_notify\", node);\n>  \n>      /* There can not actually be any routes to advertise unless we also have\n>       * the Learned_Route table, since they where introduced in the same\n> @@ -5643,6 +5670,8 @@ en_route_exchange_run(struct engine_node *node, void *data)\n>          return EN_STALE;\n>      }\n>  \n> +    vector_clear(&rt_notify->watches);\n> +\n>      struct route_exchange_ctx_in r_ctx_in = {\n>          .ovnsb_idl_txn = engine_get_context()->ovnsb_idl_txn,\n>          .sbrec_learned_route_by_datapath = sbrec_learned_route_by_datapath,\n> @@ -5651,15 +5680,11 @@ en_route_exchange_run(struct engine_node *node, void *data)\n>      };\n>      struct route_exchange_ctx_out r_ctx_out = {\n>          .sb_changes_pending = false,\n> +        .route_table_watches = &rt_notify->watches,\n>      };\n>  \n> -    hmap_init(&r_ctx_out.route_table_watches);\n> -\n>      route_exchange_run(&r_ctx_in, &r_ctx_out);\n> -    route_table_notify_update_watches(&r_ctx_out.route_table_watches);\n> -\n> -    route_table_watch_request_cleanup(&r_ctx_out.route_table_watches);\n> -    hmap_destroy(&r_ctx_out.route_table_watches);\n> +    route_table_notify_update(&rt_notify->watches);\n>  \n>      re->sb_changes_pending = r_ctx_out.sb_changes_pending;\n>  \n> @@ -5693,23 +5718,40 @@ en_route_exchange_cleanup(void *data OVS_UNUSED)\n>  {\n>  }\n>  \n> -struct ed_type_route_table_notify {\n> -    /* For incremental processing this could be tracked per datapath in\n> -     * the future. */\n> -    bool changed;\n> -};\n> -\n> +/* The route_table_notify node is an input node, but the watches are\n> + * populated by route_exchange node. The reason being that engine\n> + * periodically runs input nodes to check if there are updates, so it\n> + * could process the other nodes, however the route_table_notify cannot\n> + * be dependent on other node because it wouldn't be input node anymore. */\n>  static enum engine_node_state\n>  en_route_table_notify_run(struct engine_node *node OVS_UNUSED, void *data)\n>  {\n> +    enum engine_node_state state = EN_UNCHANGED;\n>      struct ed_type_route_table_notify *rtn = data;\n> -    enum engine_node_state state;\n> -    if (rtn->changed) {\n> -        state = EN_UPDATED;\n> -    } else {\n> -        state = EN_UNCHANGED;\n> +    struct vector *msgs;\n> +    uint32_t *table_id;\n> +\n> +    msgs = ovn_netlink_get_msgs(OVN_NL_NOTIFIER_ROUTE_V4);\n> +    VECTOR_FOR_EACH_PTR (msgs, table_id) {\n> +        if (vector_bsearch(&rtn->watches, table_id, table_id_cmp)) {\n> +            state = EN_UPDATED;\n> +            break;\n> +        }\n>      }\n> -    rtn->changed = false;\n> +\n> +    if (state != EN_UPDATED) {\n> +        msgs = ovn_netlink_get_msgs(OVN_NL_NOTIFIER_ROUTE_V6);\n> +        VECTOR_FOR_EACH_PTR (msgs, table_id) {\n> +            if (vector_bsearch(&rtn->watches, table_id, table_id_cmp)) {\n> +                state = EN_UPDATED;\n> +                break;\n> +            }\n> +        }\n> +    }\n> +\n> +    ovn_netlink_notifier_flush(OVN_NL_NOTIFIER_ROUTE_V4);\n> +    ovn_netlink_notifier_flush(OVN_NL_NOTIFIER_ROUTE_V6);\n> +\n>      return state;\n>  }\n>  \n> @@ -5718,14 +5760,19 @@ static void *\n>  en_route_table_notify_init(struct engine_node *node OVS_UNUSED,\n>                             struct engine_arg *arg OVS_UNUSED)\n>  {\n> -    struct ed_type_route_table_notify *rtn = xzalloc(sizeof *rtn);\n> -    rtn->changed = true;\n> +    struct ed_type_route_table_notify *rtn = xmalloc(sizeof *rtn);\n> +\n> +    *rtn = (struct ed_type_route_table_notify) {\n> +        .watches = VECTOR_EMPTY_INITIALIZER(uint32_t),\n> +    };\n>      return rtn;\n>  }\n>  \n>  static void\n>  en_route_table_notify_cleanup(void *data OVS_UNUSED)\n>  {\n> +    struct ed_type_route_table_notify *rtn = data;\n> +    vector_destroy(&rtn->watches);\n>  }\n>  \n>  struct ed_type_route_exchange_status {\n> @@ -6226,10 +6273,32 @@ neighbor_sb_port_binding_handler(struct engine_node *node, void *data)\n>      return EN_HANDLED_UNCHANGED;\n>  }\n>  \n> +static int\n> +if_index_cmp(const void *a_, const void *b_)\n> +{\n> +    const int32_t *a = a_;\n> +    const int32_t *b = b_;\n> +\n> +    return *a < *b ? -1 : *a > *b;\n> +}\n> +\n> +static void\n> +neighbor_table_notify_update(struct vector *watches)\n> +{\n> +    vector_qsort(watches, if_index_cmp);\n> +\n> +    bool enabled = !vector_is_empty(watches);\n> +    ovn_netlink_update_notifier(OVN_NL_NOTIFIER_NEIGHBOR, enabled);\n> +}\n> +\n> +/* The neighbor_table_notify node is an input node, but the watches are\n> + * populated by en_neighbor_exchange node. The reason being that engine\n> + * periodically runs input nodes to check if there are updates, so it\n> + * could process the other nodes, however the neighbor_table_notify cannot\n> + * be dependent on other node because it wouldn't be input node anymore. */\n>  struct ed_type_neighbor_table_notify {\n> -    /* For incremental processing this could be tracked per interface in\n> -     * the future. */\n> -    bool changed;\n> +    /* Vector of ordered 'int32_t' representing if_indexes. */\n> +    struct vector watches;\n>  };\n>  \n>  static void *\n> @@ -6239,7 +6308,7 @@ en_neighbor_table_notify_init(struct engine_node *node OVS_UNUSED,\n>      struct ed_type_neighbor_table_notify *ntn = xmalloc(sizeof *ntn);\n>  \n>      *ntn = (struct ed_type_neighbor_table_notify) {\n> -        .changed = true,\n> +        .watches = VECTOR_EMPTY_INITIALIZER(int32_t),\n>      };\n>      return ntn;\n>  }\n> @@ -6247,20 +6316,31 @@ en_neighbor_table_notify_init(struct engine_node *node OVS_UNUSED,\n>  static void\n>  en_neighbor_table_notify_cleanup(void *data OVS_UNUSED)\n>  {\n> +    struct ed_type_neighbor_table_notify *ntn = data;\n> +    vector_destroy(&ntn->watches);\n>  }\n>  \n>  static enum engine_node_state\n>  en_neighbor_table_notify_run(struct engine_node *node OVS_UNUSED,\n>                               void *data)\n>  {\n> +    enum engine_node_state state = EN_UNCHANGED;\n>      struct ed_type_neighbor_table_notify *ntn = data;\n> -    enum engine_node_state state;\n> -    if (ntn->changed) {\n> -        state = EN_UPDATED;\n> -    } else {\n> -        state = EN_UNCHANGED;\n> +    struct vector *msgs;\n> +    struct ne_table_msg *ne_msg;\n> +\n> +    msgs = ovn_netlink_get_msgs(OVN_NL_NOTIFIER_NEIGHBOR);\n> +    VECTOR_FOR_EACH_PTR (msgs, ne_msg) {\n> +        if (vector_bsearch(&ntn->watches,\n> +                           &ne_msg->nd.if_index,\n> +                           if_index_cmp)) {\n> +            state = EN_UPDATED;\n> +            break;\n> +        }\n>      }\n> -    ntn->changed = false;\n> +\n> +    ovn_netlink_notifier_flush(OVN_NL_NOTIFIER_NEIGHBOR);\n> +\n>      return state;\n>  }\n>  \n> @@ -6307,27 +6387,26 @@ en_neighbor_exchange_run(struct engine_node *node, void *data_)\n>      struct ed_type_neighbor_exchange *data = data_;\n>      const struct ed_type_neighbor *neighbor_data =\n>          engine_get_input_data(\"neighbor\", node);\n> +    struct ed_type_neighbor_table_notify *nt_notify =\n> +        engine_get_input_data(\"neighbor_table_notify\", node);\n>  \n>      evpn_remote_vteps_clear(&data->remote_vteps);\n>      evpn_static_entries_clear(&data->static_fdbs);\n>      evpn_static_entries_clear(&data->static_arps);\n> +    vector_clear(&nt_notify->watches);\n>  \n>      struct neighbor_exchange_ctx_in n_ctx_in = {\n>          .monitored_interfaces = &neighbor_data->monitored_interfaces,\n>      };\n>      struct neighbor_exchange_ctx_out n_ctx_out = {\n> -        .neighbor_table_watches =\n> -            HMAP_INITIALIZER(&n_ctx_out.neighbor_table_watches),\n> +        .neighbor_table_watches = &nt_notify->watches,\n>          .remote_vteps = &data->remote_vteps,\n>          .static_fdbs = &data->static_fdbs,\n>          .static_arps = &data->static_arps,\n>      };\n>  \n>      neighbor_exchange_run(&n_ctx_in, &n_ctx_out);\n> -    neighbor_table_notify_update_watches(&n_ctx_out.neighbor_table_watches);\n> -\n> -    neighbor_table_watch_request_cleanup(&n_ctx_out.neighbor_table_watches);\n> -    hmap_destroy(&n_ctx_out.neighbor_table_watches);\n> +    neighbor_table_notify_update(&nt_notify->watches);\n>  \n>      return EN_UPDATED;\n>  }\n> @@ -7792,18 +7871,12 @@ main(int argc, char *argv[])\n>                                 &transport_zones,\n>                                 bridge_table);\n>  \n> -                    struct ed_type_route_table_notify *rtn =\n> -                        engine_get_internal_data(&en_route_table_notify);\n> -                    rtn->changed = route_table_notify_run();\n> +                    ovn_netlink_notifiers_run();\n>  \n>                      struct ed_type_host_if_monitor *hifm =\n>                          engine_get_internal_data(&en_host_if_monitor);\n>                      hifm->changed = host_if_monitor_run();\n>  \n> -                    struct ed_type_neighbor_table_notify *ntn =\n> -                        engine_get_internal_data(&en_neighbor_table_notify);\n> -                    ntn->changed = neighbor_table_notify_run();\n> -\n>                      struct ed_type_route_exchange_status *rt_res =\n>                          engine_get_internal_data(&en_route_exchange_status);\n>                      rt_res->netlink_trigger_run =\n> @@ -8131,9 +8204,8 @@ main(int argc, char *argv[])\n>              }\n>  \n>              binding_wait();\n> -            route_table_notify_wait();\n>              host_if_monitor_wait();\n> -            neighbor_table_notify_wait();\n> +            ovn_netlink_notifiers_wait();\n>          }\n>  \n>          unixctl_server_run(unixctl);\n> @@ -8306,8 +8378,7 @@ loop_done:\n>      ovsrcu_exit();\n>      dns_resolve_destroy();\n>      route_exchange_destroy();\n> -    route_table_notify_destroy();\n> -    neighbor_table_notify_destroy();\n> +    ovn_netlink_notifiers_destroy();\n>  \n>      exit(retval);\n>  }\n> diff --git a/controller/neighbor-table-notify-stub.c b/controller/ovn-netlink-notifier-stub.c\n> similarity index 51%\n> rename from controller/neighbor-table-notify-stub.c\n> rename to controller/ovn-netlink-notifier-stub.c\n> index bb4fe5991..a90aa6a4a 100644\n> --- a/controller/neighbor-table-notify-stub.c\n> +++ b/controller/ovn-netlink-notifier-stub.c\n> @@ -1,4 +1,5 @@\n> -/* Copyright (c) 2025, Red Hat, Inc.\n> +/* Copyright (c) 2025, STACKIT GmbH & Co. KG\n> + * Copyright (c) 2026, Red Hat, Inc.\n>   *\n>   * Licensed under the Apache License, Version 2.0 (the \"License\");\n>   * you may not use this file except in compliance with the License.\n> @@ -14,44 +15,42 @@\n>   */\n>  \n>  #include <config.h>\n> -\n> -#include <stdbool.h>\n> +#include <stddef.h>\n>  \n>  #include \"openvswitch/compiler.h\"\n> -#include \"neighbor-table-notify.h\"\n> +#include \"ovn-netlink-notifier.h\"\n> +#include \"vec.h\"\n> +\n> +static struct vector empty = VECTOR_EMPTY_INITIALIZER(uint8_t);\n>  \n> -bool\n> -neighbor_table_notify_run(void)\n> +void\n> +ovn_netlink_update_notifier(enum ovn_netlink_notifier_type type OVS_UNUSED,\n> +                            bool enabled OVS_UNUSED)\n>  {\n> -    return false;\n>  }\n>  \n> -void\n> -neighbor_table_notify_wait(void)\n> +struct vector *\n> +ovn_netlink_get_msgs(enum ovn_netlink_notifier_type type OVS_UNUSED)\n>  {\n> +    return &empty;\n>  }\n>  \n>  void\n> -neighbor_table_add_watch_request(\n> -    struct hmap *neighbor_table_watches OVS_UNUSED,\n> -    int32_t if_index OVS_UNUSED,\n> -    const char *if_name OVS_UNUSED)\n> +ovn_netlink_notifier_flush(enum ovn_netlink_notifier_type type OVS_UNUSED)\n>  {\n>  }\n>  \n>  void\n> -neighbor_table_watch_request_cleanup(\n> -    struct hmap *neighbor_table_watches OVS_UNUSED)\n> +ovn_netlink_notifiers_run(void)\n>  {\n>  }\n>  \n>  void\n> -neighbor_table_notify_update_watches(\n> -    const struct hmap *neighbor_table_watches OVS_UNUSED)\n> +ovn_netlink_notifiers_wait(void)\n>  {\n>  }\n>  \n>  void\n> -neighbor_table_notify_destroy(void)\n> +ovn_netlink_notifiers_destroy(void)\n>  {\n>  }\n> diff --git a/controller/ovn-netlink-notifier.c b/controller/ovn-netlink-notifier.c\n> new file mode 100644\n> index 000000000..defa1cd54\n> --- /dev/null\n> +++ b/controller/ovn-netlink-notifier.c\n> @@ -0,0 +1,251 @@\n> +/* Copyright (c) 2026, Red Hat, Inc.\n> + *\n> + * Licensed under the Apache License, Version 2.0 (the \"License\");\n> + * you may not use this file except in compliance with the License.\n> + * You may obtain a copy of the License at:\n> + *\n> + *     http://www.apache.org/licenses/LICENSE-2.0\n> + *\n> + * Unless required by applicable law or agreed to in writing, software\n> + * distributed under the License is distributed on an \"AS IS\" BASIS,\n> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n> + * See the License for the specific language governing permissions and\n> + * limitations under the License.\n> + */\n> +\n> +#include <config.h>\n> +\n> +#include <linux/rtnetlink.h>\n> +#include <net/if.h>\n> +\n> +#include \"neighbor-exchange-netlink.h\"\n> +#include \"netlink-notifier.h\"\n> +#include \"route-exchange-netlink.h\"\n> +#include \"route-table.h\"\n> +#include \"vec.h\"\n> +\n> +#include \"openvswitch/vlog.h\"\n> +\n> +#include \"ovn-netlink-notifier.h\"\n> +\n> +VLOG_DEFINE_THIS_MODULE(ovn_netlink_notifier);\n> +\n> +#define NOTIFIER_MSGS_CAPACITY_THRESHOLD 1024\n> +\n> +struct ovn_netlink_notifier {\n> +    /* Group for which we want to receive the notification. */\n> +    int group;\n> +    /* The notifier pointers. */\n> +    struct nln_notifier *nln_notifier;\n> +    /* Messages received by given notifier. */\n> +    struct vector msgs;\n> +    /* Notifier change handler. */\n> +    nln_notify_func *change_handler;\n> +    /* Name of the notifier. */\n> +    const char *name;\n> +};\n> +\n> +union ovn_notifier_msg_change {\n> +    struct route_table_msg route;\n> +    struct ne_table_msg neighbor;\n> +};\n> +\n> +static void ovn_netlink_route_change_handler(const void *change_, void *aux);\n> +static void ovn_netlink_neighbor_change_handler(const void *change_,\n> +                                                void *aux);\n> +\n> +static struct ovn_netlink_notifier notifiers[OVN_NL_NOTIFIER_MAX] = {\n> +    [OVN_NL_NOTIFIER_ROUTE_V4] = {\n> +        .group = RTNLGRP_IPV4_ROUTE,\n> +        .msgs = VECTOR_EMPTY_INITIALIZER(uint32_t),\n> +        .change_handler = ovn_netlink_route_change_handler,\n> +        .name = \"route-ipv4\",\n> +    },\n> +    [OVN_NL_NOTIFIER_ROUTE_V6] = {\n> +        .group = RTNLGRP_IPV6_ROUTE,\n> +        .msgs = VECTOR_EMPTY_INITIALIZER(uint32_t),\n> +        .change_handler = ovn_netlink_route_change_handler,\n> +        .name = \"route-ipv6\",\n> +    },\n> +    [OVN_NL_NOTIFIER_NEIGHBOR] = {\n> +        .group = RTNLGRP_NEIGH,\n> +        .msgs = VECTOR_EMPTY_INITIALIZER(struct ne_table_msg),\n> +        .change_handler = ovn_netlink_neighbor_change_handler,\n> +        .name = \"neighbor\",\n> +    },\n> +};\n> +\n> +static struct nln *nln_handle;\n> +static union ovn_notifier_msg_change nln_msg_change;\n> +\n> +static int\n> +ovn_netlink_notifier_parse(struct ofpbuf *buf, void *change_)\n> +{\n> +    struct nlmsghdr *nlmsg = ofpbuf_at(buf, 0, NLMSG_HDRLEN);\n> +    if (!nlmsg) {\n> +        return 0;\n> +    }\n> +\n> +    union ovn_notifier_msg_change *change = change_;\n> +    if (nlmsg->nlmsg_type == RTM_NEWROUTE ||\n> +        nlmsg->nlmsg_type == RTM_DELROUTE) {\n> +        return route_table_parse(buf, &change->route);\n> +    }\n> +\n> +    if (nlmsg->nlmsg_type == RTM_NEWNEIGH ||\n> +        nlmsg->nlmsg_type == RTM_DELNEIGH) {\n> +        return ne_table_parse(buf, &change->neighbor);\n> +    }\n> +\n> +    return 0;\n> +}\n> +\n> +static void\n> +ovn_netlink_route_change_handler(const void *change_, void *aux)\n> +{\n> +    if (!change_) {\n> +        return;\n> +    }\n> +\n> +    struct ovn_netlink_notifier *notifier = aux;\n> +    union ovn_notifier_msg_change *change =\n> +        CONST_CAST(union ovn_notifier_msg_change *, change_);\n> +\n> +    struct route_data *rd = &change->route.rd;\n> +    if (rd->rtm_protocol != RTPROT_OVN) {\n> +        /* We just cannot copy the whole route_data because it has reference\n> +         * to self for the nexthop list. */\n> +        vector_push(&notifier->msgs, &rd->rta_table_id);\n> +    }\n> +\n> +    route_data_destroy(rd);\n> +}\n> +\n> +static void\n> +ovn_netlink_neighbor_change_handler(const void *change_, void *aux)\n> +{\n> +    if (!change_) {\n> +        return;\n> +    }\n> +\n> +    struct ovn_netlink_notifier *notifier = aux;\n> +    const union ovn_notifier_msg_change *change = change_;\n> +\n> +    if (!ne_is_ovn_owned(&change->neighbor.nd)) {\n> +        vector_push(&notifier->msgs, &change->neighbor);\n> +    }\n> +}\n> +\n> +static void\n> +ovn_netlink_register_notifier(enum ovn_netlink_notifier_type type)\n> +{\n> +    ovs_assert(type < OVN_NL_NOTIFIER_MAX);\n> +\n> +    struct ovn_netlink_notifier *notifier = &notifiers[type];\n> +    if (notifier->nln_notifier) {\n> +        return;\n> +    }\n> +\n> +    VLOG_INFO(\"Adding %s table watchers.\", notifier->name);\n> +    if (!nln_handle) {\n> +        nln_handle = nln_create(NETLINK_ROUTE, ovn_netlink_notifier_parse,\n> +                                &nln_msg_change);\n> +        ovs_assert(nln_handle);\n> +    }\n> +\n> +    notifier->nln_notifier = nln_notifier_create(nln_handle, notifier->group,\n> +                                                 notifier->change_handler,\n> +                                                 notifier);\n> +\n> +    if (!notifier->nln_notifier) {\n> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);\n> +        VLOG_WARN_RL(&rl, \"Failed to create %s table watcher.\",\n> +                     notifier->name);\n> +    }\n> +}\n> +\n> +static void\n> +ovn_netlink_deregister_notifier(enum ovn_netlink_notifier_type type)\n> +{\n> +    ovs_assert(type < OVN_NL_NOTIFIER_MAX);\n> +\n> +    struct ovn_netlink_notifier *notifier = &notifiers[type];\n> +    if (!notifier->nln_notifier) {\n> +        return;\n> +    }\n> +\n> +    VLOG_INFO(\"Removing %s table watchers.\", notifier->name);\n> +    nln_notifier_destroy(notifier->nln_notifier);\n> +    notifier->nln_notifier = NULL;\n> +\n> +    size_t i;\n> +    for (i = 0; i < OVN_NL_NOTIFIER_MAX; i++) {\n> +        if (notifiers[i].nln_notifier) {\n> +            break;\n> +        }\n> +    }\n> +\n> +    if (i == OVN_NL_NOTIFIER_MAX) {\n> +        /* This was the last notifier, destroy the handle too. */\n> +        nln_destroy(nln_handle);\n> +        nln_handle = NULL;\n> +    }\n> +}\n> +\n> +void\n> +ovn_netlink_update_notifier(enum ovn_netlink_notifier_type type, bool enabled)\n> +{\n> +    if (enabled) {\n> +        ovn_netlink_register_notifier(type);\n> +    } else {\n> +        ovn_netlink_deregister_notifier(type);\n> +    }\n> +}\n> +\n> +struct vector *\n> +ovn_netlink_get_msgs(enum ovn_netlink_notifier_type type)\n> +{\n> +    ovs_assert(type < OVN_NL_NOTIFIER_MAX);\n> +    return &notifiers[type].msgs;\n> +}\n> +\n> +void\n> +ovn_netlink_notifier_flush(enum ovn_netlink_notifier_type type)\n> +{\n> +    ovs_assert(type < OVN_NL_NOTIFIER_MAX);\n> +    struct ovn_netlink_notifier *notifier = &notifiers[type];\n> +    vector_clear(&notifier->msgs);\n> +}\n> +\n> +void\n> +ovn_netlink_notifiers_run(void)\n> +{\n> +    for (size_t i = 0; i < OVN_NL_NOTIFIER_MAX; i++) {\n> +        if (vector_capacity(&notifiers[i].msgs) >\n> +            NOTIFIER_MSGS_CAPACITY_THRESHOLD) {\n> +            vector_shrink_to_fit(&notifiers[i].msgs);\n> +        }\n> +    }\n> +\n> +    if (nln_handle) {\n> +        nln_run(nln_handle);\n> +    }\n> +}\n> +\n> +void\n> +ovn_netlink_notifiers_wait(void)\n> +{\n> +    if (nln_handle) {\n> +        nln_wait(nln_handle);\n> +    }\n> +}\n> +\n> +void\n> +ovn_netlink_notifiers_destroy(void)\n> +{\n> +    for (size_t i = 0; i < OVN_NL_NOTIFIER_MAX; i++) {\n> +        ovn_netlink_notifier_flush(i);\n> +        ovn_netlink_deregister_notifier(i);\n> +        vector_destroy(&notifiers[i].msgs);\n> +    }\n> +}\n> diff --git a/controller/ovn-netlink-notifier.h b/controller/ovn-netlink-notifier.h\n> new file mode 100644\n> index 000000000..b78fe466b\n> --- /dev/null\n> +++ b/controller/ovn-netlink-notifier.h\n> @@ -0,0 +1,38 @@\n> +/* Copyright (c) 2026, Red Hat, Inc.\n> + *\n> + * Licensed under the Apache License, Version 2.0 (the \"License\");\n> + * you may not use this file except in compliance with the License.\n> + * You may obtain a copy of the License at:\n> + *\n> + *     http://www.apache.org/licenses/LICENSE-2.0\n> + *\n> + * Unless required by applicable law or agreed to in writing, software\n> + * distributed under the License is distributed on an \"AS IS\" BASIS,\n> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n> + * See the License for the specific language governing permissions and\n> + * limitations under the License.\n> + */\n> +\n> +#ifndef OVN_NETLINK_NOTIFIER_H\n> +#define OVN_NETLINK_NOTIFIER_H 1\n> +\n> +#include <stdbool.h>\n> +\n> +struct vector;\n> +\n> +enum ovn_netlink_notifier_type {\n> +    OVN_NL_NOTIFIER_ROUTE_V4,\n> +    OVN_NL_NOTIFIER_ROUTE_V6,\n> +    OVN_NL_NOTIFIER_NEIGHBOR,\n> +    OVN_NL_NOTIFIER_MAX,\n> +};\n> +\n> +void ovn_netlink_update_notifier(enum ovn_netlink_notifier_type type,\n> +                                 bool enabled);\n> +struct vector *ovn_netlink_get_msgs(enum ovn_netlink_notifier_type type);\n> +void ovn_netlink_notifier_flush(enum ovn_netlink_notifier_type type);\n> +void ovn_netlink_notifiers_run(void);\n> +void ovn_netlink_notifiers_wait(void);\n> +void ovn_netlink_notifiers_destroy(void);\n> +\n> +#endif /* OVN_NETLINK_NOTIFIER_H */\n> diff --git a/controller/route-exchange-netlink.h b/controller/route-exchange-netlink.h\n> index 3ebd4546f..8ba8a1039 100644\n> --- a/controller/route-exchange-netlink.h\n> +++ b/controller/route-exchange-netlink.h\n> @@ -39,6 +39,7 @@\n>  struct in6_addr;\n>  struct hmap;\n>  struct vector;\n> +struct advertise_route_entry;\n>  \n>  struct re_nl_received_route_node {\n>      const struct sbrec_datapath_binding *db;\n> diff --git a/controller/route-exchange.c b/controller/route-exchange.c\n> index ae44ffe69..82727f4e4 100644\n> --- a/controller/route-exchange.c\n> +++ b/controller/route-exchange.c\n> @@ -31,7 +31,6 @@\n>  #include \"ha-chassis.h\"\n>  #include \"local_data.h\"\n>  #include \"route.h\"\n> -#include \"route-table-notify.h\"\n>  #include \"route-exchange.h\"\n>  #include \"route-exchange-netlink.h\"\n>  \n> @@ -306,8 +305,7 @@ route_exchange_run(const struct route_exchange_ctx_in *r_ctx_in,\n>                                 r_ctx_in->sbrec_learned_route_by_datapath,\n>                                 &r_ctx_out->sb_changes_pending);\n>  \n> -        route_table_add_watch_request(&r_ctx_out->route_table_watches,\n> -                                      table_id);\n> +        vector_push(r_ctx_out->route_table_watches, &table_id);\n>  \n>          vector_destroy(&received_routes);\n>      }\n> diff --git a/controller/route-exchange.h b/controller/route-exchange.h\n> index e3791c331..25db35568 100644\n> --- a/controller/route-exchange.h\n> +++ b/controller/route-exchange.h\n> @@ -30,7 +30,7 @@ struct route_exchange_ctx_in {\n>  };\n>  \n>  struct route_exchange_ctx_out {\n> -    struct hmap route_table_watches;\n> +    struct vector *route_table_watches;\n>      bool sb_changes_pending;\n>  };\n>  \n> diff --git a/controller/route-table-notify-stub.c b/controller/route-table-notify-stub.c\n> deleted file mode 100644\n> index 460c81dbe..000000000\n> --- a/controller/route-table-notify-stub.c\n> +++ /dev/null\n> @@ -1,55 +0,0 @@\n> -/*\n> - * Copyright (c) 2025, STACKIT GmbH & Co. KG\n> - *\n> - * Licensed under the Apache License, Version 2.0 (the \"License\");\n> - * you may not use this file except in compliance with the License.\n> - * You may obtain a copy of the License at:\n> - *\n> - *     http://www.apache.org/licenses/LICENSE-2.0\n> - *\n> - * Unless required by applicable law or agreed to in writing, software\n> - * distributed under the License is distributed on an \"AS IS\" BASIS,\n> - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n> - * See the License for the specific language governing permissions and\n> - * limitations under the License.\n> - */\n> -\n> -#include <config.h>\n> -\n> -#include <stdbool.h>\n> -\n> -#include \"openvswitch/compiler.h\"\n> -#include \"route-table-notify.h\"\n> -\n> -bool\n> -route_table_notify_run(void)\n> -{\n> -    return false;\n> -}\n> -\n> -void\n> -route_table_notify_wait(void)\n> -{\n> -}\n> -\n> -void\n> -route_table_add_watch_request(struct hmap *route_table_watches OVS_UNUSED,\n> -                              uint32_t table_id OVS_UNUSED)\n> -{\n> -}\n> -\n> -void\n> -route_table_watch_request_cleanup(struct hmap *route_table_watches OVS_UNUSED)\n> -{\n> -}\n> -\n> -void\n> -route_table_notify_update_watches(\n> -    const struct hmap *route_table_watches OVS_UNUSED)\n> -{\n> -}\n> -\n> -void\n> -route_table_notify_destroy(void)\n> -{\n> -}\n> diff --git a/controller/route-table-notify.c b/controller/route-table-notify.c\n> deleted file mode 100644\n> index 9fa2e0ea6..000000000\n> --- a/controller/route-table-notify.c\n> +++ /dev/null\n> @@ -1,238 +0,0 @@\n> -/*\n> - * Copyright (c) 2025, STACKIT GmbH & Co. KG\n> - *\n> - * Licensed under the Apache License, Version 2.0 (the \"License\");\n> - * you may not use this file except in compliance with the License.\n> - * You may obtain a copy of the License at:\n> - *\n> - *     http://www.apache.org/licenses/LICENSE-2.0\n> - *\n> - * Unless required by applicable law or agreed to in writing, software\n> - * distributed under the License is distributed on an \"AS IS\" BASIS,\n> - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n> - * See the License for the specific language governing permissions and\n> - * limitations under the License.\n> - */\n> -\n> -#include <config.h>\n> -\n> -#include <net/if.h>\n> -#include <linux/rtnetlink.h>\n> -\n> -#include \"netlink-notifier.h\"\n> -#include \"openvswitch/vlog.h\"\n> -\n> -#include \"binding.h\"\n> -#include \"hash.h\"\n> -#include \"hmapx.h\"\n> -#include \"route-table.h\"\n> -#include \"route.h\"\n> -#include \"route-table-notify.h\"\n> -#include \"route-exchange-netlink.h\"\n> -\n> -VLOG_DEFINE_THIS_MODULE(route_table_notify);\n> -\n> -struct route_table_watch_request {\n> -    struct hmap_node node;\n> -    uint32_t table_id;\n> -};\n> -\n> -struct route_table_watch_entry {\n> -    struct hmap_node node;\n> -    uint32_t table_id;\n> -};\n> -\n> -static struct hmap watches = HMAP_INITIALIZER(&watches);\n> -static bool any_route_table_changed;\n> -static struct route_table_msg nln_rtmsg_change;\n> -\n> -static struct nln *nl_route_handle;\n> -static struct nln_notifier *nl_route_notifier_v4;\n> -static struct nln_notifier *nl_route_notifier_v6;\n> -\n> -static void route_table_change(const void *change_, void *aux);\n> -\n> -static void\n> -route_table_register_notifiers(void)\n> -{\n> -    VLOG_INFO(\"Adding route table watchers.\");\n> -    ovs_assert(!nl_route_handle);\n> -\n> -    nl_route_handle = nln_create(NETLINK_ROUTE, route_table_parse,\n> -                                 &nln_rtmsg_change);\n> -    ovs_assert(nl_route_handle);\n> -\n> -    nl_route_notifier_v4 =\n> -        nln_notifier_create(nl_route_handle, RTNLGRP_IPV4_ROUTE,\n> -                            route_table_change, NULL);\n> -    if (!nl_route_notifier_v4) {\n> -        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);\n> -        VLOG_WARN_RL(&rl, \"Failed to create ipv4 route table watcher.\");\n> -    }\n> -\n> -    nl_route_notifier_v6 =\n> -        nln_notifier_create(nl_route_handle, RTNLGRP_IPV6_ROUTE,\n> -                            route_table_change, NULL);\n> -    if (!nl_route_notifier_v6) {\n> -        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);\n> -        VLOG_WARN_RL(&rl, \"Failed to create ipv6 route table watcher.\");\n> -    }\n> -}\n> -\n> -static void\n> -route_table_deregister_notifiers(void)\n> -{\n> -    VLOG_INFO(\"Removing route table watchers.\");\n> -    ovs_assert(nl_route_handle);\n> -\n> -    nln_notifier_destroy(nl_route_notifier_v4);\n> -    nln_notifier_destroy(nl_route_notifier_v6);\n> -    nln_destroy(nl_route_handle);\n> -    nl_route_notifier_v4 = NULL;\n> -    nl_route_notifier_v6 = NULL;\n> -    nl_route_handle = NULL;\n> -}\n> -\n> -static uint32_t\n> -route_table_notify_hash_watch(uint32_t table_id)\n> -{\n> -    return hash_int(table_id, 0);\n> -}\n> -\n> -void\n> -route_table_add_watch_request(struct hmap *route_table_watches,\n> -                              uint32_t table_id)\n> -{\n> -    struct route_table_watch_request *wr = xzalloc(sizeof *wr);\n> -    wr->table_id = table_id;\n> -    hmap_insert(route_table_watches, &wr->node,\n> -                route_table_notify_hash_watch(wr->table_id));\n> -}\n> -\n> -void\n> -route_table_watch_request_cleanup(struct hmap *route_table_watches)\n> -{\n> -    struct route_table_watch_request *wr;\n> -    HMAP_FOR_EACH_POP (wr, node, route_table_watches) {\n> -        free(wr);\n> -    }\n> -}\n> -\n> -static struct route_table_watch_entry *\n> -find_watch_entry(uint32_t table_id)\n> -{\n> -    struct route_table_watch_entry *we;\n> -    uint32_t hash = route_table_notify_hash_watch(table_id);\n> -    HMAP_FOR_EACH_WITH_HASH (we, node, hash, &watches) {\n> -        if (table_id == we->table_id) {\n> -            return we;\n> -        }\n> -    }\n> -    return NULL;\n> -}\n> -\n> -static void\n> -route_table_change(const void *change_, void *aux OVS_UNUSED)\n> -{\n> -    if (!change_) {\n> -        return;\n> -    }\n> -\n> -    /* We currently track whether at least one recent route table change\n> -     * was detected.  If that's the case already there's no need to\n> -     * continue. */\n> -    struct route_table_msg *change =\n> -        CONST_CAST(struct route_table_msg *, change_);\n> -    if (!any_route_table_changed && change->rd.rtm_protocol != RTPROT_OVN) {\n> -        if (find_watch_entry(change->rd.rta_table_id)) {\n> -            any_route_table_changed = true;\n> -        }\n> -    }\n> -\n> -    route_data_destroy(&change->rd);\n> -}\n> -\n> -static void\n> -add_watch_entry(uint32_t table_id)\n> -{\n> -   VLOG_INFO(\"Registering new route table watcher for table %d.\",\n> -             table_id);\n> -\n> -    struct route_table_watch_entry *we;\n> -    uint32_t hash = route_table_notify_hash_watch(table_id);\n> -    we = xzalloc(sizeof *we);\n> -    we->table_id = table_id;\n> -    hmap_insert(&watches, &we->node, hash);\n> -\n> -    if (!nl_route_handle) {\n> -        route_table_register_notifiers();\n> -    }\n> -}\n> -\n> -static void\n> -remove_watch_entry(struct route_table_watch_entry *we)\n> -{\n> -    VLOG_INFO(\"Removing route table watcher for table %d.\", we->table_id);\n> -    hmap_remove(&watches, &we->node);\n> -    free(we);\n> -\n> -    if (hmap_is_empty(&watches)) {\n> -        route_table_deregister_notifiers();\n> -    }\n> -}\n> -\n> -bool\n> -route_table_notify_run(void)\n> -{\n> -    any_route_table_changed = false;\n> -\n> -    if (nl_route_handle) {\n> -        nln_run(nl_route_handle);\n> -    }\n> -\n> -    return any_route_table_changed;\n> -}\n> -\n> -void\n> -route_table_notify_wait(void)\n> -{\n> -    if (nl_route_handle) {\n> -        nln_wait(nl_route_handle);\n> -    }\n> -}\n> -\n> -void\n> -route_table_notify_update_watches(const struct hmap *route_table_watches)\n> -{\n> -    struct hmapx sync_watches = HMAPX_INITIALIZER(&sync_watches);\n> -    struct route_table_watch_entry *we;\n> -    HMAP_FOR_EACH (we, node, &watches) {\n> -        hmapx_add(&sync_watches, we);\n> -    }\n> -\n> -    struct route_table_watch_request *wr;\n> -    HMAP_FOR_EACH (wr, node, route_table_watches) {\n> -        we = find_watch_entry(wr->table_id);\n> -        if (we) {\n> -            hmapx_find_and_delete(&sync_watches, we);\n> -        } else {\n> -            add_watch_entry(wr->table_id);\n> -        }\n> -    }\n> -\n> -    struct hmapx_node *node;\n> -    HMAPX_FOR_EACH (node, &sync_watches) {\n> -        remove_watch_entry(node->data);\n> -    }\n> -\n> -    hmapx_destroy(&sync_watches);\n> -}\n> -\n> -void\n> -route_table_notify_destroy(void)\n> -{\n> -    struct route_table_watch_entry *we;\n> -    HMAP_FOR_EACH_SAFE (we, node, &watches) {\n> -        remove_watch_entry(we);\n> -    }\n> -}\n> diff --git a/controller/route-table-notify.h b/controller/route-table-notify.h\n> deleted file mode 100644\n> index a2bc05a49..000000000\n> --- a/controller/route-table-notify.h\n> +++ /dev/null\n> @@ -1,44 +0,0 @@\n> -/*\n> - * Copyright (c) 2025, STACKIT GmbH & Co. KG\n> - *\n> - * Licensed under the Apache License, Version 2.0 (the \"License\");\n> - * you may not use this file except in compliance with the License.\n> - * You may obtain a copy of the License at:\n> - *\n> - *     http://www.apache.org/licenses/LICENSE-2.0\n> - *\n> - * Unless required by applicable law or agreed to in writing, software\n> - * distributed under the License is distributed on an \"AS IS\" BASIS,\n> - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n> - * See the License for the specific language governing permissions and\n> - * limitations under the License.\n> - */\n> -\n> -#ifndef ROUTE_TABLE_NOTIFY_H\n> -#define ROUTE_TABLE_NOTIFY_H 1\n> -\n> -#include <stdbool.h>\n> -#include \"openvswitch/hmap.h\"\n> -\n> -/* Returns true if any route table has changed enough that we need\n> - * to learn new routes. */\n> -bool route_table_notify_run(void);\n> -void route_table_notify_wait(void);\n> -\n> -/* Add a watch request to the hmap. The hmap should later be passed to\n> - * route_table_notify_update_watches*/\n> -void route_table_add_watch_request(struct hmap *route_table_watches,\n> -                                   uint32_t table_id);\n> -\n> -/* Cleanup all watch request in the provided hmap that where added using\n> - * route_table_add_watch_request. */\n> -void route_table_watch_request_cleanup(struct hmap *route_table_watches);\n> -\n> -/* Updates the list of route table watches that are currently active.\n> - * hmap should contain struct route_table_watch_request */\n> -void route_table_notify_update_watches(const struct hmap *route_table_watches);\n> -\n> -/* Cleans up all route table watches. */\n> -void route_table_notify_destroy(void);\n> -\n> -#endif /* ROUTE_TABLE_NOTIFY_H */\n> diff --git a/tests/automake.mk b/tests/automake.mk\n> index 2dfc0bfa7..75a4b00d7 100644\n> --- a/tests/automake.mk\n> +++ b/tests/automake.mk\n> @@ -303,10 +303,10 @@ tests_ovstest_SOURCES += \\\n>  \tcontroller/host-if-monitor.h \\\n>  \tcontroller/neighbor-exchange-netlink.c \\\n>  \tcontroller/neighbor-exchange-netlink.h \\\n> -\tcontroller/neighbor-table-notify.c \\\n> -\tcontroller/neighbor-table-notify.h \\\n>  \tcontroller/neighbor.c \\\n>  \tcontroller/neighbor.h \\\n> +\tcontroller/ovn-netlink-notifier.c \\\n> +\tcontroller/ovn-netlink-notifier.h \\\n>  \tcontroller/route-exchange-netlink.c \\\n>  \tcontroller/route-exchange-netlink.h \\\n>  \ttests/test-ovn-netlink.c\n> diff --git a/tests/system-ovn-netlink.at b/tests/system-ovn-netlink.at\n> index 4e581aa74..8bf1055d1 100644\n> --- a/tests/system-ovn-netlink.at\n> +++ b/tests/system-ovn-netlink.at\n> @@ -229,6 +229,7 @@ on_exit 'ip link del br-test'\n>  check ip link set br-test address 00:00:00:00:00:01\n>  check ip address add dev br-test 10.10.10.1/24\n>  check ip link set dev br-test up\n> +br_if_index=$(netlink_if_index br-test)\n>  \n>  check ip link add lo-test type dummy\n>  on_exit 'ip link del lo-test'\n> @@ -237,43 +238,47 @@ check ip link set lo-test address 00:00:00:00:00:02\n>  check ip link set dev lo-test up\n>  lo_if_index=$(netlink_if_index lo-test)\n>  \n> -check ip link add br-test-unused type bridge\n> -on_exit 'ip link del br-test-unused'\n> -check ip link set br-test-unused address 00:00:00:00:00:03\n> -check ip address add dev br-test-unused 20.20.20.1/24\n> -check ip link set dev br-test-unused up\n> -\n> -check ip link add lo-test-unused type dummy\n> -on_exit 'ip link del lo-test-unused'\n> -check ip link set lo-test-unused master br-test-unused\n> -check ip link set lo-test-unused address 00:00:00:00:00:04\n> -check ip link set dev lo-test-unused up\n> -\n>  dnl Should notify if an entry is added to a bridge port monitored by OVN.\n> -check ovstest test-ovn-netlink neighbor-table-notify lo-test $lo_if_index \\\n> -    'bridge fdb add 00:00:00:00:00:05 dev lo-test' \\\n> -    true\n> +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> +    'bridge fdb add 00:00:00:00:00:05 dev lo-test'], [0], [dnl\n> +Add neighbor ifindex=$lo_if_index vlan=0 eth=00:00:00:00:00:05 dst=:: port=0\n> +])\n> +\n> +dnl Should notify if an entry is removed from a bridge port monitored by OVN.\n> +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> +    'bridge fdb del 00:00:00:00:00:05 dev lo-test'], [0], [dnl\n> +Delete neighbor ifindex=$lo_if_index vlan=0 eth=00:00:00:00:00:05 dst=:: port=0\n> +])\n>  \n> -dnl Should NOT notify if an entry is added to a bridge port that's not\n> +dnl Should NOT notify if an static entry is added to a bridge port\n>  dnl monitored by OVN.\n> -check ovstest test-ovn-netlink neighbor-table-notify lo-test $lo_if_index \\\n> -    'bridge fdb add 00:00:00:00:00:05 dev lo-test-unused' \\\n> -    false\n> +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> +    'bridge fdb add 00:00:00:00:00:06 dev lo-test master static'], [0], [dnl\n> +])\n>  \n> -br_if_index=$(netlink_if_index br-test)\n>  dnl Should notify if an entry is added to a bridge that's monitored by\n>  dnl OVN.\n> -check ovstest test-ovn-netlink neighbor-table-notify br-test $br_if_index \\\n> +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n>      'ip neigh add 10.10.10.10 lladdr 00:00:00:00:10:00 \\\n> -        dev br-test extern_learn' \\\n> -    true\n> +        dev br-test extern_learn'], [0], [dnl\n> +Add neighbor ifindex=$br_if_index vlan=0 eth=00:00:00:00:10:00 dst=10.10.10.10 port=0\n> +])\n>  \n> -dnl Should NOT notify if an entry is added to a bridge that's not monitored by\n> +dnl Should notify if an entry is removed from a bridge that's monitored by\n>  dnl OVN.\n> -check ovstest test-ovn-netlink neighbor-table-notify br-test $br_if_index \\\n> +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> +    'ip neigh del 10.10.10.10 lladdr 00:00:00:00:10:00 \\\n> +        dev br-test' | sort], [0], [dnl\n> +Add neighbor ifindex=$br_if_index vlan=0 eth=00:00:00:00:00:00 dst=10.10.10.10 port=0\n> +Delete neighbor ifindex=$br_if_index vlan=0 eth=00:00:00:00:00:00 dst=10.10.10.10 port=0\n> +])\n> +\n> +dnl Should NOT notify if an noarp entry is added to a bridge port\n> +dnl monitored by OVN.\n> +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n>      'ip neigh add 20.20.20.20 lladdr 00:00:00:00:20:00 \\\n> -        dev br-test-unused extern_learn' \\\n> -    false\n> +        dev br-test nud noarp'], [0], [dnl\n> +])\n>  AT_CLEANUP\n>  \n>  AT_SETUP([netlink - host-if-monitor])\n> diff --git a/tests/test-ovn-netlink.c b/tests/test-ovn-netlink.c\n> index 6e9b46d04..efc3c9e5e 100644\n> --- a/tests/test-ovn-netlink.c\n> +++ b/tests/test-ovn-netlink.c\n> @@ -23,7 +23,7 @@\n>  \n>  #include \"controller/host-if-monitor.h\"\n>  #include \"controller/neighbor-exchange-netlink.h\"\n> -#include \"controller/neighbor-table-notify.h\"\n> +#include \"controller/ovn-netlink-notifier.h\"\n>  #include \"controller/neighbor.h\"\n>  #include \"controller/route.h\"\n>  #include \"controller/route-exchange-netlink.h\"\n> @@ -109,41 +109,48 @@ done:\n>  }\n>  \n>  static void\n> -test_neighbor_table_notify(struct ovs_cmdl_context *ctx)\n> +run_command_under_notifier(const char *cmd)\n>  {\n> -    unsigned int shift = 1;\n> +    ovn_netlink_notifiers_run();\n> +    ovn_netlink_notifiers_wait();\n>  \n> -    const char *if_name = test_read_value(ctx, shift++, \"if_name\");\n> -    if (!if_name) {\n> -        return;\n> +    int rc = system(cmd);\n> +    if (rc) {\n> +        exit(rc);\n>      }\n>  \n> -    unsigned int if_index;\n> -    if (!test_read_uint_value(ctx, shift++, \"if_index\", &if_index)) {\n> -        return;\n> -    }\n> +    ovn_netlink_notifiers_run();\n> +}\n> +\n> +static void\n> +test_neighbor_table_notify(struct ovs_cmdl_context *ctx)\n> +{\n> +    unsigned int shift = 1;\n>  \n>      const char *cmd = test_read_value(ctx, shift++, \"shell_command\");\n>      if (!cmd) {\n>          return;\n>      }\n>  \n> -    const char *notify = test_read_value(ctx, shift++, \"should_notify\");\n> -    bool expect_notify = notify && !strcmp(notify, \"true\");\n> -\n> -    struct hmap table_watches = HMAP_INITIALIZER(&table_watches);\n> -    neighbor_table_add_watch_request(&table_watches, if_index, if_name);\n> -    neighbor_table_notify_update_watches(&table_watches);\n> +    ovn_netlink_update_notifier(OVN_NL_NOTIFIER_NEIGHBOR, true);\n> +    run_command_under_notifier(cmd);\n>  \n> -    neighbor_table_notify_run();\n> -    neighbor_table_notify_wait();\n> +    struct vector *msgs = ovn_netlink_get_msgs(OVN_NL_NOTIFIER_NEIGHBOR);\n>  \n> -    int rc = system(cmd);\n> -    if (rc) {\n> -        exit(rc);\n> +    struct ne_table_msg *msg;\n> +    VECTOR_FOR_EACH_PTR (msgs, msg) {\n> +        char addr_s[INET6_ADDRSTRLEN + 1];\n> +        printf(\"%s neighbor ifindex=%\"PRId32\" vlan=%\"PRIu16\" \"\n> +               \"eth=\" ETH_ADDR_FMT \" dst=%s port=%\"PRIu16\"\\n\",\n> +               msg->nlmsg_type == RTM_NEWNEIGH ? \"Add\" : \"Delete\",\n> +               msg->nd.if_index, msg->nd.vlan, ETH_ADDR_ARGS(msg->nd.lladdr),\n> +               ipv6_string_mapped(addr_s, &msg->nd.addr)\n> +                   ? addr_s\n> +                   : \"(invalid)\",\n> +               msg->nd.port);\n>      }\n> -    ovs_assert(neighbor_table_notify_run() == expect_notify);\n> -    neighbor_table_watch_request_cleanup(&table_watches);\n> +\n> +    ovn_netlink_notifiers_destroy();\n>  }\n>  \n>  static void\n> @@ -249,7 +256,7 @@ test_ovn_netlink(int argc, char *argv[])\n>      set_program_name(argv[0]);\n>      static const struct ovs_cmdl_command commands[] = {\n>          {\"neighbor-sync\", NULL, 2, INT_MAX, test_neighbor_sync, OVS_RO},\n> -        {\"neighbor-table-notify\", NULL, 3, 4,\n> +        {\"neighbor-table-notify\", NULL, 1, 1,\n>           test_neighbor_table_notify, OVS_RO},\n>          {\"host-if-monitor\", NULL, 2, 3, test_host_if_monitor, OVS_RO},\n>          {\"route-sync\", NULL, 1, INT_MAX, test_route_sync, OVS_RO},\n> -- \n> 2.53.0\n> \n> _______________________________________________\n> dev mailing list\n> dev@openvswitch.org\n> https://mail.openvswitch.org/mailman/listinfo/ovs-dev\n>","headers":{"Return-Path":"<ovs-dev-bounces@openvswitch.org>","X-Original-To":["incoming@patchwork.ozlabs.org","dev@openvswitch.org"],"Delivered-To":["patchwork-incoming@legolas.ozlabs.org","ovs-dev@lists.linuxfoundation.org"],"Authentication-Results":["legolas.ozlabs.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=Bw85mxVq;\n\tdkim=fail reason=\"signature verification failed\" (2048-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=google header.b=qRYuQ6a5;\n\tdkim-atps=neutral","legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org\n (client-ip=140.211.166.136; helo=smtp3.osuosl.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org)","smtp3.osuosl.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key,\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=Bw85mxVq;\n\tdkim=fail reason=\"signature verification failed\" (2048-bit key,\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=google header.b=qRYuQ6a5","smtp4.osuosl.org; dmarc=pass (p=quarantine dis=none)\n header.from=redhat.com","smtp4.osuosl.org;\n dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com\n header.a=rsa-sha256 header.s=mimecast20190719 header.b=Bw85mxVq;\n dkim=pass (2048-bit key,\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=google header.b=qRYuQ6a5"],"Received":["from smtp3.osuosl.org (smtp3.osuosl.org [140.211.166.136])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fsjNx5tj3z1yGS\n\tfor <incoming@patchwork.ozlabs.org>; Sat, 11 Apr 2026 02:46:09 +1000 (AEST)","from localhost (localhost [127.0.0.1])\n\tby smtp3.osuosl.org (Postfix) with ESMTP id D31DF610BA;\n\tFri, 10 Apr 2026 16:46:06 +0000 (UTC)","from smtp3.osuosl.org ([127.0.0.1])\n by localhost (smtp3.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id OpFYS-nFmTU1; Fri, 10 Apr 2026 16:46:04 +0000 (UTC)","from lists.linuxfoundation.org (lf-lists.osuosl.org\n [IPv6:2605:bc80:3010:104::8cd3:938])\n\tby smtp3.osuosl.org (Postfix) with ESMTPS id 2FAC7610B8;\n\tFri, 10 Apr 2026 16:46:04 +0000 (UTC)","from lf-lists.osuosl.org (localhost [127.0.0.1])\n\tby lists.linuxfoundation.org (Postfix) with ESMTP id 09516C054A;\n\tFri, 10 Apr 2026 16:46:04 +0000 (UTC)","from smtp4.osuosl.org (smtp4.osuosl.org [140.211.166.137])\n by lists.linuxfoundation.org (Postfix) with ESMTP id C22F3C0549\n for <dev@openvswitch.org>; Fri, 10 Apr 2026 16:46:02 +0000 (UTC)","from localhost (localhost [127.0.0.1])\n by smtp4.osuosl.org (Postfix) with ESMTP id A4226411A1\n for <dev@openvswitch.org>; Fri, 10 Apr 2026 16:46:02 +0000 (UTC)","from smtp4.osuosl.org ([127.0.0.1])\n by localhost (smtp4.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id Pnzkimvd5va3 for <dev@openvswitch.org>;\n Fri, 10 Apr 2026 16:46:00 +0000 (UTC)","from us-smtp-delivery-124.mimecast.com\n (us-smtp-delivery-124.mimecast.com [170.10.129.124])\n by smtp4.osuosl.org (Postfix) with ESMTPS id 35ED6411A0\n for <dev@openvswitch.org>; Fri, 10 Apr 2026 16:45:59 +0000 (UTC)","from mail-wr1-f70.google.com (mail-wr1-f70.google.com\n [209.85.221.70]) by relay.mimecast.com with ESMTP with STARTTLS\n (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id\n us-mta-111-GvbIqOMYPq6xewa8Rh2Lrg-1; Fri, 10 Apr 2026 12:45:55 -0400","by mail-wr1-f70.google.com with SMTP id\n ffacd0b85a97d-43d103e46c3so1447558f8f.3\n for <dev@openvswitch.org>; Fri, 10 Apr 2026 09:45:55 -0700 (PDT)","from localhost (net-37-119-153-93.cust.vodafonedsl.it.\n [37.119.153.93]) by smtp.gmail.com with ESMTPSA id\n ffacd0b85a97d-43d63e50044sm8719267f8f.25.2026.04.10.09.45.51\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Fri, 10 Apr 2026 09:45:52 -0700 (PDT)"],"X-Virus-Scanned":["amavis at osuosl.org","amavis at osuosl.org"],"X-Comment":"SPF check N/A for local connections -\n client-ip=2605:bc80:3010:104::8cd3:938; helo=lists.linuxfoundation.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=<UNKNOWN> ","DKIM-Filter":["OpenDKIM Filter v2.11.0 smtp3.osuosl.org 2FAC7610B8","OpenDKIM Filter v2.11.0 smtp4.osuosl.org 35ED6411A0"],"Received-SPF":"Pass (mailfrom) identity=mailfrom; client-ip=170.10.129.124;\n helo=us-smtp-delivery-124.mimecast.com;\n envelope-from=lorenzo.bianconi@redhat.com; receiver=<UNKNOWN>","DMARC-Filter":"OpenDMARC Filter v1.4.2 smtp4.osuosl.org 35ED6411A0","DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1775839558;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:cc:mime-version:mime-version:content-type:content-type:\n in-reply-to:in-reply-to:references:references;\n bh=FHiC/zLPL4h9wGXG3G+V3jQlds5K2GEjMy2ncjz/GRo=;\n b=Bw85mxVqXmTccoczleYBEium1be/lHDkzqshYDTtM6k/1NDPvykcTCL/95NYDOMTJ5no+X\n x79wpJ/PMOcChrI/qVmuviSmMMnodeNXUTWQeJm0wHEZ01f966+qFC4wgmDxgOA0GzEQVZ\n So59vxS5K9t2vwMud35mDjnK+Hnxirg=","v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=redhat.com; s=google; t=1775839555; x=1776444355; darn=openvswitch.org;\n h=in-reply-to:content-disposition:mime-version:references:message-id\n :subject:cc:to:from:date:from:to:cc:subject:date:message-id:reply-to;\n bh=FHiC/zLPL4h9wGXG3G+V3jQlds5K2GEjMy2ncjz/GRo=;\n b=qRYuQ6a5G8nNU2qM4bKx1ieoAl0sgD7FXO2XCZY9yxKJ1jpBJOhdM/718aEBUJkTVz\n ws4ewSvRQyCdbCk6+r5D/wpeAaMlewq5nvzt0p1HSm8txntcx79Q9Pwscr4DuPLawfdU\n vGN7iLEqdTNgMwuV0/gCOdBZqEsTmy5YJRuEDFhGPzctokLjUN5GLDvMfIJK8qaq6buI\n FIkSmXO+Vll8JSWNvtp0kOO4q/ETK5BJwlGn7ECdxPuFQJ0ktZ9CvKfJe/yVzPWwJJvp\n boC4AoCK74OWekgAC9gj7Ly0VektZJf/j51NDhpvlhqSCoXHNyRsdi97KeycZqUo9Pa+\n Pkaw=="],"X-MC-Unique":"GvbIqOMYPq6xewa8Rh2Lrg-1","X-Mimecast-MFC-AGG-ID":"GvbIqOMYPq6xewa8Rh2Lrg_1775839555","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1775839555; x=1776444355;\n h=in-reply-to:content-disposition:mime-version:references:message-id\n :subject:cc:to:from:date:x-gm-gg:x-gm-message-state:from:to:cc\n :subject:date:message-id:reply-to;\n bh=FHiC/zLPL4h9wGXG3G+V3jQlds5K2GEjMy2ncjz/GRo=;\n b=K0cWt0Mr03Wp0x5qlr/yiEzT/LUPwlywKx6s9DmhAxS8m2F4yI8dtJiJCtCaj5QOBb\n PWkLCZ78sRo6r4rkc/BRrjmVyWOgyeQuwWD7/IsYHhWxsPzWGlu0UgALh+/eFHYakcCo\n UJ5bu64tr2ZB9ebsKYsGIWHnVV3e6OZk0P9a7VLf9SRhiZjPkAtf6GRvNGlFhsVRukqT\n F5hnBStIOrZkMIfQf3EX6fjx5n8qg3nL75gRMcgtNLFJgw9YC7nQTTsfob41UWgqq8Aa\n bPesgKueOkhQGSa6AdBQSIHLNRwJD57Gaq8qYvwcKrONR55gmAowSPz1/VYG2F4FCZZy\n KTyg==","X-Gm-Message-State":"AOJu0YzGrR05m8Delr30WhzWkTpNocJR9Z4+Xy5+WQOWKwFYHGuVVcT0\n 1+mKWvy0qnVMKcc+aPHCpH9wOJoQM8J8GMTDZqif9UndhBgaMgpaeNE/fBVLD6SZZObYUtTHF2k\n lj32cMtss2knaSfAwJK1FosrzcWX9Paw/y3qMBn+is+i3jpBhg0ilWg==","X-Gm-Gg":"AeBDietLCD9W61gFuaQA7rAn7vekt3SBL4U9357mWT2uoFdDdqVsv8GUd9RE8B2GOoC\n GvQjdAdF7aqlxBX19+FcepwK9y1L95yVatjQGKBvm2FYoRQvs1WzYnzOq/vaNmyilj1GYuUcXKQ\n aBFLa0utvCPyFo4sdjt4Bvkn+r2xCjyVzVjXHfSuh+TC1bX0OZnHMuo+D0DUqq5322R6qcsxJh3\n c8dFE5qZ3nO55JqyoOvVMfnyi4U0FldFSo24O1qJQf2wsbcDAfSDG2O4VZpFVJhS5OIynFNCmIx\n gx8W9rlFKoZUzQyuxe0AHy3lvjls8QjoAY50XsV3UaU4OjEzc7vYbwjDbWVHvHYr2PXqpPDlqLo\n v1yh9ywdq1YvcCjkN7ddnrj7U+fV4NKKgG3QseqkqCman+TyzyWWRny7IHpjqsim/6gr6pQ==","X-Received":["by 2002:a05:6000:3107:b0:43b:3e34:7fe with SMTP id\n ffacd0b85a97d-43d64289402mr6011873f8f.21.1775839554069;\n Fri, 10 Apr 2026 09:45:54 -0700 (PDT)","by 2002:a05:6000:3107:b0:43b:3e34:7fe with SMTP id\n ffacd0b85a97d-43d64289402mr6011802f8f.21.1775839553085;\n Fri, 10 Apr 2026 09:45:53 -0700 (PDT)"],"Date":"Fri, 10 Apr 2026 18:45:51 +0200","To":"Ales Musil <amusil@redhat.com>","Cc":"dev@openvswitch.org, dceara@redhat.com","Message-ID":"<adkpP1n5DQX4QAis@lore-desk>","References":"<20260402082510.1417440-1-amusil@redhat.com>\n <20260402082510.1417440-5-amusil@redhat.com>","MIME-Version":"1.0","In-Reply-To":"<20260402082510.1417440-5-amusil@redhat.com>","X-Content-Filtered-By":"Mailman/MimeDel 2.1.30","Subject":"Re: [ovs-dev] [PATCH ovn 4/6] controller: Consolidate the netlink\n notifiers.","X-BeenThere":"ovs-dev@openvswitch.org","X-Mailman-Version":"2.1.30","Precedence":"list","List-Id":"<ovs-dev.openvswitch.org>","List-Unsubscribe":"<https://mail.openvswitch.org/mailman/options/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=unsubscribe>","List-Archive":"<http://mail.openvswitch.org/pipermail/ovs-dev/>","List-Post":"<mailto:ovs-dev@openvswitch.org>","List-Help":"<mailto:ovs-dev-request@openvswitch.org?subject=help>","List-Subscribe":"<https://mail.openvswitch.org/mailman/listinfo/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=subscribe>","From":"Lorenzo Bianconi via dev <ovs-dev@openvswitch.org>","Reply-To":"Lorenzo Bianconi <lorenzo.bianconi@redhat.com>","Content-Type":"multipart/mixed; boundary=\"===============5036926245467143398==\"","Errors-To":"ovs-dev-bounces@openvswitch.org","Sender":"\"dev\" <ovs-dev-bounces@openvswitch.org>"}},{"id":3676728,"web_url":"http://patchwork.ozlabs.org/comment/3676728/","msgid":"<CALVEqe5htbf2a+t35BGCgJVEC7e+gShrBkffz8K2C+EsyeQD8A@mail.gmail.com>","list_archive_url":null,"date":"2026-04-13T13:00:29","subject":"Re: [ovs-dev] [PATCH ovn 4/6] controller: Consolidate the netlink\n notifiers.","submitter":{"id":83634,"url":"http://patchwork.ozlabs.org/api/people/83634/","name":"Ales Musil","email":"amusil@redhat.com"},"content":"On Fri, Apr 10, 2026 at 6:46 PM Lorenzo Bianconi <\nlorenzo.bianconi@redhat.com> wrote:\n\n> > The netlink notifiers used a lot of code that was more or less\n> > identical to each other. Extract the common code into separate module\n> > which allows the definition of listeners and their specific data.\n> > This should make it easier to add any new notifier, which will be the\n> > case in the future. It should also make it slightly easier to track\n> > individual updates and changes that could be processed incrementally\n> > instead of full recompute when there is any change.\n> >\n> > Signed-off-by: Ales Musil <amusil@redhat.com>\n>\n> Acked-by: Lorenzo Bianconi <lorenzo.bianconi@redhat.com>\n>\n> > ---\n> >  controller/automake.mk                        |  13 +-\n> >  controller/neighbor-exchange.c                |   4 +-\n> >  controller/neighbor-exchange.h                |   4 +-\n> >  controller/neighbor-table-notify.c            | 244 -----------------\n> >  controller/neighbor-table-notify.h            |  45 ----\n> >  controller/ovn-controller.c                   | 169 ++++++++----\n> >  ...ify-stub.c => ovn-netlink-notifier-stub.c} |  35 ++-\n> >  controller/ovn-netlink-notifier.c             | 251 ++++++++++++++++++\n> >  controller/ovn-netlink-notifier.h             |  38 +++\n> >  controller/route-exchange-netlink.h           |   1 +\n> >  controller/route-exchange.c                   |   4 +-\n> >  controller/route-exchange.h                   |   2 +-\n> >  controller/route-table-notify-stub.c          |  55 ----\n> >  controller/route-table-notify.c               | 238 -----------------\n> >  controller/route-table-notify.h               |  44 ---\n> >  tests/automake.mk                             |   4 +-\n> >  tests/system-ovn-netlink.at                   |  59 ++--\n> >  tests/test-ovn-netlink.c                      |  55 ++--\n> >  18 files changed, 502 insertions(+), 763 deletions(-)\n> >  delete mode 100644 controller/neighbor-table-notify.c\n> >  delete mode 100644 controller/neighbor-table-notify.h\n> >  rename controller/{neighbor-table-notify-stub.c =>\n> ovn-netlink-notifier-stub.c} (51%)\n> >  create mode 100644 controller/ovn-netlink-notifier.c\n> >  create mode 100644 controller/ovn-netlink-notifier.h\n> >  delete mode 100644 controller/route-table-notify-stub.c\n> >  delete mode 100644 controller/route-table-notify.c\n> >  delete mode 100644 controller/route-table-notify.h\n> >\n> > diff --git a/controller/automake.mk b/controller/automake.mk\n> > index d6809df10..c37b89b6c 100644\n> > --- a/controller/automake.mk\n> > +++ b/controller/automake.mk\n> > @@ -32,6 +32,7 @@ controller_ovn_controller_SOURCES = \\\n> >       controller/lport.h \\\n> >       controller/ofctrl.c \\\n> >       controller/ofctrl.h \\\n> > +     controller/ovn-netlink-notifier.h \\\n> >       controller/neighbor.c \\\n> >       controller/neighbor.h \\\n> >       controller/neighbor-of.c \\\n> > @@ -63,33 +64,29 @@ controller_ovn_controller_SOURCES = \\\n> >       controller/ecmp-next-hop-monitor.h \\\n> >       controller/ecmp-next-hop-monitor.c \\\n> >       controller/route-exchange.h \\\n> > -     controller/route-table-notify.h \\\n> >       controller/route.h \\\n> >       controller/route.c \\\n> >       controller/garp_rarp.h \\\n> >       controller/garp_rarp.c \\\n> >       controller/neighbor-exchange.h \\\n> > -     controller/neighbor-table-notify.h \\\n> >       controller/host-if-monitor.h\n> >\n> >  if HAVE_NETLINK\n> >  controller_ovn_controller_SOURCES += \\\n> >       controller/host-if-monitor.c \\\n> > +     controller/ovn-netlink-notifier.c \\\n> >       controller/neighbor-exchange-netlink.h \\\n> >       controller/neighbor-exchange-netlink.c \\\n> >       controller/neighbor-exchange.c \\\n> > -     controller/neighbor-table-notify.c \\\n> >       controller/route-exchange-netlink.h \\\n> >       controller/route-exchange-netlink.c \\\n> > -     controller/route-exchange.c \\\n> > -     controller/route-table-notify.c\n> > +     controller/route-exchange.c\n> >  else\n> >  controller_ovn_controller_SOURCES += \\\n> >       controller/host-if-monitor-stub.c \\\n> > +     controller/ovn-netlink-notifier-stub.c \\\n> >       controller/neighbor-exchange-stub.c \\\n> > -     controller/neighbor-table-notify-stub.c \\\n> > -     controller/route-exchange-stub.c \\\n> > -     controller/route-table-notify-stub.c\n> > +     controller/route-exchange-stub.c\n> >  endif\n> >\n> >  controller_ovn_controller_LDADD = lib/libovn.la $(OVS_LIBDIR)/\n> libopenvswitch.la\n> > diff --git a/controller/neighbor-exchange.c\n> b/controller/neighbor-exchange.c\n> > index e40f39e24..47e757712 100644\n> > --- a/controller/neighbor-exchange.c\n> > +++ b/controller/neighbor-exchange.c\n> > @@ -21,7 +21,6 @@\n> >  #include \"neighbor.h\"\n> >  #include \"neighbor-exchange.h\"\n> >  #include \"neighbor-exchange-netlink.h\"\n> > -#include \"neighbor-table-notify.h\"\n> >  #include \"openvswitch/poll-loop.h\"\n> >  #include \"openvswitch/vlog.h\"\n> >  #include \"ovn-util.h\"\n> > @@ -136,8 +135,7 @@ neighbor_exchange_run(const struct\n> neighbor_exchange_ctx_in *n_ctx_in,\n> >              break;\n> >          }\n> >\n> > -\n> neighbor_table_add_watch_request(&n_ctx_out->neighbor_table_watches,\n> > -                                         if_index, nim->if_name);\n> > +        vector_push(n_ctx_out->neighbor_table_watches, &if_index);\n> >          vector_destroy(&received_neighbors);\n> >      }\n> >  }\n> > diff --git a/controller/neighbor-exchange.h\n> b/controller/neighbor-exchange.h\n> > index b4257f14c..32c87a8ab 100644\n> > --- a/controller/neighbor-exchange.h\n> > +++ b/controller/neighbor-exchange.h\n> > @@ -30,8 +30,8 @@ struct neighbor_exchange_ctx_in {\n> >  };\n> >\n> >  struct neighbor_exchange_ctx_out {\n> > -    /* Contains struct neighbor_table_watch_request. */\n> > -    struct hmap neighbor_table_watches;\n> > +    /* Contains int32_t representing if_index. */\n> > +    struct vector *neighbor_table_watches;\n> >      /* Contains 'struct evpn_remote_vtep'. */\n> >      struct hmap *remote_vteps;\n> >      /* Contains 'struct evpn_static_entry', remote FDB entries learned\n> through\n> > diff --git a/controller/neighbor-table-notify.c\n> b/controller/neighbor-table-notify.c\n> > deleted file mode 100644\n> > index 04caa21df..000000000\n> > --- a/controller/neighbor-table-notify.c\n> > +++ /dev/null\n> > @@ -1,244 +0,0 @@\n> > -/* Copyright (c) 2025, Red Hat, Inc.\n> > - *\n> > - * Licensed under the Apache License, Version 2.0 (the \"License\");\n> > - * you may not use this file except in compliance with the License.\n> > - * You may obtain a copy of the License at:\n> > - *\n> > - *     http://www.apache.org/licenses/LICENSE-2.0\n> > - *\n> > - * Unless required by applicable law or agreed to in writing, software\n> > - * distributed under the License is distributed on an \"AS IS\" BASIS,\n> > - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n> implied.\n> > - * See the License for the specific language governing permissions and\n> > - * limitations under the License.\n> > - */\n> > -\n> > -#include <config.h>\n> > -\n> > -#include <linux/rtnetlink.h>\n> > -#include <net/if.h>\n> > -\n> > -#include \"hash.h\"\n> > -#include \"hmapx.h\"\n> > -#include \"lib/util.h\"\n> > -#include \"netlink-notifier.h\"\n> > -#include \"openvswitch/vlog.h\"\n> > -\n> > -#include \"neighbor-exchange-netlink.h\"\n> > -#include \"neighbor-table-notify.h\"\n> > -\n> > -VLOG_DEFINE_THIS_MODULE(neighbor_table_notify);\n> > -\n> > -struct neighbor_table_watch_request {\n> > -    struct hmap_node node;\n> > -    int32_t if_index;\n> > -    char if_name[IFNAMSIZ + 1];\n> > -};\n> > -\n> > -struct neighbor_table_watch_entry {\n> > -    struct hmap_node node;\n> > -    int32_t if_index;\n> > -    char if_name[IFNAMSIZ + 1];\n> > -};\n> > -\n> > -static struct hmap watches = HMAP_INITIALIZER(&watches);\n> > -static bool any_neighbor_table_changed;\n> > -static struct ne_table_msg nln_nmsg_change;\n> > -\n> > -static struct nln *nl_neighbor_handle;\n> > -static struct nln_notifier *nl_neighbor_notifier;\n> > -\n> > -static void neighbor_table_change(const void *change_, void *aux);\n> > -\n> > -static void\n> > -neighbor_table_register_notifiers(void)\n> > -{\n> > -    VLOG_INFO(\"Adding neighbor table watchers.\");\n> > -    ovs_assert(!nl_neighbor_handle);\n> > -\n> > -    nl_neighbor_handle = nln_create(NETLINK_ROUTE, ne_table_parse,\n> > -                                    &nln_nmsg_change);\n> > -    ovs_assert(nl_neighbor_handle);\n> > -\n> > -    nl_neighbor_notifier =\n> > -        nln_notifier_create(nl_neighbor_handle, RTNLGRP_NEIGH,\n> > -                            neighbor_table_change, NULL);\n> > -    if (!nl_neighbor_notifier) {\n> > -        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);\n> > -        VLOG_WARN_RL(&rl, \"Failed to create neighbor table watcher.\");\n> > -    }\n> > -}\n> > -\n> > -static void\n> > -neighbor_table_deregister_notifiers(void)\n> > -{\n> > -    VLOG_INFO(\"Removing neighbor table watchers.\");\n> > -    ovs_assert(nl_neighbor_handle);\n> > -\n> > -    nln_notifier_destroy(nl_neighbor_notifier);\n> > -    nln_destroy(nl_neighbor_handle);\n> > -    nl_neighbor_notifier = NULL;\n> > -    nl_neighbor_handle = NULL;\n> > -}\n> > -\n> > -static uint32_t\n> > -neighbor_table_notify_hash_watch(int32_t if_index)\n> > -{\n> > -    /* To allow lookups triggered by netlink messages, don't include the\n> > -     * if_name in the hash.  The netlink updates only include if_index.\n> */\n> > -    return hash_int(if_index, 0);\n> > -}\n> > -\n> > -static void\n> > -add_watch_entry(int32_t if_index, const char *if_name)\n> > -{\n> > -   VLOG_DBG(\"Registering new neighbor table watcher \"\n> > -            \"for interface %s (%\"PRId32\").\",\n> > -            if_name, if_index);\n> > -\n> > -    struct neighbor_table_watch_entry *we;\n> > -    uint32_t hash = neighbor_table_notify_hash_watch(if_index);\n> > -    we = xzalloc(sizeof *we);\n> > -    we->if_index = if_index;\n> > -    ovs_strzcpy(we->if_name, if_name, sizeof we->if_name);\n> > -    hmap_insert(&watches, &we->node, hash);\n> > -\n> > -    if (!nl_neighbor_handle) {\n> > -        neighbor_table_register_notifiers();\n> > -    }\n> > -}\n> > -\n> > -static void\n> > -remove_watch_entry(struct neighbor_table_watch_entry *we)\n> > -{\n> > -    VLOG_DBG(\"Removing neighbor table watcher for interface %s\n> (%\"PRId32\").\",\n> > -             we->if_name, we->if_index);\n> > -    hmap_remove(&watches, &we->node);\n> > -    free(we);\n> > -\n> > -    if (hmap_is_empty(&watches)) {\n> > -        neighbor_table_deregister_notifiers();\n> > -    }\n> > -}\n> > -\n> > -bool\n> > -neighbor_table_notify_run(void)\n> > -{\n> > -    any_neighbor_table_changed = false;\n> > -\n> > -    if (nl_neighbor_handle) {\n> > -        nln_run(nl_neighbor_handle);\n> > -    }\n> > -\n> > -    return any_neighbor_table_changed;\n> > -}\n> > -\n> > -void\n> > -neighbor_table_notify_wait(void)\n> > -{\n> > -    if (nl_neighbor_handle) {\n> > -        nln_wait(nl_neighbor_handle);\n> > -    }\n> > -}\n> > -\n> > -void\n> > -neighbor_table_add_watch_request(struct hmap *neighbor_table_watches,\n> > -                                 int32_t if_index, const char *if_name)\n> > -{\n> > -    struct neighbor_table_watch_request *wr = xzalloc(sizeof *wr);\n> > -\n> > -    wr->if_index = if_index;\n> > -    ovs_strzcpy(wr->if_name, if_name, sizeof wr->if_name);\n> > -    hmap_insert(neighbor_table_watches, &wr->node,\n> > -                neighbor_table_notify_hash_watch(wr->if_index));\n> > -}\n> > -\n> > -void\n> > -neighbor_table_watch_request_cleanup(struct hmap\n> *neighbor_table_watches)\n> > -{\n> > -    struct neighbor_table_watch_request *wr;\n> > -    HMAP_FOR_EACH_POP (wr, node, neighbor_table_watches) {\n> > -        free(wr);\n> > -    }\n> > -}\n> > -\n> > -static struct neighbor_table_watch_entry *\n> > -find_watch_entry(int32_t if_index, const char *if_name)\n> > -{\n> > -    struct neighbor_table_watch_entry *we;\n> > -    uint32_t hash = neighbor_table_notify_hash_watch(if_index);\n> > -    HMAP_FOR_EACH_WITH_HASH (we, node, hash, &watches) {\n> > -        if (if_index == we->if_index && !strcmp(if_name, we->if_name)) {\n> > -            return we;\n> > -        }\n> > -    }\n> > -    return NULL;\n> > -}\n> > -\n> > -static struct neighbor_table_watch_entry *\n> > -find_watch_entry_by_if_index(int32_t if_index)\n> > -{\n> > -    struct neighbor_table_watch_entry *we;\n> > -    uint32_t hash = neighbor_table_notify_hash_watch(if_index);\n> > -    HMAP_FOR_EACH_WITH_HASH (we, node, hash, &watches) {\n> > -        if (if_index == we->if_index) {\n> > -            return we;\n> > -        }\n> > -    }\n> > -    return NULL;\n> > -}\n> > -\n> > -void\n> > -neighbor_table_notify_update_watches(const struct hmap\n> *neighbor_table_watches)\n> > -{\n> > -    struct hmapx sync_watches = HMAPX_INITIALIZER(&sync_watches);\n> > -    struct neighbor_table_watch_entry *we;\n> > -    HMAP_FOR_EACH (we, node, &watches) {\n> > -        hmapx_add(&sync_watches, we);\n> > -    }\n> > -\n> > -    struct neighbor_table_watch_request *wr;\n> > -    HMAP_FOR_EACH (wr, node, neighbor_table_watches) {\n> > -        we = find_watch_entry(wr->if_index, wr->if_name);\n> > -        if (we) {\n> > -            hmapx_find_and_delete(&sync_watches, we);\n> > -        } else {\n> > -            add_watch_entry(wr->if_index, wr->if_name);\n> > -        }\n> > -    }\n> > -\n> > -    struct hmapx_node *node;\n> > -    HMAPX_FOR_EACH (node, &sync_watches) {\n> > -        remove_watch_entry(node->data);\n> > -    }\n> > -\n> > -    hmapx_destroy(&sync_watches);\n> > -}\n> > -\n> > -void\n> > -neighbor_table_notify_destroy(void)\n> > -{\n> > -    struct neighbor_table_watch_entry *we;\n> > -    HMAP_FOR_EACH_SAFE (we, node, &watches) {\n> > -        remove_watch_entry(we);\n> > -    }\n> > -}\n> > -\n> > -static void\n> > -neighbor_table_change(const void *change_, void *aux OVS_UNUSED)\n> > -{\n> > -    /* We currently track whether at least one recent neighbor table\n> change\n> > -     * was detected.  If that's the case already there's no need to\n> > -     * continue. */\n> > -    if (any_neighbor_table_changed) {\n> > -        return;\n> > -    }\n> > -\n> > -    const struct ne_table_msg *change = change_;\n> > -\n> > -    if (change && !ne_is_ovn_owned(&change->nd)) {\n> > -        if (find_watch_entry_by_if_index(change->nd.if_index)) {\n> > -            any_neighbor_table_changed = true;\n> > -        }\n> > -    }\n> > -}\n> > diff --git a/controller/neighbor-table-notify.h\n> b/controller/neighbor-table-notify.h\n> > deleted file mode 100644\n> > index 9f21271cc..000000000\n> > --- a/controller/neighbor-table-notify.h\n> > +++ /dev/null\n> > @@ -1,45 +0,0 @@\n> > -/* Copyright (c) 2025, Red Hat, Inc.\n> > - *\n> > - * Licensed under the Apache License, Version 2.0 (the \"License\");\n> > - * you may not use this file except in compliance with the License.\n> > - * You may obtain a copy of the License at:\n> > - *\n> > - *     http://www.apache.org/licenses/LICENSE-2.0\n> > - *\n> > - * Unless required by applicable law or agreed to in writing, software\n> > - * distributed under the License is distributed on an \"AS IS\" BASIS,\n> > - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n> implied.\n> > - * See the License for the specific language governing permissions and\n> > - * limitations under the License.\n> > - */\n> > -\n> > -#ifndef NEIGHBOR_TABLE_NOTIFY_H\n> > -#define NEIGHBOR_TABLE_NOTIFY_H 1\n> > -\n> > -#include <stdbool.h>\n> > -#include \"openvswitch/hmap.h\"\n> > -\n> > -/* Returns true if any neighbor table has changed enough that we need\n> > - * to learn new neighbor entries. */\n> > -bool neighbor_table_notify_run(void);\n> > -void neighbor_table_notify_wait(void);\n> > -\n> > -/* Add a watch request to the hmap. The hmap should later be passed to\n> > - * neighbor_table_notify_update_watches*/\n> > -void neighbor_table_add_watch_request(struct hmap\n> *neighbor_table_watches,\n> > -                                      int32_t if_index, const char\n> *if_name);\n> > -\n> > -/* Cleanup all watch request in the provided hmap that where added using\n> > - * neighbor_table_add_watch_request. */\n> > -void neighbor_table_watch_request_cleanup(\n> > -    struct hmap *neighbor_table_watches);\n> > -\n> > -/* Updates the list of neighbor table watches that are currently active.\n> > - * hmap should contain struct neighbor_table_watch_request */\n> > -void neighbor_table_notify_update_watches(\n> > -    const struct hmap *neighbor_table_watches);\n> > -\n> > -/* Cleans up all neighbor table watches. */\n> > -void neighbor_table_notify_destroy(void);\n> > -\n> > -#endif /* NEIGHBOR_TABLE_NOTIFY_H */\n> > diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c\n> > index 5b7eb3014..41abd3bae 100644\n> > --- a/controller/ovn-controller.c\n> > +++ b/controller/ovn-controller.c\n> > @@ -53,6 +53,7 @@\n> >  #include \"openvswitch/vlog.h\"\n> >  #include \"ovn/actions.h\"\n> >  #include \"ovn/features.h\"\n> > +#include \"ovn-netlink-notifier.h\"\n> >  #include \"lib/chassis-index.h\"\n> >  #include \"lib/extend-table.h\"\n> >  #include \"lib/ip-mcast-index.h\"\n> > @@ -92,12 +93,12 @@\n> >  #include \"acl-ids.h\"\n> >  #include \"route.h\"\n> >  #include \"route-exchange.h\"\n> > -#include \"route-table-notify.h\"\n> > +#include \"route-table.h\"\n> >  #include \"garp_rarp.h\"\n> >  #include \"host-if-monitor.h\"\n> >  #include \"neighbor.h\"\n> >  #include \"neighbor-exchange.h\"\n> > -#include \"neighbor-table-notify.h\"\n> > +#include \"neighbor-exchange-netlink.h\"\n> >  #include \"evpn-arp.h\"\n> >  #include \"evpn-binding.h\"\n> >  #include \"evpn-fdb.h\"\n> > @@ -5610,6 +5611,30 @@ route_sb_datapath_binding_handler(struct\n> engine_node *node,\n> >      return EN_HANDLED_UNCHANGED;\n> >  }\n> >\n> > +static int\n> > +table_id_cmp(const void *a_, const void *b_)\n> > +{\n> > +    const uint32_t *a = a_;\n> > +    const uint32_t *b = b_;\n> > +\n> > +    return *a < *b ? -1 : *a > *b;\n> > +}\n> > +\n> > +static void\n> > +route_table_notify_update(struct vector *watches)\n> > +{\n> > +    vector_qsort(watches, table_id_cmp);\n> > +\n> > +    bool enabled = !vector_is_empty(watches);\n> > +    ovn_netlink_update_notifier(OVN_NL_NOTIFIER_ROUTE_V4, enabled);\n> > +    ovn_netlink_update_notifier(OVN_NL_NOTIFIER_ROUTE_V6, enabled);\n> > +}\n> > +\n> > +struct ed_type_route_table_notify {\n> > +    /* Vector of ordered 'uint32_t' representing table_ids. */\n> > +    struct vector watches;\n> > +};\n> > +\n> >  struct ed_type_route_exchange {\n> >      /* We need the idl to check if the Learned_Route table exists. */\n> >      struct ovsdb_idl *sb_idl;\n> > @@ -5635,6 +5660,8 @@ en_route_exchange_run(struct engine_node *node,\n> void *data)\n> >\n> >      struct ed_type_route *route_data =\n> >          engine_get_input_data(\"route\", node);\n> > +    struct ed_type_route_table_notify *rt_notify =\n> > +        engine_get_input_data(\"route_table_notify\", node);\n> >\n> >      /* There can not actually be any routes to advertise unless we also\n> have\n> >       * the Learned_Route table, since they where introduced in the same\n> > @@ -5643,6 +5670,8 @@ en_route_exchange_run(struct engine_node *node,\n> void *data)\n> >          return EN_STALE;\n> >      }\n> >\n> > +    vector_clear(&rt_notify->watches);\n> > +\n> >      struct route_exchange_ctx_in r_ctx_in = {\n> >          .ovnsb_idl_txn = engine_get_context()->ovnsb_idl_txn,\n> >          .sbrec_learned_route_by_datapath =\n> sbrec_learned_route_by_datapath,\n> > @@ -5651,15 +5680,11 @@ en_route_exchange_run(struct engine_node *node,\n> void *data)\n> >      };\n> >      struct route_exchange_ctx_out r_ctx_out = {\n> >          .sb_changes_pending = false,\n> > +        .route_table_watches = &rt_notify->watches,\n> >      };\n> >\n> > -    hmap_init(&r_ctx_out.route_table_watches);\n> > -\n> >      route_exchange_run(&r_ctx_in, &r_ctx_out);\n> > -    route_table_notify_update_watches(&r_ctx_out.route_table_watches);\n> > -\n> > -    route_table_watch_request_cleanup(&r_ctx_out.route_table_watches);\n> > -    hmap_destroy(&r_ctx_out.route_table_watches);\n> > +    route_table_notify_update(&rt_notify->watches);\n> >\n> >      re->sb_changes_pending = r_ctx_out.sb_changes_pending;\n> >\n> > @@ -5693,23 +5718,40 @@ en_route_exchange_cleanup(void *data OVS_UNUSED)\n> >  {\n> >  }\n> >\n> > -struct ed_type_route_table_notify {\n> > -    /* For incremental processing this could be tracked per datapath in\n> > -     * the future. */\n> > -    bool changed;\n> > -};\n> > -\n> > +/* The route_table_notify node is an input node, but the watches are\n> > + * populated by route_exchange node. The reason being that engine\n> > + * periodically runs input nodes to check if there are updates, so it\n> > + * could process the other nodes, however the route_table_notify cannot\n> > + * be dependent on other node because it wouldn't be input node\n> anymore. */\n> >  static enum engine_node_state\n> >  en_route_table_notify_run(struct engine_node *node OVS_UNUSED, void\n> *data)\n> >  {\n> > +    enum engine_node_state state = EN_UNCHANGED;\n> >      struct ed_type_route_table_notify *rtn = data;\n> > -    enum engine_node_state state;\n> > -    if (rtn->changed) {\n> > -        state = EN_UPDATED;\n> > -    } else {\n> > -        state = EN_UNCHANGED;\n> > +    struct vector *msgs;\n> > +    uint32_t *table_id;\n> > +\n> > +    msgs = ovn_netlink_get_msgs(OVN_NL_NOTIFIER_ROUTE_V4);\n> > +    VECTOR_FOR_EACH_PTR (msgs, table_id) {\n> > +        if (vector_bsearch(&rtn->watches, table_id, table_id_cmp)) {\n> > +            state = EN_UPDATED;\n> > +            break;\n> > +        }\n> >      }\n> > -    rtn->changed = false;\n> > +\n> > +    if (state != EN_UPDATED) {\n> > +        msgs = ovn_netlink_get_msgs(OVN_NL_NOTIFIER_ROUTE_V6);\n> > +        VECTOR_FOR_EACH_PTR (msgs, table_id) {\n> > +            if (vector_bsearch(&rtn->watches, table_id, table_id_cmp)) {\n> > +                state = EN_UPDATED;\n> > +                break;\n> > +            }\n> > +        }\n> > +    }\n> > +\n> > +    ovn_netlink_notifier_flush(OVN_NL_NOTIFIER_ROUTE_V4);\n> > +    ovn_netlink_notifier_flush(OVN_NL_NOTIFIER_ROUTE_V6);\n> > +\n> >      return state;\n> >  }\n> >\n> > @@ -5718,14 +5760,19 @@ static void *\n> >  en_route_table_notify_init(struct engine_node *node OVS_UNUSED,\n> >                             struct engine_arg *arg OVS_UNUSED)\n> >  {\n> > -    struct ed_type_route_table_notify *rtn = xzalloc(sizeof *rtn);\n> > -    rtn->changed = true;\n> > +    struct ed_type_route_table_notify *rtn = xmalloc(sizeof *rtn);\n> > +\n> > +    *rtn = (struct ed_type_route_table_notify) {\n> > +        .watches = VECTOR_EMPTY_INITIALIZER(uint32_t),\n> > +    };\n> >      return rtn;\n> >  }\n> >\n> >  static void\n> >  en_route_table_notify_cleanup(void *data OVS_UNUSED)\n> >  {\n> > +    struct ed_type_route_table_notify *rtn = data;\n> > +    vector_destroy(&rtn->watches);\n> >  }\n> >\n> >  struct ed_type_route_exchange_status {\n> > @@ -6226,10 +6273,32 @@ neighbor_sb_port_binding_handler(struct\n> engine_node *node, void *data)\n> >      return EN_HANDLED_UNCHANGED;\n> >  }\n> >\n> > +static int\n> > +if_index_cmp(const void *a_, const void *b_)\n> > +{\n> > +    const int32_t *a = a_;\n> > +    const int32_t *b = b_;\n> > +\n> > +    return *a < *b ? -1 : *a > *b;\n> > +}\n> > +\n> > +static void\n> > +neighbor_table_notify_update(struct vector *watches)\n> > +{\n> > +    vector_qsort(watches, if_index_cmp);\n> > +\n> > +    bool enabled = !vector_is_empty(watches);\n> > +    ovn_netlink_update_notifier(OVN_NL_NOTIFIER_NEIGHBOR, enabled);\n> > +}\n> > +\n> > +/* The neighbor_table_notify node is an input node, but the watches are\n> > + * populated by en_neighbor_exchange node. The reason being that engine\n> > + * periodically runs input nodes to check if there are updates, so it\n> > + * could process the other nodes, however the neighbor_table_notify\n> cannot\n> > + * be dependent on other node because it wouldn't be input node\n> anymore. */\n> >  struct ed_type_neighbor_table_notify {\n> > -    /* For incremental processing this could be tracked per interface in\n> > -     * the future. */\n> > -    bool changed;\n> > +    /* Vector of ordered 'int32_t' representing if_indexes. */\n> > +    struct vector watches;\n> >  };\n> >\n> >  static void *\n> > @@ -6239,7 +6308,7 @@ en_neighbor_table_notify_init(struct engine_node\n> *node OVS_UNUSED,\n> >      struct ed_type_neighbor_table_notify *ntn = xmalloc(sizeof *ntn);\n> >\n> >      *ntn = (struct ed_type_neighbor_table_notify) {\n> > -        .changed = true,\n> > +        .watches = VECTOR_EMPTY_INITIALIZER(int32_t),\n> >      };\n> >      return ntn;\n> >  }\n> > @@ -6247,20 +6316,31 @@ en_neighbor_table_notify_init(struct engine_node\n> *node OVS_UNUSED,\n> >  static void\n> >  en_neighbor_table_notify_cleanup(void *data OVS_UNUSED)\n> >  {\n> > +    struct ed_type_neighbor_table_notify *ntn = data;\n> > +    vector_destroy(&ntn->watches);\n> >  }\n> >\n> >  static enum engine_node_state\n> >  en_neighbor_table_notify_run(struct engine_node *node OVS_UNUSED,\n> >                               void *data)\n> >  {\n> > +    enum engine_node_state state = EN_UNCHANGED;\n> >      struct ed_type_neighbor_table_notify *ntn = data;\n> > -    enum engine_node_state state;\n> > -    if (ntn->changed) {\n> > -        state = EN_UPDATED;\n> > -    } else {\n> > -        state = EN_UNCHANGED;\n> > +    struct vector *msgs;\n> > +    struct ne_table_msg *ne_msg;\n> > +\n> > +    msgs = ovn_netlink_get_msgs(OVN_NL_NOTIFIER_NEIGHBOR);\n> > +    VECTOR_FOR_EACH_PTR (msgs, ne_msg) {\n> > +        if (vector_bsearch(&ntn->watches,\n> > +                           &ne_msg->nd.if_index,\n> > +                           if_index_cmp)) {\n> > +            state = EN_UPDATED;\n> > +            break;\n> > +        }\n> >      }\n> > -    ntn->changed = false;\n> > +\n> > +    ovn_netlink_notifier_flush(OVN_NL_NOTIFIER_NEIGHBOR);\n> > +\n> >      return state;\n> >  }\n> >\n> > @@ -6307,27 +6387,26 @@ en_neighbor_exchange_run(struct engine_node\n> *node, void *data_)\n> >      struct ed_type_neighbor_exchange *data = data_;\n> >      const struct ed_type_neighbor *neighbor_data =\n> >          engine_get_input_data(\"neighbor\", node);\n> > +    struct ed_type_neighbor_table_notify *nt_notify =\n> > +        engine_get_input_data(\"neighbor_table_notify\", node);\n> >\n> >      evpn_remote_vteps_clear(&data->remote_vteps);\n> >      evpn_static_entries_clear(&data->static_fdbs);\n> >      evpn_static_entries_clear(&data->static_arps);\n> > +    vector_clear(&nt_notify->watches);\n> >\n> >      struct neighbor_exchange_ctx_in n_ctx_in = {\n> >          .monitored_interfaces = &neighbor_data->monitored_interfaces,\n> >      };\n> >      struct neighbor_exchange_ctx_out n_ctx_out = {\n> > -        .neighbor_table_watches =\n> > -            HMAP_INITIALIZER(&n_ctx_out.neighbor_table_watches),\n> > +        .neighbor_table_watches = &nt_notify->watches,\n> >          .remote_vteps = &data->remote_vteps,\n> >          .static_fdbs = &data->static_fdbs,\n> >          .static_arps = &data->static_arps,\n> >      };\n> >\n> >      neighbor_exchange_run(&n_ctx_in, &n_ctx_out);\n> > -\n> neighbor_table_notify_update_watches(&n_ctx_out.neighbor_table_watches);\n> > -\n> > -\n> neighbor_table_watch_request_cleanup(&n_ctx_out.neighbor_table_watches);\n> > -    hmap_destroy(&n_ctx_out.neighbor_table_watches);\n> > +    neighbor_table_notify_update(&nt_notify->watches);\n> >\n> >      return EN_UPDATED;\n> >  }\n> > @@ -7792,18 +7871,12 @@ main(int argc, char *argv[])\n> >                                 &transport_zones,\n> >                                 bridge_table);\n> >\n> > -                    struct ed_type_route_table_notify *rtn =\n> > -\n> engine_get_internal_data(&en_route_table_notify);\n> > -                    rtn->changed = route_table_notify_run();\n> > +                    ovn_netlink_notifiers_run();\n> >\n> >                      struct ed_type_host_if_monitor *hifm =\n> >                          engine_get_internal_data(&en_host_if_monitor);\n> >                      hifm->changed = host_if_monitor_run();\n> >\n> > -                    struct ed_type_neighbor_table_notify *ntn =\n> > -\n> engine_get_internal_data(&en_neighbor_table_notify);\n> > -                    ntn->changed = neighbor_table_notify_run();\n> > -\n> >                      struct ed_type_route_exchange_status *rt_res =\n> >\n> engine_get_internal_data(&en_route_exchange_status);\n> >                      rt_res->netlink_trigger_run =\n> > @@ -8131,9 +8204,8 @@ main(int argc, char *argv[])\n> >              }\n> >\n> >              binding_wait();\n> > -            route_table_notify_wait();\n> >              host_if_monitor_wait();\n> > -            neighbor_table_notify_wait();\n> > +            ovn_netlink_notifiers_wait();\n> >          }\n> >\n> >          unixctl_server_run(unixctl);\n> > @@ -8306,8 +8378,7 @@ loop_done:\n> >      ovsrcu_exit();\n> >      dns_resolve_destroy();\n> >      route_exchange_destroy();\n> > -    route_table_notify_destroy();\n> > -    neighbor_table_notify_destroy();\n> > +    ovn_netlink_notifiers_destroy();\n> >\n> >      exit(retval);\n> >  }\n> > diff --git a/controller/neighbor-table-notify-stub.c\n> b/controller/ovn-netlink-notifier-stub.c\n> > similarity index 51%\n> > rename from controller/neighbor-table-notify-stub.c\n> > rename to controller/ovn-netlink-notifier-stub.c\n> > index bb4fe5991..a90aa6a4a 100644\n> > --- a/controller/neighbor-table-notify-stub.c\n> > +++ b/controller/ovn-netlink-notifier-stub.c\n> > @@ -1,4 +1,5 @@\n> > -/* Copyright (c) 2025, Red Hat, Inc.\n> > +/* Copyright (c) 2025, STACKIT GmbH & Co. KG\n> > + * Copyright (c) 2026, Red Hat, Inc.\n> >   *\n> >   * Licensed under the Apache License, Version 2.0 (the \"License\");\n> >   * you may not use this file except in compliance with the License.\n> > @@ -14,44 +15,42 @@\n> >   */\n> >\n> >  #include <config.h>\n> > -\n> > -#include <stdbool.h>\n> > +#include <stddef.h>\n> >\n> >  #include \"openvswitch/compiler.h\"\n> > -#include \"neighbor-table-notify.h\"\n> > +#include \"ovn-netlink-notifier.h\"\n> > +#include \"vec.h\"\n> > +\n> > +static struct vector empty = VECTOR_EMPTY_INITIALIZER(uint8_t);\n> >\n> > -bool\n> > -neighbor_table_notify_run(void)\n> > +void\n> > +ovn_netlink_update_notifier(enum ovn_netlink_notifier_type type\n> OVS_UNUSED,\n> > +                            bool enabled OVS_UNUSED)\n> >  {\n> > -    return false;\n> >  }\n> >\n> > -void\n> > -neighbor_table_notify_wait(void)\n> > +struct vector *\n> > +ovn_netlink_get_msgs(enum ovn_netlink_notifier_type type OVS_UNUSED)\n> >  {\n> > +    return &empty;\n> >  }\n> >\n> >  void\n> > -neighbor_table_add_watch_request(\n> > -    struct hmap *neighbor_table_watches OVS_UNUSED,\n> > -    int32_t if_index OVS_UNUSED,\n> > -    const char *if_name OVS_UNUSED)\n> > +ovn_netlink_notifier_flush(enum ovn_netlink_notifier_type type\n> OVS_UNUSED)\n> >  {\n> >  }\n> >\n> >  void\n> > -neighbor_table_watch_request_cleanup(\n> > -    struct hmap *neighbor_table_watches OVS_UNUSED)\n> > +ovn_netlink_notifiers_run(void)\n> >  {\n> >  }\n> >\n> >  void\n> > -neighbor_table_notify_update_watches(\n> > -    const struct hmap *neighbor_table_watches OVS_UNUSED)\n> > +ovn_netlink_notifiers_wait(void)\n> >  {\n> >  }\n> >\n> >  void\n> > -neighbor_table_notify_destroy(void)\n> > +ovn_netlink_notifiers_destroy(void)\n> >  {\n> >  }\n> > diff --git a/controller/ovn-netlink-notifier.c\n> b/controller/ovn-netlink-notifier.c\n> > new file mode 100644\n> > index 000000000..defa1cd54\n> > --- /dev/null\n> > +++ b/controller/ovn-netlink-notifier.c\n> > @@ -0,0 +1,251 @@\n> > +/* Copyright (c) 2026, Red Hat, Inc.\n> > + *\n> > + * Licensed under the Apache License, Version 2.0 (the \"License\");\n> > + * you may not use this file except in compliance with the License.\n> > + * You may obtain a copy of the License at:\n> > + *\n> > + *     http://www.apache.org/licenses/LICENSE-2.0\n> > + *\n> > + * Unless required by applicable law or agreed to in writing, software\n> > + * distributed under the License is distributed on an \"AS IS\" BASIS,\n> > + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n> implied.\n> > + * See the License for the specific language governing permissions and\n> > + * limitations under the License.\n> > + */\n> > +\n> > +#include <config.h>\n> > +\n> > +#include <linux/rtnetlink.h>\n> > +#include <net/if.h>\n> > +\n> > +#include \"neighbor-exchange-netlink.h\"\n> > +#include \"netlink-notifier.h\"\n> > +#include \"route-exchange-netlink.h\"\n> > +#include \"route-table.h\"\n> > +#include \"vec.h\"\n> > +\n> > +#include \"openvswitch/vlog.h\"\n> > +\n> > +#include \"ovn-netlink-notifier.h\"\n> > +\n> > +VLOG_DEFINE_THIS_MODULE(ovn_netlink_notifier);\n> > +\n> > +#define NOTIFIER_MSGS_CAPACITY_THRESHOLD 1024\n> > +\n> > +struct ovn_netlink_notifier {\n> > +    /* Group for which we want to receive the notification. */\n> > +    int group;\n> > +    /* The notifier pointers. */\n> > +    struct nln_notifier *nln_notifier;\n> > +    /* Messages received by given notifier. */\n> > +    struct vector msgs;\n> > +    /* Notifier change handler. */\n> > +    nln_notify_func *change_handler;\n> > +    /* Name of the notifier. */\n> > +    const char *name;\n> > +};\n> > +\n> > +union ovn_notifier_msg_change {\n> > +    struct route_table_msg route;\n> > +    struct ne_table_msg neighbor;\n> > +};\n> > +\n> > +static void ovn_netlink_route_change_handler(const void *change_, void\n> *aux);\n> > +static void ovn_netlink_neighbor_change_handler(const void *change_,\n> > +                                                void *aux);\n> > +\n> > +static struct ovn_netlink_notifier notifiers[OVN_NL_NOTIFIER_MAX] = {\n> > +    [OVN_NL_NOTIFIER_ROUTE_V4] = {\n> > +        .group = RTNLGRP_IPV4_ROUTE,\n> > +        .msgs = VECTOR_EMPTY_INITIALIZER(uint32_t),\n> > +        .change_handler = ovn_netlink_route_change_handler,\n> > +        .name = \"route-ipv4\",\n> > +    },\n> > +    [OVN_NL_NOTIFIER_ROUTE_V6] = {\n> > +        .group = RTNLGRP_IPV6_ROUTE,\n> > +        .msgs = VECTOR_EMPTY_INITIALIZER(uint32_t),\n> > +        .change_handler = ovn_netlink_route_change_handler,\n> > +        .name = \"route-ipv6\",\n> > +    },\n> > +    [OVN_NL_NOTIFIER_NEIGHBOR] = {\n> > +        .group = RTNLGRP_NEIGH,\n> > +        .msgs = VECTOR_EMPTY_INITIALIZER(struct ne_table_msg),\n> > +        .change_handler = ovn_netlink_neighbor_change_handler,\n> > +        .name = \"neighbor\",\n> > +    },\n> > +};\n> > +\n> > +static struct nln *nln_handle;\n> > +static union ovn_notifier_msg_change nln_msg_change;\n> > +\n> > +static int\n> > +ovn_netlink_notifier_parse(struct ofpbuf *buf, void *change_)\n> > +{\n> > +    struct nlmsghdr *nlmsg = ofpbuf_at(buf, 0, NLMSG_HDRLEN);\n> > +    if (!nlmsg) {\n> > +        return 0;\n> > +    }\n> > +\n> > +    union ovn_notifier_msg_change *change = change_;\n> > +    if (nlmsg->nlmsg_type == RTM_NEWROUTE ||\n> > +        nlmsg->nlmsg_type == RTM_DELROUTE) {\n> > +        return route_table_parse(buf, &change->route);\n> > +    }\n> > +\n> > +    if (nlmsg->nlmsg_type == RTM_NEWNEIGH ||\n> > +        nlmsg->nlmsg_type == RTM_DELNEIGH) {\n> > +        return ne_table_parse(buf, &change->neighbor);\n> > +    }\n> > +\n> > +    return 0;\n> > +}\n> > +\n> > +static void\n> > +ovn_netlink_route_change_handler(const void *change_, void *aux)\n> > +{\n> > +    if (!change_) {\n> > +        return;\n> > +    }\n> > +\n> > +    struct ovn_netlink_notifier *notifier = aux;\n> > +    union ovn_notifier_msg_change *change =\n> > +        CONST_CAST(union ovn_notifier_msg_change *, change_);\n> > +\n> > +    struct route_data *rd = &change->route.rd;\n> > +    if (rd->rtm_protocol != RTPROT_OVN) {\n> > +        /* We just cannot copy the whole route_data because it has\n> reference\n> > +         * to self for the nexthop list. */\n> > +        vector_push(&notifier->msgs, &rd->rta_table_id);\n> > +    }\n> > +\n> > +    route_data_destroy(rd);\n> > +}\n> > +\n> > +static void\n> > +ovn_netlink_neighbor_change_handler(const void *change_, void *aux)\n> > +{\n> > +    if (!change_) {\n> > +        return;\n> > +    }\n> > +\n> > +    struct ovn_netlink_notifier *notifier = aux;\n> > +    const union ovn_notifier_msg_change *change = change_;\n> > +\n> > +    if (!ne_is_ovn_owned(&change->neighbor.nd)) {\n> > +        vector_push(&notifier->msgs, &change->neighbor);\n> > +    }\n> > +}\n> > +\n> > +static void\n> > +ovn_netlink_register_notifier(enum ovn_netlink_notifier_type type)\n> > +{\n> > +    ovs_assert(type < OVN_NL_NOTIFIER_MAX);\n> > +\n> > +    struct ovn_netlink_notifier *notifier = &notifiers[type];\n> > +    if (notifier->nln_notifier) {\n> > +        return;\n> > +    }\n> > +\n> > +    VLOG_INFO(\"Adding %s table watchers.\", notifier->name);\n> > +    if (!nln_handle) {\n> > +        nln_handle = nln_create(NETLINK_ROUTE,\n> ovn_netlink_notifier_parse,\n> > +                                &nln_msg_change);\n> > +        ovs_assert(nln_handle);\n> > +    }\n> > +\n> > +    notifier->nln_notifier = nln_notifier_create(nln_handle,\n> notifier->group,\n> > +\n>  notifier->change_handler,\n> > +                                                 notifier);\n> > +\n> > +    if (!notifier->nln_notifier) {\n> > +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);\n> > +        VLOG_WARN_RL(&rl, \"Failed to create %s table watcher.\",\n> > +                     notifier->name);\n> > +    }\n> > +}\n> > +\n> > +static void\n> > +ovn_netlink_deregister_notifier(enum ovn_netlink_notifier_type type)\n> > +{\n> > +    ovs_assert(type < OVN_NL_NOTIFIER_MAX);\n> > +\n> > +    struct ovn_netlink_notifier *notifier = &notifiers[type];\n> > +    if (!notifier->nln_notifier) {\n> > +        return;\n> > +    }\n> > +\n> > +    VLOG_INFO(\"Removing %s table watchers.\", notifier->name);\n> > +    nln_notifier_destroy(notifier->nln_notifier);\n> > +    notifier->nln_notifier = NULL;\n> > +\n> > +    size_t i;\n> > +    for (i = 0; i < OVN_NL_NOTIFIER_MAX; i++) {\n> > +        if (notifiers[i].nln_notifier) {\n> > +            break;\n> > +        }\n> > +    }\n> > +\n> > +    if (i == OVN_NL_NOTIFIER_MAX) {\n> > +        /* This was the last notifier, destroy the handle too. */\n> > +        nln_destroy(nln_handle);\n> > +        nln_handle = NULL;\n> > +    }\n> > +}\n> > +\n> > +void\n> > +ovn_netlink_update_notifier(enum ovn_netlink_notifier_type type, bool\n> enabled)\n> > +{\n> > +    if (enabled) {\n> > +        ovn_netlink_register_notifier(type);\n> > +    } else {\n> > +        ovn_netlink_deregister_notifier(type);\n> > +    }\n> > +}\n> > +\n> > +struct vector *\n> > +ovn_netlink_get_msgs(enum ovn_netlink_notifier_type type)\n> > +{\n> > +    ovs_assert(type < OVN_NL_NOTIFIER_MAX);\n> > +    return &notifiers[type].msgs;\n> > +}\n> > +\n> > +void\n> > +ovn_netlink_notifier_flush(enum ovn_netlink_notifier_type type)\n> > +{\n> > +    ovs_assert(type < OVN_NL_NOTIFIER_MAX);\n> > +    struct ovn_netlink_notifier *notifier = &notifiers[type];\n> > +    vector_clear(&notifier->msgs);\n> > +}\n> > +\n> > +void\n> > +ovn_netlink_notifiers_run(void)\n> > +{\n> > +    for (size_t i = 0; i < OVN_NL_NOTIFIER_MAX; i++) {\n> > +        if (vector_capacity(&notifiers[i].msgs) >\n> > +            NOTIFIER_MSGS_CAPACITY_THRESHOLD) {\n> > +            vector_shrink_to_fit(&notifiers[i].msgs);\n> > +        }\n> > +    }\n> > +\n> > +    if (nln_handle) {\n> > +        nln_run(nln_handle);\n> > +    }\n> > +}\n> > +\n> > +void\n> > +ovn_netlink_notifiers_wait(void)\n> > +{\n> > +    if (nln_handle) {\n> > +        nln_wait(nln_handle);\n> > +    }\n> > +}\n> > +\n> > +void\n> > +ovn_netlink_notifiers_destroy(void)\n> > +{\n> > +    for (size_t i = 0; i < OVN_NL_NOTIFIER_MAX; i++) {\n> > +        ovn_netlink_notifier_flush(i);\n> > +        ovn_netlink_deregister_notifier(i);\n> > +        vector_destroy(&notifiers[i].msgs);\n> > +    }\n> > +}\n> > diff --git a/controller/ovn-netlink-notifier.h\n> b/controller/ovn-netlink-notifier.h\n> > new file mode 100644\n> > index 000000000..b78fe466b\n> > --- /dev/null\n> > +++ b/controller/ovn-netlink-notifier.h\n> > @@ -0,0 +1,38 @@\n> > +/* Copyright (c) 2026, Red Hat, Inc.\n> > + *\n> > + * Licensed under the Apache License, Version 2.0 (the \"License\");\n> > + * you may not use this file except in compliance with the License.\n> > + * You may obtain a copy of the License at:\n> > + *\n> > + *     http://www.apache.org/licenses/LICENSE-2.0\n> > + *\n> > + * Unless required by applicable law or agreed to in writing, software\n> > + * distributed under the License is distributed on an \"AS IS\" BASIS,\n> > + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n> implied.\n> > + * See the License for the specific language governing permissions and\n> > + * limitations under the License.\n> > + */\n> > +\n> > +#ifndef OVN_NETLINK_NOTIFIER_H\n> > +#define OVN_NETLINK_NOTIFIER_H 1\n> > +\n> > +#include <stdbool.h>\n> > +\n> > +struct vector;\n> > +\n> > +enum ovn_netlink_notifier_type {\n> > +    OVN_NL_NOTIFIER_ROUTE_V4,\n> > +    OVN_NL_NOTIFIER_ROUTE_V6,\n> > +    OVN_NL_NOTIFIER_NEIGHBOR,\n> > +    OVN_NL_NOTIFIER_MAX,\n> > +};\n> > +\n> > +void ovn_netlink_update_notifier(enum ovn_netlink_notifier_type type,\n> > +                                 bool enabled);\n> > +struct vector *ovn_netlink_get_msgs(enum ovn_netlink_notifier_type\n> type);\n> > +void ovn_netlink_notifier_flush(enum ovn_netlink_notifier_type type);\n> > +void ovn_netlink_notifiers_run(void);\n> > +void ovn_netlink_notifiers_wait(void);\n> > +void ovn_netlink_notifiers_destroy(void);\n> > +\n> > +#endif /* OVN_NETLINK_NOTIFIER_H */\n> > diff --git a/controller/route-exchange-netlink.h\n> b/controller/route-exchange-netlink.h\n> > index 3ebd4546f..8ba8a1039 100644\n> > --- a/controller/route-exchange-netlink.h\n> > +++ b/controller/route-exchange-netlink.h\n> > @@ -39,6 +39,7 @@\n> >  struct in6_addr;\n> >  struct hmap;\n> >  struct vector;\n> > +struct advertise_route_entry;\n> >\n> >  struct re_nl_received_route_node {\n> >      const struct sbrec_datapath_binding *db;\n> > diff --git a/controller/route-exchange.c b/controller/route-exchange.c\n> > index ae44ffe69..82727f4e4 100644\n> > --- a/controller/route-exchange.c\n> > +++ b/controller/route-exchange.c\n> > @@ -31,7 +31,6 @@\n> >  #include \"ha-chassis.h\"\n> >  #include \"local_data.h\"\n> >  #include \"route.h\"\n> > -#include \"route-table-notify.h\"\n> >  #include \"route-exchange.h\"\n> >  #include \"route-exchange-netlink.h\"\n> >\n> > @@ -306,8 +305,7 @@ route_exchange_run(const struct\n> route_exchange_ctx_in *r_ctx_in,\n> >\n>  r_ctx_in->sbrec_learned_route_by_datapath,\n> >                                 &r_ctx_out->sb_changes_pending);\n> >\n> > -        route_table_add_watch_request(&r_ctx_out->route_table_watches,\n> > -                                      table_id);\n> > +        vector_push(r_ctx_out->route_table_watches, &table_id);\n> >\n> >          vector_destroy(&received_routes);\n> >      }\n> > diff --git a/controller/route-exchange.h b/controller/route-exchange.h\n> > index e3791c331..25db35568 100644\n> > --- a/controller/route-exchange.h\n> > +++ b/controller/route-exchange.h\n> > @@ -30,7 +30,7 @@ struct route_exchange_ctx_in {\n> >  };\n> >\n> >  struct route_exchange_ctx_out {\n> > -    struct hmap route_table_watches;\n> > +    struct vector *route_table_watches;\n> >      bool sb_changes_pending;\n> >  };\n> >\n> > diff --git a/controller/route-table-notify-stub.c\n> b/controller/route-table-notify-stub.c\n> > deleted file mode 100644\n> > index 460c81dbe..000000000\n> > --- a/controller/route-table-notify-stub.c\n> > +++ /dev/null\n> > @@ -1,55 +0,0 @@\n> > -/*\n> > - * Copyright (c) 2025, STACKIT GmbH & Co. KG\n> > - *\n> > - * Licensed under the Apache License, Version 2.0 (the \"License\");\n> > - * you may not use this file except in compliance with the License.\n> > - * You may obtain a copy of the License at:\n> > - *\n> > - *     http://www.apache.org/licenses/LICENSE-2.0\n> > - *\n> > - * Unless required by applicable law or agreed to in writing, software\n> > - * distributed under the License is distributed on an \"AS IS\" BASIS,\n> > - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n> implied.\n> > - * See the License for the specific language governing permissions and\n> > - * limitations under the License.\n> > - */\n> > -\n> > -#include <config.h>\n> > -\n> > -#include <stdbool.h>\n> > -\n> > -#include \"openvswitch/compiler.h\"\n> > -#include \"route-table-notify.h\"\n> > -\n> > -bool\n> > -route_table_notify_run(void)\n> > -{\n> > -    return false;\n> > -}\n> > -\n> > -void\n> > -route_table_notify_wait(void)\n> > -{\n> > -}\n> > -\n> > -void\n> > -route_table_add_watch_request(struct hmap *route_table_watches\n> OVS_UNUSED,\n> > -                              uint32_t table_id OVS_UNUSED)\n> > -{\n> > -}\n> > -\n> > -void\n> > -route_table_watch_request_cleanup(struct hmap *route_table_watches\n> OVS_UNUSED)\n> > -{\n> > -}\n> > -\n> > -void\n> > -route_table_notify_update_watches(\n> > -    const struct hmap *route_table_watches OVS_UNUSED)\n> > -{\n> > -}\n> > -\n> > -void\n> > -route_table_notify_destroy(void)\n> > -{\n> > -}\n> > diff --git a/controller/route-table-notify.c\n> b/controller/route-table-notify.c\n> > deleted file mode 100644\n> > index 9fa2e0ea6..000000000\n> > --- a/controller/route-table-notify.c\n> > +++ /dev/null\n> > @@ -1,238 +0,0 @@\n> > -/*\n> > - * Copyright (c) 2025, STACKIT GmbH & Co. KG\n> > - *\n> > - * Licensed under the Apache License, Version 2.0 (the \"License\");\n> > - * you may not use this file except in compliance with the License.\n> > - * You may obtain a copy of the License at:\n> > - *\n> > - *     http://www.apache.org/licenses/LICENSE-2.0\n> > - *\n> > - * Unless required by applicable law or agreed to in writing, software\n> > - * distributed under the License is distributed on an \"AS IS\" BASIS,\n> > - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n> implied.\n> > - * See the License for the specific language governing permissions and\n> > - * limitations under the License.\n> > - */\n> > -\n> > -#include <config.h>\n> > -\n> > -#include <net/if.h>\n> > -#include <linux/rtnetlink.h>\n> > -\n> > -#include \"netlink-notifier.h\"\n> > -#include \"openvswitch/vlog.h\"\n> > -\n> > -#include \"binding.h\"\n> > -#include \"hash.h\"\n> > -#include \"hmapx.h\"\n> > -#include \"route-table.h\"\n> > -#include \"route.h\"\n> > -#include \"route-table-notify.h\"\n> > -#include \"route-exchange-netlink.h\"\n> > -\n> > -VLOG_DEFINE_THIS_MODULE(route_table_notify);\n> > -\n> > -struct route_table_watch_request {\n> > -    struct hmap_node node;\n> > -    uint32_t table_id;\n> > -};\n> > -\n> > -struct route_table_watch_entry {\n> > -    struct hmap_node node;\n> > -    uint32_t table_id;\n> > -};\n> > -\n> > -static struct hmap watches = HMAP_INITIALIZER(&watches);\n> > -static bool any_route_table_changed;\n> > -static struct route_table_msg nln_rtmsg_change;\n> > -\n> > -static struct nln *nl_route_handle;\n> > -static struct nln_notifier *nl_route_notifier_v4;\n> > -static struct nln_notifier *nl_route_notifier_v6;\n> > -\n> > -static void route_table_change(const void *change_, void *aux);\n> > -\n> > -static void\n> > -route_table_register_notifiers(void)\n> > -{\n> > -    VLOG_INFO(\"Adding route table watchers.\");\n> > -    ovs_assert(!nl_route_handle);\n> > -\n> > -    nl_route_handle = nln_create(NETLINK_ROUTE, route_table_parse,\n> > -                                 &nln_rtmsg_change);\n> > -    ovs_assert(nl_route_handle);\n> > -\n> > -    nl_route_notifier_v4 =\n> > -        nln_notifier_create(nl_route_handle, RTNLGRP_IPV4_ROUTE,\n> > -                            route_table_change, NULL);\n> > -    if (!nl_route_notifier_v4) {\n> > -        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);\n> > -        VLOG_WARN_RL(&rl, \"Failed to create ipv4 route table watcher.\");\n> > -    }\n> > -\n> > -    nl_route_notifier_v6 =\n> > -        nln_notifier_create(nl_route_handle, RTNLGRP_IPV6_ROUTE,\n> > -                            route_table_change, NULL);\n> > -    if (!nl_route_notifier_v6) {\n> > -        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);\n> > -        VLOG_WARN_RL(&rl, \"Failed to create ipv6 route table watcher.\");\n> > -    }\n> > -}\n> > -\n> > -static void\n> > -route_table_deregister_notifiers(void)\n> > -{\n> > -    VLOG_INFO(\"Removing route table watchers.\");\n> > -    ovs_assert(nl_route_handle);\n> > -\n> > -    nln_notifier_destroy(nl_route_notifier_v4);\n> > -    nln_notifier_destroy(nl_route_notifier_v6);\n> > -    nln_destroy(nl_route_handle);\n> > -    nl_route_notifier_v4 = NULL;\n> > -    nl_route_notifier_v6 = NULL;\n> > -    nl_route_handle = NULL;\n> > -}\n> > -\n> > -static uint32_t\n> > -route_table_notify_hash_watch(uint32_t table_id)\n> > -{\n> > -    return hash_int(table_id, 0);\n> > -}\n> > -\n> > -void\n> > -route_table_add_watch_request(struct hmap *route_table_watches,\n> > -                              uint32_t table_id)\n> > -{\n> > -    struct route_table_watch_request *wr = xzalloc(sizeof *wr);\n> > -    wr->table_id = table_id;\n> > -    hmap_insert(route_table_watches, &wr->node,\n> > -                route_table_notify_hash_watch(wr->table_id));\n> > -}\n> > -\n> > -void\n> > -route_table_watch_request_cleanup(struct hmap *route_table_watches)\n> > -{\n> > -    struct route_table_watch_request *wr;\n> > -    HMAP_FOR_EACH_POP (wr, node, route_table_watches) {\n> > -        free(wr);\n> > -    }\n> > -}\n> > -\n> > -static struct route_table_watch_entry *\n> > -find_watch_entry(uint32_t table_id)\n> > -{\n> > -    struct route_table_watch_entry *we;\n> > -    uint32_t hash = route_table_notify_hash_watch(table_id);\n> > -    HMAP_FOR_EACH_WITH_HASH (we, node, hash, &watches) {\n> > -        if (table_id == we->table_id) {\n> > -            return we;\n> > -        }\n> > -    }\n> > -    return NULL;\n> > -}\n> > -\n> > -static void\n> > -route_table_change(const void *change_, void *aux OVS_UNUSED)\n> > -{\n> > -    if (!change_) {\n> > -        return;\n> > -    }\n> > -\n> > -    /* We currently track whether at least one recent route table change\n> > -     * was detected.  If that's the case already there's no need to\n> > -     * continue. */\n> > -    struct route_table_msg *change =\n> > -        CONST_CAST(struct route_table_msg *, change_);\n> > -    if (!any_route_table_changed && change->rd.rtm_protocol !=\n> RTPROT_OVN) {\n> > -        if (find_watch_entry(change->rd.rta_table_id)) {\n> > -            any_route_table_changed = true;\n> > -        }\n> > -    }\n> > -\n> > -    route_data_destroy(&change->rd);\n> > -}\n> > -\n> > -static void\n> > -add_watch_entry(uint32_t table_id)\n> > -{\n> > -   VLOG_INFO(\"Registering new route table watcher for table %d.\",\n> > -             table_id);\n> > -\n> > -    struct route_table_watch_entry *we;\n> > -    uint32_t hash = route_table_notify_hash_watch(table_id);\n> > -    we = xzalloc(sizeof *we);\n> > -    we->table_id = table_id;\n> > -    hmap_insert(&watches, &we->node, hash);\n> > -\n> > -    if (!nl_route_handle) {\n> > -        route_table_register_notifiers();\n> > -    }\n> > -}\n> > -\n> > -static void\n> > -remove_watch_entry(struct route_table_watch_entry *we)\n> > -{\n> > -    VLOG_INFO(\"Removing route table watcher for table %d.\",\n> we->table_id);\n> > -    hmap_remove(&watches, &we->node);\n> > -    free(we);\n> > -\n> > -    if (hmap_is_empty(&watches)) {\n> > -        route_table_deregister_notifiers();\n> > -    }\n> > -}\n> > -\n> > -bool\n> > -route_table_notify_run(void)\n> > -{\n> > -    any_route_table_changed = false;\n> > -\n> > -    if (nl_route_handle) {\n> > -        nln_run(nl_route_handle);\n> > -    }\n> > -\n> > -    return any_route_table_changed;\n> > -}\n> > -\n> > -void\n> > -route_table_notify_wait(void)\n> > -{\n> > -    if (nl_route_handle) {\n> > -        nln_wait(nl_route_handle);\n> > -    }\n> > -}\n> > -\n> > -void\n> > -route_table_notify_update_watches(const struct hmap\n> *route_table_watches)\n> > -{\n> > -    struct hmapx sync_watches = HMAPX_INITIALIZER(&sync_watches);\n> > -    struct route_table_watch_entry *we;\n> > -    HMAP_FOR_EACH (we, node, &watches) {\n> > -        hmapx_add(&sync_watches, we);\n> > -    }\n> > -\n> > -    struct route_table_watch_request *wr;\n> > -    HMAP_FOR_EACH (wr, node, route_table_watches) {\n> > -        we = find_watch_entry(wr->table_id);\n> > -        if (we) {\n> > -            hmapx_find_and_delete(&sync_watches, we);\n> > -        } else {\n> > -            add_watch_entry(wr->table_id);\n> > -        }\n> > -    }\n> > -\n> > -    struct hmapx_node *node;\n> > -    HMAPX_FOR_EACH (node, &sync_watches) {\n> > -        remove_watch_entry(node->data);\n> > -    }\n> > -\n> > -    hmapx_destroy(&sync_watches);\n> > -}\n> > -\n> > -void\n> > -route_table_notify_destroy(void)\n> > -{\n> > -    struct route_table_watch_entry *we;\n> > -    HMAP_FOR_EACH_SAFE (we, node, &watches) {\n> > -        remove_watch_entry(we);\n> > -    }\n> > -}\n> > diff --git a/controller/route-table-notify.h\n> b/controller/route-table-notify.h\n> > deleted file mode 100644\n> > index a2bc05a49..000000000\n> > --- a/controller/route-table-notify.h\n> > +++ /dev/null\n> > @@ -1,44 +0,0 @@\n> > -/*\n> > - * Copyright (c) 2025, STACKIT GmbH & Co. KG\n> > - *\n> > - * Licensed under the Apache License, Version 2.0 (the \"License\");\n> > - * you may not use this file except in compliance with the License.\n> > - * You may obtain a copy of the License at:\n> > - *\n> > - *     http://www.apache.org/licenses/LICENSE-2.0\n> > - *\n> > - * Unless required by applicable law or agreed to in writing, software\n> > - * distributed under the License is distributed on an \"AS IS\" BASIS,\n> > - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n> implied.\n> > - * See the License for the specific language governing permissions and\n> > - * limitations under the License.\n> > - */\n> > -\n> > -#ifndef ROUTE_TABLE_NOTIFY_H\n> > -#define ROUTE_TABLE_NOTIFY_H 1\n> > -\n> > -#include <stdbool.h>\n> > -#include \"openvswitch/hmap.h\"\n> > -\n> > -/* Returns true if any route table has changed enough that we need\n> > - * to learn new routes. */\n> > -bool route_table_notify_run(void);\n> > -void route_table_notify_wait(void);\n> > -\n> > -/* Add a watch request to the hmap. The hmap should later be passed to\n> > - * route_table_notify_update_watches*/\n> > -void route_table_add_watch_request(struct hmap *route_table_watches,\n> > -                                   uint32_t table_id);\n> > -\n> > -/* Cleanup all watch request in the provided hmap that where added using\n> > - * route_table_add_watch_request. */\n> > -void route_table_watch_request_cleanup(struct hmap\n> *route_table_watches);\n> > -\n> > -/* Updates the list of route table watches that are currently active.\n> > - * hmap should contain struct route_table_watch_request */\n> > -void route_table_notify_update_watches(const struct hmap\n> *route_table_watches);\n> > -\n> > -/* Cleans up all route table watches. */\n> > -void route_table_notify_destroy(void);\n> > -\n> > -#endif /* ROUTE_TABLE_NOTIFY_H */\n> > diff --git a/tests/automake.mk b/tests/automake.mk\n> > index 2dfc0bfa7..75a4b00d7 100644\n> > --- a/tests/automake.mk\n> > +++ b/tests/automake.mk\n> > @@ -303,10 +303,10 @@ tests_ovstest_SOURCES += \\\n> >       controller/host-if-monitor.h \\\n> >       controller/neighbor-exchange-netlink.c \\\n> >       controller/neighbor-exchange-netlink.h \\\n> > -     controller/neighbor-table-notify.c \\\n> > -     controller/neighbor-table-notify.h \\\n> >       controller/neighbor.c \\\n> >       controller/neighbor.h \\\n> > +     controller/ovn-netlink-notifier.c \\\n> > +     controller/ovn-netlink-notifier.h \\\n> >       controller/route-exchange-netlink.c \\\n> >       controller/route-exchange-netlink.h \\\n> >       tests/test-ovn-netlink.c\n> > diff --git a/tests/system-ovn-netlink.at b/tests/system-ovn-netlink.at\n> > index 4e581aa74..8bf1055d1 100644\n> > --- a/tests/system-ovn-netlink.at\n> > +++ b/tests/system-ovn-netlink.at\n> > @@ -229,6 +229,7 @@ on_exit 'ip link del br-test'\n> >  check ip link set br-test address 00:00:00:00:00:01\n> >  check ip address add dev br-test 10.10.10.1/24\n> >  check ip link set dev br-test up\n> > +br_if_index=$(netlink_if_index br-test)\n> >\n> >  check ip link add lo-test type dummy\n> >  on_exit 'ip link del lo-test'\n> > @@ -237,43 +238,47 @@ check ip link set lo-test address 00:00:00:00:00:02\n> >  check ip link set dev lo-test up\n> >  lo_if_index=$(netlink_if_index lo-test)\n> >\n> > -check ip link add br-test-unused type bridge\n> > -on_exit 'ip link del br-test-unused'\n> > -check ip link set br-test-unused address 00:00:00:00:00:03\n> > -check ip address add dev br-test-unused 20.20.20.1/24\n> > -check ip link set dev br-test-unused up\n> > -\n> > -check ip link add lo-test-unused type dummy\n> > -on_exit 'ip link del lo-test-unused'\n> > -check ip link set lo-test-unused master br-test-unused\n> > -check ip link set lo-test-unused address 00:00:00:00:00:04\n> > -check ip link set dev lo-test-unused up\n> > -\n> >  dnl Should notify if an entry is added to a bridge port monitored by\n> OVN.\n> > -check ovstest test-ovn-netlink neighbor-table-notify lo-test\n> $lo_if_index \\\n> > -    'bridge fdb add 00:00:00:00:00:05 dev lo-test' \\\n> > -    true\n> > +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> > +    'bridge fdb add 00:00:00:00:00:05 dev lo-test'], [0], [dnl\n> > +Add neighbor ifindex=$lo_if_index vlan=0 eth=00:00:00:00:00:05 dst=::\n> port=0\n> > +])\n> > +\n> > +dnl Should notify if an entry is removed from a bridge port monitored\n> by OVN.\n> > +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> > +    'bridge fdb del 00:00:00:00:00:05 dev lo-test'], [0], [dnl\n> > +Delete neighbor ifindex=$lo_if_index vlan=0 eth=00:00:00:00:00:05\n> dst=:: port=0\n> > +])\n> >\n> > -dnl Should NOT notify if an entry is added to a bridge port that's not\n> > +dnl Should NOT notify if an static entry is added to a bridge port\n> >  dnl monitored by OVN.\n> > -check ovstest test-ovn-netlink neighbor-table-notify lo-test\n> $lo_if_index \\\n> > -    'bridge fdb add 00:00:00:00:00:05 dev lo-test-unused' \\\n> > -    false\n> > +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> > +    'bridge fdb add 00:00:00:00:00:06 dev lo-test master static'], [0],\n> [dnl\n> > +])\n> >\n> > -br_if_index=$(netlink_if_index br-test)\n> >  dnl Should notify if an entry is added to a bridge that's monitored by\n> >  dnl OVN.\n> > -check ovstest test-ovn-netlink neighbor-table-notify br-test\n> $br_if_index \\\n> > +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> >      'ip neigh add 10.10.10.10 lladdr 00:00:00:00:10:00 \\\n> > -        dev br-test extern_learn' \\\n> > -    true\n> > +        dev br-test extern_learn'], [0], [dnl\n> > +Add neighbor ifindex=$br_if_index vlan=0 eth=00:00:00:00:10:00\n> dst=10.10.10.10 port=0\n> > +])\n> >\n> > -dnl Should NOT notify if an entry is added to a bridge that's not\n> monitored by\n> > +dnl Should notify if an entry is removed from a bridge that's monitored\n> by\n> >  dnl OVN.\n> > -check ovstest test-ovn-netlink neighbor-table-notify br-test\n> $br_if_index \\\n> > +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> > +    'ip neigh del 10.10.10.10 lladdr 00:00:00:00:10:00 \\\n> > +        dev br-test' | sort], [0], [dnl\n> > +Add neighbor ifindex=$br_if_index vlan=0 eth=00:00:00:00:00:00\n> dst=10.10.10.10 port=0\n> > +Delete neighbor ifindex=$br_if_index vlan=0 eth=00:00:00:00:00:00\n> dst=10.10.10.10 port=0\n> > +])\n> > +\n> > +dnl Should NOT notify if an noarp entry is added to a bridge port\n> > +dnl monitored by OVN.\n> > +AT_CHECK_UNQUOTED([ovstest test-ovn-netlink neighbor-table-notify \\\n> >      'ip neigh add 20.20.20.20 lladdr 00:00:00:00:20:00 \\\n> > -        dev br-test-unused extern_learn' \\\n> > -    false\n> > +        dev br-test nud noarp'], [0], [dnl\n> > +])\n> >  AT_CLEANUP\n> >\n> >  AT_SETUP([netlink - host-if-monitor])\n> > diff --git a/tests/test-ovn-netlink.c b/tests/test-ovn-netlink.c\n> > index 6e9b46d04..efc3c9e5e 100644\n> > --- a/tests/test-ovn-netlink.c\n> > +++ b/tests/test-ovn-netlink.c\n> > @@ -23,7 +23,7 @@\n> >\n> >  #include \"controller/host-if-monitor.h\"\n> >  #include \"controller/neighbor-exchange-netlink.h\"\n> > -#include \"controller/neighbor-table-notify.h\"\n> > +#include \"controller/ovn-netlink-notifier.h\"\n> >  #include \"controller/neighbor.h\"\n> >  #include \"controller/route.h\"\n> >  #include \"controller/route-exchange-netlink.h\"\n> > @@ -109,41 +109,48 @@ done:\n> >  }\n> >\n> >  static void\n> > -test_neighbor_table_notify(struct ovs_cmdl_context *ctx)\n> > +run_command_under_notifier(const char *cmd)\n> >  {\n> > -    unsigned int shift = 1;\n> > +    ovn_netlink_notifiers_run();\n> > +    ovn_netlink_notifiers_wait();\n> >\n> > -    const char *if_name = test_read_value(ctx, shift++, \"if_name\");\n> > -    if (!if_name) {\n> > -        return;\n> > +    int rc = system(cmd);\n> > +    if (rc) {\n> > +        exit(rc);\n> >      }\n> >\n> > -    unsigned int if_index;\n> > -    if (!test_read_uint_value(ctx, shift++, \"if_index\", &if_index)) {\n> > -        return;\n> > -    }\n> > +    ovn_netlink_notifiers_run();\n> > +}\n> > +\n> > +static void\n> > +test_neighbor_table_notify(struct ovs_cmdl_context *ctx)\n> > +{\n> > +    unsigned int shift = 1;\n> >\n> >      const char *cmd = test_read_value(ctx, shift++, \"shell_command\");\n> >      if (!cmd) {\n> >          return;\n> >      }\n> >\n> > -    const char *notify = test_read_value(ctx, shift++, \"should_notify\");\n> > -    bool expect_notify = notify && !strcmp(notify, \"true\");\n> > -\n> > -    struct hmap table_watches = HMAP_INITIALIZER(&table_watches);\n> > -    neighbor_table_add_watch_request(&table_watches, if_index, if_name);\n> > -    neighbor_table_notify_update_watches(&table_watches);\n> > +    ovn_netlink_update_notifier(OVN_NL_NOTIFIER_NEIGHBOR, true);\n> > +    run_command_under_notifier(cmd);\n> >\n> > -    neighbor_table_notify_run();\n> > -    neighbor_table_notify_wait();\n> > +    struct vector *msgs =\n> ovn_netlink_get_msgs(OVN_NL_NOTIFIER_NEIGHBOR);\n> >\n> > -    int rc = system(cmd);\n> > -    if (rc) {\n> > -        exit(rc);\n> > +    struct ne_table_msg *msg;\n> > +    VECTOR_FOR_EACH_PTR (msgs, msg) {\n> > +        char addr_s[INET6_ADDRSTRLEN + 1];\n> > +        printf(\"%s neighbor ifindex=%\"PRId32\" vlan=%\"PRIu16\" \"\n> > +               \"eth=\" ETH_ADDR_FMT \" dst=%s port=%\"PRIu16\"\\n\",\n> > +               msg->nlmsg_type == RTM_NEWNEIGH ? \"Add\" : \"Delete\",\n> > +               msg->nd.if_index, msg->nd.vlan,\n> ETH_ADDR_ARGS(msg->nd.lladdr),\n> > +               ipv6_string_mapped(addr_s, &msg->nd.addr)\n> > +                   ? addr_s\n> > +                   : \"(invalid)\",\n> > +               msg->nd.port);\n> >      }\n> > -    ovs_assert(neighbor_table_notify_run() == expect_notify);\n> > -    neighbor_table_watch_request_cleanup(&table_watches);\n> > +\n> > +    ovn_netlink_notifiers_destroy();\n> >  }\n> >\n> >  static void\n> > @@ -249,7 +256,7 @@ test_ovn_netlink(int argc, char *argv[])\n> >      set_program_name(argv[0]);\n> >      static const struct ovs_cmdl_command commands[] = {\n> >          {\"neighbor-sync\", NULL, 2, INT_MAX, test_neighbor_sync, OVS_RO},\n> > -        {\"neighbor-table-notify\", NULL, 3, 4,\n> > +        {\"neighbor-table-notify\", NULL, 1, 1,\n> >           test_neighbor_table_notify, OVS_RO},\n> >          {\"host-if-monitor\", NULL, 2, 3, test_host_if_monitor, OVS_RO},\n> >          {\"route-sync\", NULL, 1, INT_MAX, test_route_sync, OVS_RO},\n> > --\n> > 2.53.0\n> >\n> > _______________________________________________\n> > dev mailing list\n> > dev@openvswitch.org\n> > https://mail.openvswitch.org/mailman/listinfo/ovs-dev\n> >\n>\n\nThank you Dumitru and Lorenzo,\n\nI have addressed the nit with 2 others I noticed in the meantime. With that\napplied to main.\n\nRegards,\nAles","headers":{"Return-Path":"<ovs-dev-bounces@openvswitch.org>","X-Original-To":["incoming@patchwork.ozlabs.org","dev@openvswitch.org"],"Delivered-To":["patchwork-incoming@legolas.ozlabs.org","ovs-dev@lists.linuxfoundation.org"],"Authentication-Results":["legolas.ozlabs.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=g3VteP0o;\n\tdkim-atps=neutral","legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org\n (client-ip=2605:bc80:3010::138; helo=smtp1.osuosl.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org)","smtp1.osuosl.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key)\n header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=g3VteP0o","smtp1.osuosl.org; dmarc=pass (p=quarantine dis=none)\n header.from=redhat.com"],"Received":["from smtp1.osuosl.org (smtp1.osuosl.org [IPv6:2605:bc80:3010::138])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fvSGB2jM8z1yDG\n\tfor <incoming@patchwork.ozlabs.org>; Mon, 13 Apr 2026 23:01:22 +1000 (AEST)","from localhost (localhost [127.0.0.1])\n\tby smtp1.osuosl.org (Postfix) with ESMTP id C534884735;\n\tMon, 13 Apr 2026 13:01:20 +0000 (UTC)","from smtp1.osuosl.org ([127.0.0.1])\n by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id GKjCUTRbpYpM; Mon, 13 Apr 2026 13:01:14 +0000 (UTC)","from lists.linuxfoundation.org (lf-lists.osuosl.org\n [IPv6:2605:bc80:3010:104::8cd3:938])\n\tby smtp1.osuosl.org (Postfix) with ESMTPS id 2B18884752;\n\tMon, 13 Apr 2026 13:01:14 +0000 (UTC)","from lf-lists.osuosl.org (localhost [127.0.0.1])\n\tby lists.linuxfoundation.org (Postfix) with ESMTP id 05F0CC0902;\n\tMon, 13 Apr 2026 13:01:14 +0000 (UTC)","from smtp1.osuosl.org (smtp1.osuosl.org [140.211.166.138])\n by lists.linuxfoundation.org (Postfix) with ESMTP id 11F40C0903\n for <dev@openvswitch.org>; Mon, 13 Apr 2026 13:01:13 +0000 (UTC)","from localhost (localhost [127.0.0.1])\n by smtp1.osuosl.org (Postfix) with ESMTP id 7F7E6846F3\n for <dev@openvswitch.org>; Mon, 13 Apr 2026 13:00:57 +0000 (UTC)","from smtp1.osuosl.org ([127.0.0.1])\n by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id 6tHHFqC02iDR for <dev@openvswitch.org>;\n Mon, 13 Apr 2026 13:00:53 +0000 (UTC)","from us-smtp-delivery-124.mimecast.com\n (us-smtp-delivery-124.mimecast.com [170.10.129.124])\n by smtp1.osuosl.org (Postfix) with ESMTPS id CA0C5846DA\n for <dev@openvswitch.org>; Mon, 13 Apr 2026 13:00:52 +0000 (UTC)","from mail-yw1-f197.google.com (mail-yw1-f197.google.com\n [209.85.128.197]) by relay.mimecast.com with ESMTP with STARTTLS\n (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id\n us-mta-619-PcHiEdzTNvm6utyBvsNyLA-1; Mon, 13 Apr 2026 09:00:45 -0400","by mail-yw1-f197.google.com with SMTP id\n 00721157ae682-79064868702so94669777b3.3\n for <dev@openvswitch.org>; Mon, 13 Apr 2026 06:00:45 -0700 (PDT)"],"X-Virus-Scanned":["amavis at osuosl.org","amavis at osuosl.org"],"X-Comment":"SPF check N/A for local connections -\n client-ip=2605:bc80:3010:104::8cd3:938; helo=lists.linuxfoundation.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=<UNKNOWN> ","DKIM-Filter":["OpenDKIM Filter v2.11.0 smtp1.osuosl.org 2B18884752","OpenDKIM Filter v2.11.0 smtp1.osuosl.org CA0C5846DA"],"Received-SPF":"Pass (mailfrom) identity=mailfrom; client-ip=170.10.129.124;\n helo=us-smtp-delivery-124.mimecast.com; envelope-from=amusil@redhat.com;\n receiver=<UNKNOWN>","DMARC-Filter":"OpenDMARC Filter v1.4.2 smtp1.osuosl.org CA0C5846DA","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1776085251;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:cc:mime-version:mime-version:content-type:content-type:\n in-reply-to:in-reply-to:references:references;\n bh=8D91HznvSIK4sHBdMcFFK9yX+Y63ZbTxJgBYAibH5TE=;\n b=g3VteP0oTVWl03ziV7winxUbl3dKplYpd8+6mB4ahBfIBVCu+9lWFp9h5Dr2Hlicwfooso\n joBAnsIyu8zA657S+siZ2/1+46DVAwylrYCpaPCE45W0DbTkGTO3onlLlaK+SKA6dpjO59\n +aovESTs/JEKfRdDPUWDlCrfWF8X0ps=","X-MC-Unique":"PcHiEdzTNvm6utyBvsNyLA-1","X-Mimecast-MFC-AGG-ID":"PcHiEdzTNvm6utyBvsNyLA_1776085245","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1776085245; x=1776690045;\n h=cc:to:subject:message-id:date:from:in-reply-to:references\n :mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date\n :message-id:reply-to;\n bh=8D91HznvSIK4sHBdMcFFK9yX+Y63ZbTxJgBYAibH5TE=;\n b=jiSfWtHPUP0LAbEq5lqyWnn+mEbI/hPH+rsq285VVchMHIYW3CdXO/UdEioMf7VayK\n etYmTAtrJuuChOulvOTdEEgFoGWKa9AkSqVGKCMjyHOM9aQjgV1HMgZ1wBthQCgPXrpl\n uqKqPKxkSZz4slcjF3B8idHAJt0besri9CTX4HohsZZhq5rJM7Ims0KGnhDGNfIV8yRh\n o6o1u3PM4QticY4EVyqVfkPJZpOCZ2XWEilZYQx/aSweuTDubwvQQ7aTemDu0cenDMr/\n wOPFZGRfxb2qA/rdVOEB/XZe7NRI4XR5CLeKJY+bQl9x0KoUCSTKi6/zWM2Qlzr6kUE5\n fOWQ==","X-Gm-Message-State":"AOJu0Yzs2yqZ2naCULpFsh+fyO0lOkd3e/tImsuA+5I969PG36pqVi67\n DAovzP1hm5QNRJZWRa5qcuGRl+qVEF1bKJrtzHNjsbsrwc7Yv4Y6ox5IM9r2UfZ9EUrmH38IxEo\n 5IzK9Xkv2z7oqdabO0UVOkZtBatF3rhiIgT17P2vcqzTKjSKtSupetuqj9ZCOwuJDBpPyOQtGg9\n MzxplKB9I+lXirjMSCClXYOWNFSBs0","X-Gm-Gg":"AeBDiesRmuegvKva5pxdrWhbWSWSy9PEHcyeHo/EBBp4f321iS9H1+zP4XkFsxU73ik\n 5qnQZvExc6HYQd+ojLnZEn5DBqThw58C/caXc1HFV96IpxWGxb135SvKpMj1JnYTGyABV3XBIfq\n kuN6D4KF/kWga8gqjzgso6mU4dMraoKQ3m0c5EVTxaT1LArnPFNpCzVBk2hnLH8JNR7vaFSc/SZ\n DBRw/tTUtchl/TXkPZrn2KQiK9h9sgld0Hvu9ablrEW+8Y7VNNCO7uy5Sjsv5FmPURT6N2ot867\n zpbR6f4BqKrJfOLMncRwufbBsGYGDWjGXdDWilICoe8dhraMAmzJAOz8sGvxaNo4nNEXkGYMdDj\n dTcAIFuRPVSTu6dr1wE6itJQOS4YBzDE+wBYxLUr7E6sJIKQ=","X-Received":["by 2002:a05:690c:6308:b0:7ae:904c:11e2 with SMTP id\n 00721157ae682-7af702dbba8mr147531217b3.24.1776085242906;\n Mon, 13 Apr 2026 06:00:42 -0700 (PDT)","by 2002:a05:690c:6308:b0:7ae:904c:11e2 with SMTP id\n 00721157ae682-7af702dbba8mr147529877b3.24.1776085241634; Mon, 13 Apr 2026\n 06:00:41 -0700 (PDT)"],"MIME-Version":"1.0","References":"<20260402082510.1417440-1-amusil@redhat.com>\n <20260402082510.1417440-5-amusil@redhat.com>\n <adkpP1n5DQX4QAis@lore-desk>","In-Reply-To":"<adkpP1n5DQX4QAis@lore-desk>","Date":"Mon, 13 Apr 2026 15:00:29 +0200","X-Gm-Features":"AQROBzCucnip7Fgv7FBpMBARG4paCv7v71unHLDKe6DR6Sypp4EbeRORzeTa5io","Message-ID":"\n <CALVEqe5htbf2a+t35BGCgJVEC7e+gShrBkffz8K2C+EsyeQD8A@mail.gmail.com>","To":"Lorenzo Bianconi <lorenzo.bianconi@redhat.com>","Cc":"dev@openvswitch.org, dceara@redhat.com","X-Mimecast-Spam-Score":"0","X-Mimecast-MFC-PROC-ID":"poNqu2NLLMWcrBqXKW-5VsqTAsbZElw1pB2EdwKhkww_1776085245","X-Mimecast-Originator":"redhat.com","X-Content-Filtered-By":"Mailman/MimeDel 2.1.30","Subject":"Re: [ovs-dev] [PATCH ovn 4/6] controller: Consolidate the netlink\n notifiers.","X-BeenThere":"ovs-dev@openvswitch.org","X-Mailman-Version":"2.1.30","Precedence":"list","List-Id":"<ovs-dev.openvswitch.org>","List-Unsubscribe":"<https://mail.openvswitch.org/mailman/options/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=unsubscribe>","List-Archive":"<http://mail.openvswitch.org/pipermail/ovs-dev/>","List-Post":"<mailto:ovs-dev@openvswitch.org>","List-Help":"<mailto:ovs-dev-request@openvswitch.org?subject=help>","List-Subscribe":"<https://mail.openvswitch.org/mailman/listinfo/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=subscribe>","From":"Ales Musil via dev <ovs-dev@openvswitch.org>","Reply-To":"Ales Musil <amusil@redhat.com>","Content-Type":"text/plain; charset=\"utf-8\"","Content-Transfer-Encoding":"base64","Errors-To":"ovs-dev-bounces@openvswitch.org","Sender":"\"dev\" <ovs-dev-bounces@openvswitch.org>"}}]