marker: rework pause/resume from project-wide to per-definition

The project-wide mute (POST /markers/pause|resume + _markers_paused) paused
every marker with one button. The actual need is per-rule control: pause one
definition and toggle only its inherited global-{name} copies across all links.

- Drop project-level: _markers_paused (init/asdict/load/start_all), the
  pause_all/resume_all_markers methods, and the /markers/pause|resume routes.
- Add per-definition: a persisted `paused` flag on each definition;
  pause/resume_marker_definition fan out update_marker(enabled) to every
  global-{name} copy — instant, via the existing enable_packet_filter toggle
  (no NIO rebuild, pcap/emitted preserved). New links inherit a paused
  definition already off (inherit_marker passes enabled=not paused).
- start_marker takes an enabled kwarg; update_marker's enabled-only short-circuit
  now also covers inherited copies so def pause/resume is instant.
- Routes: POST /marker-definitions/{name}/pause|resume.
- Docs + tests updated.
This commit is contained in:
YueGuobin committed 2026-08-02 22:46:33 +08:00
1 parent e97df86d96
commit 1eeee024bd
7 files changed
+113 -114

No files matched your search

+38 -31
View File
@@ -219,37 +219,6 @@ def get_project_markers(project: Project = Depends(dep_project)) -> dict:
return project.markers
@router.post(
"/{project_id}/markers/pause",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Project.Modify"))]
)
async def pause_project_markers(project: Project = Depends(dep_project)) -> None:
"""
Pause marker signal+pcap emission project-wide (``marker pause`` on every
marker-hosting node's uBridge; resume is instant, sink retained).
Required privilege: Project.Modify
"""
await project.pause_all_markers()
@router.post(
"/{project_id}/markers/resume",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Project.Modify"))]
)
async def resume_project_markers(project: Project = Depends(dep_project)) -> None:
"""
Resume marker signal+pcap emission project-wide.
Required privilege: Project.Modify
"""
await project.resume_all_markers()
# ---------------------------------------------------------------------------
# Project-level marker definitions (global rules inherited by every link)
# ---------------------------------------------------------------------------
@@ -332,6 +301,44 @@ async def update_marker_definition(
return project.marker_definitions.get(def_name, {})
@router.post(
"/{project_id}/marker-definitions/{def_name}/pause",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Project.Modify"))]
)
async def pause_marker_definition(
def_name: str,
project: Project = Depends(dep_project)
) -> None:
"""
Pause a definition: toggle off every inherited ``global-{def_name}`` copy
on every link (uBridge ``enable_packet_filter off``, instant — no NIO
rebuild). The definition's ``paused`` flag is persisted, so links created
later inherit it already paused.
Required privilege: Project.Modify
"""
await project.pause_marker_definition(def_name)
@router.post(
"/{project_id}/marker-definitions/{def_name}/resume",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Project.Modify"))]
)
async def resume_marker_definition(
def_name: str,
project: Project = Depends(dep_project)
) -> None:
"""Resume a paused definition (toggle on every inherited copy).
Required privilege: Project.Modify
"""
await project.resume_marker_definition(def_name)
@router.delete(
"/{project_id}/marker-definitions/{def_name}",
status_code=status.HTTP_204_NO_CONTENT,
+2 -1
View File
@@ -131,6 +131,7 @@ class Link:
direction=marker_def.get("direction"),
color=marker_def.get("color"),
highlight_duration=marker_def.get("highlight_duration"),
enabled=not marker_def.get("paused", False),
inherited_from=def_name,
)
@@ -341,7 +342,7 @@ class Link:
raise NotImplementedError
async def start_marker(self, name, bpf, tag=None, direction=None, capture_node_id=None):
async def start_marker(self, name, bpf, tag=None, direction=None, capture_node_id=None, enabled=True):
"""
Attach a traffic-insight marker to this link (base — UDPLink overrides).
"""
+23 -38
View File
@@ -214,7 +214,6 @@ class Project:
self._nodes = {}
self._links = {}
self._marker_definitions = {} # name → {bpf, tag, color, highlight_duration}
self._markers_paused = False # project-wide marker mute (persisted, see asdict)
self._drawings = {}
self._snapshots = {}
self._computes = []
@@ -925,44 +924,37 @@ class Project:
}
return result
async def pause_all_markers(self):
async def pause_marker_definition(self, name):
"""
Pause marker signal+pcap emission on every node hosting a marker
(``marker pause`` per capture node's uBridge). Node-deduplicated and
best-effort: a node hosting markers on several links is paused once,
and a node that is down or running an old compute is skipped.
Pause every inherited copy of a definition (``global-{name}``) on every
link: toggle each filter off in place via ``update_marker(enabled=False)``
— uBridge ``enable_packet_filter off``, no NIO rebuild, pcap/emitted
preserved. The definition's ``paused`` flag is persisted, so links
created later inherit the marker already paused.
"""
self._markers_paused = True
seen = set()
if name not in self._marker_definitions:
raise ControllerError(f"Marker definition '{name}' not found")
self._marker_definitions[name]["paused"] = True
marker_name = f"global-{name}"
for link in list(self._links.values()):
for info in link.markers.values():
node_id = info.get("capture_node_id")
if not node_id or node_id in seen:
continue
seen.add(node_id)
try:
await self.get_node(node_id).post("/markers/pause")
except Exception:
pass
if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name:
await link.update_marker(marker_name, enabled=False, inherited=True)
self.dump()
self.emit_notification("project.updated", self.asdict())
async def resume_all_markers(self):
"""Resume marker signal+pcap emission on every marker-hosting node."""
async def resume_marker_definition(self, name):
"""Resume every inherited copy of a definition (toggle on)."""
self._markers_paused = False
seen = set()
if name not in self._marker_definitions:
raise ControllerError(f"Marker definition '{name}' not found")
self._marker_definitions[name]["paused"] = False
marker_name = f"global-{name}"
for link in list(self._links.values()):
for info in link.markers.values():
node_id = info.get("capture_node_id")
if not node_id or node_id in seen:
continue
seen.add(node_id)
try:
await self.get_node(node_id).post("/markers/resume")
except Exception:
pass
if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name:
await link.update_marker(marker_name, enabled=True, inherited=True)
self.dump()
self.emit_notification("project.updated", self.asdict())
@property
def marker_definitions(self):
@@ -983,7 +975,7 @@ class Project:
f"Marker definition '{name}' already exists in this project"
)
self._marker_definitions[name] = {"bpf": bpf, "tag": tag, "direction": direction, "color": color, "highlight_duration": highlight_duration}
self._marker_definitions[name] = {"bpf": bpf, "tag": tag, "direction": direction, "color": color, "highlight_duration": highlight_duration, "paused": False}
await self._apply_def_to_all_links(name)
self.dump()
self.emit_notification("project.updated", self.asdict())
@@ -1477,7 +1469,6 @@ class Project:
defs = project_data.get("marker_definitions")
if isinstance(defs, dict):
self._marker_definitions = defs
self._markers_paused = bool(project_data.get("markers_paused", False))
topology = project_data["topology"]
for compute in topology.get("computes", []):
@@ -1816,11 +1807,6 @@ class Project:
if not node.is_always_running():
pool.append(node.start)
await pool.join()
# marker pause is a uBridge runtime flag that resets when a node
# restarts, so re-apply the project-wide mute to the freshly started
# uBridges (markers are installed during node start).
if self._markers_paused:
await self.pause_all_markers()
@open_required
async def stop_all(self):
@@ -1936,7 +1922,6 @@ class Project:
"variables": self._variables,
"created_by": self._created_by,
"marker_definitions": self._marker_definitions,
"markers_paused": self._markers_paused,
}
def __repr__(self):
+3 -3
View File
@@ -350,7 +350,7 @@ class UDPLink(Link):
# explicitly deletes a marker via the REST API, and a marker is torn
# down automatically only when its link is deleted.
async def start_marker(self, name, bpf, tag=None, direction=None, capture_node_id=None, color=None, highlight_duration=None, inherited_from=None):
async def start_marker(self, name, bpf, tag=None, direction=None, capture_node_id=None, color=None, highlight_duration=None, enabled=True, inherited_from=None):
"""
Attach a traffic-insight marker to this link.
@@ -390,7 +390,7 @@ class UDPLink(Link):
marker_entry = {
"bpf": bpf,
"tag": tag,
"enabled": True,
"enabled": enabled,
"color": color,
"highlight_duration": highlight_duration,
"capture_node_id": marker_side["node"].id,
@@ -472,7 +472,7 @@ class UDPLink(Link):
and color is None
and highlight_duration is None
)
if only_enabled and self._created and not marker_info.get("inherited_from"):
if only_enabled and self._created:
capture_node_id = marker_info.get("capture_node_id")
side = next((s for s in self._nodes if str(s["node"].id) == str(capture_node_id)), None)
if side is not None: