From 57558508bf8e90a10ecbbcbbb458de7464933b47 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 11 Aug 2026 08:53:44 +0800 Subject: [PATCH] perf: batch update/delete marker-def fan-out too (full project-level) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit per-def operations are project-level — create, update AND delete all modify the marker policy on every link — so all three must batch, not just create. Extend memory_only to update_marker and stop_marker (merge/delete into _markers + refresh _link_data, no per-link HTTP), and route update_marker_definition and delete_marker_definition through the same two-phase path as create: memory-only per link, then one PUT /projects/{id}/nios/batch per compute. Trade-off: a full reapply resets every marker's pcap on the link (the old precise update_marker/stop_marker preserved sibling pcaps). For project-level policy changes this is acceptable — real-time insight matters more than pcap continuity, and batching turns 5000+ round-trips into one per compute. --- .gitignore | 1 + gns3server/controller/project.py | 45 ++++++++++++++++--------------- gns3server/controller/udp_link.py | 17 ++++++++++-- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 5d4619d10..7677a0718 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,4 @@ venv gns3server/agent/gns3_copilot/cache/tiktoken/ gns3.log +/configs/ diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 12897eae3..c525473e0 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -1158,23 +1158,25 @@ class Project: # data_link_type decides which links host an inherited copy (serial # links are skipped unless a WAN encapsulation is chosen), so a change # needs a full re-fan-out: drop every copy, then re-apply. - await self._marker_apply_concurrently( - affected, - lambda link: link.stop_marker(f"global-{name}", inherited=True, dump=False), - lambda link, e: f"Failed to remove inherited marker global-{name} from link {link.id}: {e}", - ) + for link in affected: + try: + await link.stop_marker(f"global-{name}", inherited=True, dump=False, memory_only=True) + except ControllerError as e: + log.warning("Failed to remove inherited marker global-%s from link %s: %s", name, link.id, e) await self._apply_def_to_all_links(name) else: - # Sync: update every inherited copy across all links. - await self._marker_apply_concurrently( - affected, - lambda link: link.update_marker( - f"global-{name}", bpf=d["bpf"], tag=d.get("tag"), direction=d.get("direction"), - color=d.get("color"), highlight_duration=d.get("highlight_duration"), inherited=True, - dump=False - ), - lambda link, e: f"Failed to sync marker global-{name} on link {link.id}: {e}", - ) + # Sync: update every inherited copy across all links in memory, then + # batch-push to computes (one PUT /nios/batch per compute). + for link in affected: + try: + await link.update_marker( + f"global-{name}", bpf=d["bpf"], tag=d.get("tag"), direction=d.get("direction"), + color=d.get("color"), highlight_duration=d.get("highlight_duration"), inherited=True, + dump=False, memory_only=True + ) + except ControllerError as e: + log.warning("Failed to sync marker global-%s on link %s: %s", name, link.id, e) + await self._batch_update_link_nios(affected) self.dump() self.emit_notification("project.updated", self.asdict()) @@ -1195,12 +1197,13 @@ class Project: if f"global-{name}" in link.markers and link.markers[f"global-{name}"].get("inherited_from") == name ] - await self._marker_apply_concurrently( - affected, - lambda link: link.stop_marker(f"global-{name}", inherited=True), - # A missing compute or broken link shouldn't block the delete. - lambda link, e: f"Failed to remove inherited marker global-{name} from link {link.id}: {e}", - ) + for link in affected: + try: + await link.stop_marker(f"global-{name}", inherited=True, memory_only=True) + except ControllerError as e: + # A missing compute or broken link shouldn't block the delete. + log.warning("Failed to remove inherited marker global-%s from link %s: %s", name, link.id, e) + await self._batch_update_link_nios(affected) self.dump() self.emit_notification("project.updated", self.asdict()) diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index 4b0bec4ec..86f863010 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -497,7 +497,7 @@ class UDPLink(Link): self._link_data[1]["markers"] = node2_markers self._link_data[1]["suspend"] = self._suspended - async def stop_marker(self, name, inherited=False, dump=True): + async def stop_marker(self, name, inherited=False, dump=True, memory_only=False): """ Remove a traffic-insight marker from this link. @@ -522,6 +522,12 @@ class UDPLink(Link): capture_node_id = self._markers[name].get("capture_node_id") del self._markers[name] + if memory_only: + # Project-level def-delete fan-out: marker is gone from _markers; + # refresh _link_data so the batch dispatch drops it from uBridge + # via full reapply. No per-link delete round-trip, notification or dump. + self._refresh_link_data() + return # Remove the marker filter + its pcap on the capture node directly — NOT a # full NIO reapply (which would reset_packet_filters and close/reopen every # sibling marker's pcap). delete_packet_filter removes just this filter; @@ -541,7 +547,7 @@ class UDPLink(Link): if dump: self._project.dump() - async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=_UNSET, color=None, highlight_duration=None, inherited=False, dump=True): + async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=_UNSET, color=None, highlight_duration=None, inherited=False, dump=True, memory_only=False): """ Update an existing marker's fields and push to uBridge fine-grained — no full NIO reapply, so sibling markers' pcaps stay open. bpf/tag/direction @@ -590,6 +596,13 @@ class UDPLink(Link): if direction is not _UNSET: marker_info["direction"] = direction # None = clear back to both directions + if memory_only: + # Project-level def sync fan-out: state is already merged into + # _markers; just refresh _link_data so the batch dispatch carries + # it. No per-link uBridge rebuild, notification or dump. + self._refresh_link_data() + return + # Push to uBridge fine-grained — NO full NIO reapply (which would # reset_packet_filters and close/reopen every sibling marker's pcap): # * bpf/tag/direction changed → rebuild just this filter (delete + add),