Commit 793bf128 authored by Vitaly Lipatov's avatar Vitaly Lipatov

Pre-aggregate resolved lists with mapcidr for instant API export

route-update.sh: after writing resolved, run mapcidr -a and -aa in background to generate resolved.agg1 and resolved.agg2 route-web-api.py: read pre-aggregated files from cache instead of calling mapcidr on each request. Instant response for aggregate=1|2. Co-Authored-By: 's avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
parent 4816b6b3
......@@ -811,6 +811,15 @@ for line in sys.stdin:
mv "$_resolved_new" "$STATE_DIR/$_state/resolved"
chmod g+r "$STATE_DIR/$_state/resolved" 2>/dev/null
# Pre-aggregate for fast API export (background, non-blocking)
if command -v mapcidr >/dev/null 2>&1 ; then
(
mapcidr -a -silent < "$STATE_DIR/$_state/resolved" > "$STATE_DIR/$_state/resolved.agg1" 2>/dev/null
mapcidr -aa -silent < "$STATE_DIR/$_state/resolved" > "$STATE_DIR/$_state/resolved.agg2" 2>/dev/null
chmod g+r "$STATE_DIR/$_state/resolved.agg1" "$STATE_DIR/$_state/resolved.agg2" 2>/dev/null
) &
fi
return 0
}
......
......@@ -54,8 +54,16 @@ _export_cache_time = 0
_export_cache_lock = threading.Lock()
def _read_ip_file(path):
try:
with open(path, "r") as f:
return [l.strip() for l in f if l.strip()]
except OSError:
return []
def _load_resolved_lists():
"""Read all resolved files from .state/routes.d and routes6.d."""
"""Read resolved, resolved.agg1, resolved.agg2 from .state/."""
cache = {}
for proto, subdir in [("ipv4", "routes.d"), ("ipv6", "routes6.d")]:
cache[proto] = {}
......@@ -71,12 +79,15 @@ def _load_resolved_lists():
resolved = os.path.join(group_dir, listname, "resolved")
if not os.path.isfile(resolved):
continue
try:
with open(resolved, "r") as f:
ips = [l.strip() for l in f if l.strip()]
cache[proto][group][listname] = ips
except OSError:
pass
ips = _read_ip_file(resolved)
if not ips:
continue
entry = {"raw": ips}
for lvl in ("agg1", "agg2"):
agg_file = resolved + "." + lvl
if os.path.isfile(agg_file):
entry[lvl] = _read_ip_file(agg_file)
cache[proto][group][listname] = entry
return cache
......@@ -1461,7 +1472,7 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
for group, lists in cache.get(proto, {}).items():
if group not in groups:
groups[group] = {"ipv4": 0, "ipv6": 0, "lists": []}
total = sum(len(v) for v in lists.values())
total = sum(len(v.get("raw", [])) for v in lists.values())
groups[group][proto] = total
if proto == "ipv4":
groups[group]["lists"] = sorted(lists.keys())
......@@ -1482,7 +1493,15 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
cache = get_export_cache()
def _collect(protos_list):
def _pick_ips(entry, level):
"""Pick IPs from entry dict by aggregation level."""
if level >= 2 and "agg2" in entry:
return entry["agg2"]
if level >= 1 and "agg1" in entry:
return entry["agg1"]
return entry.get("raw", [])
def _collect(protos_list, level=0):
"""Collect IPs for given protos."""
result = {}
for p in protos_list:
......@@ -1492,15 +1511,15 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
else:
groups_iter = [(g, proto_data[g]) for g in groups_requested if g in proto_data]
for gname, lists in groups_iter:
for lname, ips in lists.items():
for lname, entry in lists.items():
if listname and lname != listname:
continue
key = f"{gname}/{lname}" if len(groups_requested) != 1 or "all" in groups_requested else lname
result.setdefault(key, []).extend(ips)
result.setdefault(key, []).extend(_pick_ips(entry, level))
return result
protos = ["ipv4", "ipv6"] if proto == "all" else [proto]
collected = _collect(protos)
collected = _collect(protos, agg_level)
if not collected:
self.send_error_json(404, f"Group '{group_param}' or list '{listname}' not found")
......@@ -1514,13 +1533,11 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
if fmt == "mikrotik" and proto == "all":
# Separate IPv4 and IPv6 blocks
col4 = _collect(["ipv4"])
col6 = _collect(["ipv6"])
col4 = _collect(["ipv4"], agg_level)
col6 = _collect(["ipv6"], agg_level)
ips4 = _flatten(col4)
ips6 = _flatten(col6)
if agg_level:
ips4 = aggregate_ips(ips4, agg_level)
ips6 = aggregate_ips(ips6, agg_level)
# IPs already pre-aggregated from cache
body = ""
if ips4:
body += format_mikrotik(ips4, mikrotik_list, ipv6=False)
......@@ -1533,8 +1550,7 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
return
all_ips = _flatten(collected)
if agg_level:
all_ips = aggregate_ips(all_ips, agg_level)
# IPs already pre-aggregated from cache
is_ipv6 = proto == "ipv6"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment