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: ...@@ -811,6 +811,15 @@ for line in sys.stdin:
mv "$_resolved_new" "$STATE_DIR/$_state/resolved" mv "$_resolved_new" "$STATE_DIR/$_state/resolved"
chmod g+r "$STATE_DIR/$_state/resolved" 2>/dev/null 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 return 0
} }
......
...@@ -54,8 +54,16 @@ _export_cache_time = 0 ...@@ -54,8 +54,16 @@ _export_cache_time = 0
_export_cache_lock = threading.Lock() _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(): def _load_resolved_lists():
"""Read all resolved files from .state/routes.d and routes6.d.""" """Read resolved, resolved.agg1, resolved.agg2 from .state/."""
cache = {} cache = {}
for proto, subdir in [("ipv4", "routes.d"), ("ipv6", "routes6.d")]: for proto, subdir in [("ipv4", "routes.d"), ("ipv6", "routes6.d")]:
cache[proto] = {} cache[proto] = {}
...@@ -71,12 +79,15 @@ def _load_resolved_lists(): ...@@ -71,12 +79,15 @@ def _load_resolved_lists():
resolved = os.path.join(group_dir, listname, "resolved") resolved = os.path.join(group_dir, listname, "resolved")
if not os.path.isfile(resolved): if not os.path.isfile(resolved):
continue continue
try: ips = _read_ip_file(resolved)
with open(resolved, "r") as f: if not ips:
ips = [l.strip() for l in f if l.strip()] continue
cache[proto][group][listname] = ips entry = {"raw": ips}
except OSError: for lvl in ("agg1", "agg2"):
pass agg_file = resolved + "." + lvl
if os.path.isfile(agg_file):
entry[lvl] = _read_ip_file(agg_file)
cache[proto][group][listname] = entry
return cache return cache
...@@ -1461,7 +1472,7 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1461,7 +1472,7 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
for group, lists in cache.get(proto, {}).items(): for group, lists in cache.get(proto, {}).items():
if group not in groups: if group not in groups:
groups[group] = {"ipv4": 0, "ipv6": 0, "lists": []} 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 groups[group][proto] = total
if proto == "ipv4": if proto == "ipv4":
groups[group]["lists"] = sorted(lists.keys()) groups[group]["lists"] = sorted(lists.keys())
...@@ -1482,7 +1493,15 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1482,7 +1493,15 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
cache = get_export_cache() 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.""" """Collect IPs for given protos."""
result = {} result = {}
for p in protos_list: for p in protos_list:
...@@ -1492,15 +1511,15 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1492,15 +1511,15 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
else: else:
groups_iter = [(g, proto_data[g]) for g in groups_requested if g in proto_data] groups_iter = [(g, proto_data[g]) for g in groups_requested if g in proto_data]
for gname, lists in groups_iter: for gname, lists in groups_iter:
for lname, ips in lists.items(): for lname, entry in lists.items():
if listname and lname != listname: if listname and lname != listname:
continue continue
key = f"{gname}/{lname}" if len(groups_requested) != 1 or "all" in groups_requested else lname 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 return result
protos = ["ipv4", "ipv6"] if proto == "all" else [proto] protos = ["ipv4", "ipv6"] if proto == "all" else [proto]
collected = _collect(protos) collected = _collect(protos, agg_level)
if not collected: if not collected:
self.send_error_json(404, f"Group '{group_param}' or list '{listname}' not found") self.send_error_json(404, f"Group '{group_param}' or list '{listname}' not found")
...@@ -1514,13 +1533,11 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1514,13 +1533,11 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
if fmt == "mikrotik" and proto == "all": if fmt == "mikrotik" and proto == "all":
# Separate IPv4 and IPv6 blocks # Separate IPv4 and IPv6 blocks
col4 = _collect(["ipv4"]) col4 = _collect(["ipv4"], agg_level)
col6 = _collect(["ipv6"]) col6 = _collect(["ipv6"], agg_level)
ips4 = _flatten(col4) ips4 = _flatten(col4)
ips6 = _flatten(col6) ips6 = _flatten(col6)
if agg_level: # IPs already pre-aggregated from cache
ips4 = aggregate_ips(ips4, agg_level)
ips6 = aggregate_ips(ips6, agg_level)
body = "" body = ""
if ips4: if ips4:
body += format_mikrotik(ips4, mikrotik_list, ipv6=False) body += format_mikrotik(ips4, mikrotik_list, ipv6=False)
...@@ -1533,8 +1550,7 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1533,8 +1550,7 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
return return
all_ips = _flatten(collected) all_ips = _flatten(collected)
if agg_level: # IPs already pre-aggregated from cache
all_ips = aggregate_ips(all_ips, agg_level)
is_ipv6 = proto == "ipv6" 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