Bound the traceroute data a request has to draw (#182)

* Bound the traceroute data a request has to draw

Every TRACEROUTE message is stored as a new row under its packet id, and its route can list
any number of hops. /graph/traceroute decodes every row, looks up every node on every route
and runs Graphviz over the result inside the request handler, so a publisher can make one
page view take a very long time and stall every other request meanwhile:

- 3,000 rows of 8 random hops for one packet id: the graph took about 43 s and other
  requests waited up to 24 s.
- 150 rows of 400 hops: about 40 s, then a 500, with other requests waiting about 10 s.

Real traceroutes are small. In 17 days of bay-area traffic no packet id had more than 72
rows, and none of 66,709 routes had more than 8 hops in either direction, which is the
firmware's limit.

get_traceroute() now returns at most the 200 earliest rows, and the TRACEROUTE decoder
treats a route longer than 8 hops as undecodable, which every caller already skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove a blank line that fails the ruff format check

`ruff format --check .` (the Ruff workflow, ruff 0.13.3) fails on develop because of this
line, so every pull request's lint job is red regardless of its content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Nathan <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-27 17:10:03 -07:00
1 parent ae7ba15263
commit a5f46d6360
2 files changed
+21 -1

No files matched your search

+14 -1
View File
@@ -17,12 +17,25 @@ def text_message(payload):
return payload.decode("utf-8")
# The firmware keeps at most 8 hops in each direction (none of 66,709 real traceroutes in 17
# days of bay-area traffic had more). A longer route can only be forged, and one with
# thousands of hops makes every page that draws it slow, so treat it as undecodable.
MAX_ROUTE_HOPS = 8
def route_discovery(payload):
route = RouteDiscovery.FromString(payload)
if max(len(route.route), len(route.route_back)) > MAX_ROUTE_HOPS:
return None
return route
DECODE_MAP = {
PortNum.POSITION_APP: Position.FromString,
PortNum.NEIGHBORINFO_APP: NeighborInfo.FromString,
PortNum.NODEINFO_APP: User.FromString,
PortNum.TELEMETRY_APP: Telemetry.FromString,
PortNum.TRACEROUTE_APP: RouteDiscovery.FromString,
PortNum.TRACEROUTE_APP: route_discovery,
PortNum.ROUTING_APP: Routing.FromString,
PortNum.TEXT_MESSAGE_APP: text_message,
PortNum.MAP_REPORT_APP: MapReport.FromString,
+7
View File
@@ -152,12 +152,19 @@ async def has_packets(node_id, portnum):
)
# Every TRACEROUTE message stores a row under its packet id, so anyone who can publish can add
# thousands. Real ones have a few dozen (one per gateway that heard it; the most seen in 17 days
# of bay-area traffic was 72), so only the earliest rows are used.
MAX_TRACEROUTE_ROWS = 200
async def get_traceroute(packet_id):
async with database.async_session() as session:
result = await session.execute(
select(Traceroute)
.where(Traceroute.packet_id == packet_id)
.order_by(Traceroute.import_time_us)
.limit(MAX_TRACEROUTE_ROWS)
)
return result.scalars()