Skip to content

Commit 2c3a671

Browse files
joocerclaude
andcommitted
Sort the responsive derivation server-side, oldest first
`$orderby` naming an aggregate alias is accepted by this feed - `$orderby=last_at` against a `last_at` the aggregate produced returns correctly ordered rows, verified against the live service. odata.py asserted the opposite, and that claim is why the sort was done client-side. Sorting here meant reading everything to find the head of a queue: 130408 responsive http hosts pulled to pick the 1101 refresh would take next, against a 100000-row page ceiling this feed cannot be paged past without silently duplicating and dropping rows. So http and https could not be derived at all. Ordering in the query turns $top from a completeness problem into a deliberate truncation of the newest end - refresh consumes ~26000 hosts a day oldest-first, so the ones it needs next are nowhere near the cut, and the list is rebuilt daily regardless. The discovery blocklist loses the most recently seen ~23% of its entries, which is worth about one re-discovered host an hour out of ~1100 found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 816f76e commit 2c3a671

3 files changed

Lines changed: 59 additions & 5 deletions

File tree

src/ichnos/odata.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def grouped_max(
208208
base_url: str = DEFAULT_ODATA_BASE,
209209
prefix: str = DEFAULT_ODATA_PREFIX,
210210
top: int = MAX_TOP,
211+
order_by: Optional[str] = None,
211212
get: Optional[Callable[..., Any]] = None,
212213
) -> List[Dict[str, Any]]:
213214
"""One row per distinct `column`, carrying `max(max_column)` as `alias`.
@@ -222,14 +223,21 @@ def grouped_max(
222223
ever succeed" and "when did we last try it") instead of issuing a second query and
223224
joining the results here. Grouping is what the feed is for.
224225
225-
Note the feed will not `$orderby` an aggregate alias, so callers sort the result
226-
themselves; at the sizes this returns that is trivial next to the transfer saved by
227-
aggregating server-side.
226+
`order_by` sorts server-side, and it may name the aggregate alias - `$orderby=last_at`
227+
against a `last_at` produced by the aggregate is accepted and correct, verified
228+
against the live feed. This module used to assert the opposite and sort client-side,
229+
which is what forced whole-result reads: to find the thousand oldest hosts it pulled
230+
all 130408 of them, and so needed a page size it did not have. Ordering here instead
231+
turns `$top` from a completeness problem into a deliberate truncation of the end of
232+
the queue nobody reaches.
228233
"""
229234
columns = [column] if isinstance(column, str) else list(column)
230235
inner = f"groupby(({','.join(columns)}),aggregate({max_column} with max as {alias}))"
231236
apply_expr = f"filter({where})/{inner}" if where else inner
232-
query = f"$apply={quote(apply_expr, safe='()/,')}&$top={top}"
237+
query = f"$apply={quote(apply_expr, safe='()/,')}"
238+
if order_by:
239+
query += f"&$orderby={quote(order_by, safe='')}"
240+
query += f"&$top={top}"
233241
return [
234242
row
235243
for row in iter_rows(path, query, token=token, base_url=base_url, prefix=prefix,

src/ichnos/responsive.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,14 @@ def fetch_responsive_hosts(
137137
dataset = f"{workspace}/{collection}/{OBSERVATIONS_DATASET}"
138138

139139
def newest_per_ip(status_clause: str) -> dict:
140-
kwargs = {"where": f"{scoped} and {status_clause}", "token": token, "get": get}
140+
# Oldest first, server-side, so a result too large for one page is truncated at
141+
# the *newest* end - the end refresh would not have reached before tomorrow's
142+
# derivation replaces the list anyway. Sorting here instead of client-side is
143+
# what makes a single page sufficient: http has 130408 responsive hosts against
144+
# a 100000-row ceiling, but refresh consumes ~26000 a day, so the thousand it
145+
# needs next are never near the truncation.
146+
kwargs = {"where": f"{scoped} and {status_clause}", "token": token, "get": get,
147+
"order_by": "last_at"}
141148
if base_url:
142149
kwargs["base_url"] = base_url
143150
rows = grouped_max(dataset, "ip", "observed_at", "last_at", **kwargs)

tests/test_responsive.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,42 @@ def test_a_single_page_aggregate_read_is_returned_normally():
314314
)
315315
assert rows == [{"ip": "203.0.113.1", "response_status": "success",
316316
"last_at": "2026-08-01T00:00:00Z"}]
317+
318+
319+
def test_the_derivation_asks_the_feed_to_sort_oldest_first():
320+
"""Sorting server-side is what makes a single page enough.
321+
322+
Sorting client-side meant the whole set had to be read to find the head of the
323+
queue: 130408 responsive http hosts against a 100000-row page ceiling that cannot
324+
be paged past without silently corrupting the result. Ordering in the query turns
325+
`$top` from a completeness problem into a truncation of the *newest* end - and
326+
refresh consumes ~26000 hosts a day oldest-first, so the ones it needs next are
327+
nowhere near the cut, and tomorrow's derivation replaces the list regardless.
328+
329+
`$orderby` naming the aggregate alias is accepted by this feed. The module asserted
330+
the opposite for a long time, which is how the client-side sort got there."""
331+
calls = []
332+
fetch_responsive_hosts(
333+
"http", workspace="ichnos", collection="landing", token="t",
334+
now=datetime(2026, 8, 4, tzinfo=timezone.utc),
335+
get=_fake_derivation([], [], calls),
336+
)
337+
for url in calls:
338+
assert "$orderby=last_at" in url
339+
assert url.index("$orderby") < url.index("$top"), "orderby must precede top"
340+
341+
342+
def test_truncation_lands_on_the_newest_hosts_not_the_oldest():
343+
"""The property that makes the truncation safe. Whatever the feed returns, the
344+
hosts refresh takes next are the least recently attempted - so a short read costs
345+
coverage at the back of the queue, never at the front."""
346+
hosts = fetch_responsive_hosts(
347+
"http", workspace="ichnos", collection="landing", token="t",
348+
now=datetime(2026, 8, 5, tzinfo=timezone.utc),
349+
get=_fake_derivation([
350+
_row("oldest", "2026-07-30T00:00:00Z"),
351+
_row("middle", "2026-08-02T00:00:00Z"),
352+
_row("newest", "2026-08-04T00:00:00Z"),
353+
]),
354+
)
355+
assert [ip for ip, _ in hosts] == ["oldest", "middle", "newest"]

0 commit comments

Comments
 (0)