Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bbot/scanner/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,14 @@ def modules_status(self, _log=False, detailed=False):
mem_percent = mem_status.percent
prev_delay = self._ingress_delay
new_delay = self._compute_ingress_delay(mem_percent)
# if no module has work in flight or queued, ingress is the only drain — don't throttle it
drain_mode = not any(
(m.running or m.outgoing_event_queue.qsize() > 0 or m.num_incoming_events > 0)
for m in self.modules.values()
if not m._intercept
)
if drain_mode:
new_delay = 0.0
self._ingress_delay = new_delay
if mem_percent > self.max_mem_percent:
free_memory_human = self.helpers.bytes_to_human(mem_status.available)
Expand Down
67 changes: 56 additions & 11 deletions bbot/test/test_step_1/test_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,18 +460,63 @@ def mock_memory_status():
# status loop wires memory_status -> _ingress_delay
assert scan._ingress_delay == 0.0

mem_percent[0] = 93.0
scan.modules_status(_log=False)
assert scan._ingress_delay == pytest.approx(3.0), "delay should engage above threshold"

mem_percent[0] = 97.0
scan.modules_status(_log=False)
assert scan._ingress_delay == pytest.approx(5.0), "delay should clamp at the cap"

mem_percent[0] = 80.0
scan.modules_status(_log=False)
assert scan._ingress_delay == 0.0, "delay should clear once memory drops back"
# the drain-mode bypass only zeros the delay when no module has work; pin a fake task so
# the engagement curve below exercises the memory-driven formula, not the bypass
target_module = next(m for m in scan.modules.values() if not m._intercept)
target_module._task_counter.tasks["fake-task"] = SimpleNamespace(n=1)
try:
mem_percent[0] = 93.0
scan.modules_status(_log=False)
assert scan._ingress_delay == pytest.approx(3.0), "delay should engage above threshold"

mem_percent[0] = 97.0
scan.modules_status(_log=False)
assert scan._ingress_delay == pytest.approx(5.0), "delay should clamp at the cap"

mem_percent[0] = 80.0
scan.modules_status(_log=False)
assert scan._ingress_delay == 0.0, "delay should clear once memory drops back"
finally:
target_module._task_counter.tasks.pop("fake-task", None)

# scan still produces events with the throttle disengaged
events = [e async for e in scan.async_start()]
assert any(e.type == "IP_ADDRESS" for e in events), "scan should still produce events"


@pytest.mark.asyncio
async def test_memory_backpressure_drain_mode_bypass(bbot_scanner, monkeypatch):
"""Throttle must clear in drain mode: with memory pinned high but no module producing,
ingress IS the drain and throttling it traps the scan in a feedback loop."""
from types import SimpleNamespace

mem_percent = [93.0]

def mock_memory_status():
return SimpleNamespace(percent=mem_percent[0], available=1_000_000_000)

scan = bbot_scanner("127.0.0.1", config={"max_mem_percent": 90})
await scan._prep()
monkeypatch.setattr("bbot.core.helpers.misc.memory_status", mock_memory_status)

non_intercept = [m for m in scan.modules.values() if not m._intercept]
assert non_intercept, "test requires at least one non-intercept module"

# simulate a healthy scan: at least one module is mid-handle
target_module = non_intercept[0]
target_module._task_counter.tasks["fake-task"] = SimpleNamespace(n=1)
try:
scan.modules_status(_log=False)
assert scan._ingress_delay == pytest.approx(3.0), (
"throttle should engage when memory is high and a module is running"
)
finally:
target_module._task_counter.tasks.pop("fake-task", None)

# now drain mode: no module running, no module has queued work
for m in non_intercept:
assert not m.running
assert m.outgoing_event_queue.qsize() == 0
assert m.num_incoming_events == 0
scan.modules_status(_log=False)
assert scan._ingress_delay == 0.0, "throttle must clear in drain mode (no producers, ingress is the drain)"
Loading