Skip to content

Make a COTTAS query killable, and make a timeout report as one - #16

Merged
ecrum19 merged 3 commits into
mainfrom
fix/cottas-query-timeout
Sep 22, 2026
Merged

ecrum19 merged 3 commits into
mainfrom
fix/cottas-query-timeout

Conversation

@ecrum19

@ecrum19 ecrum19 commented Sep 18, 2026

Copy link
Copy Markdown
Owner

What was wrong

06_equivalence hung twice on the same query — 41 hours, then 10 hours with --validation-query-timeout 1800 and --validation-time-budget 14400 on the command line. Two independent causes, either of which alone would have been enough:

1. The query was not interruptible. CottasEngine queried in-process through rdflib.Graph(store=pycottas.COTTASStore(...)). The call sits inside pycottas, so no timeout can reach it, and the time budget is checked between queries so it never gets a turn either.

2. Nothing ever produced exit 124. That is the code the query loop keys on (executions[query_id].get("exitCode") == 124). Both HTTP engines caught every exception and returned 1, making a timeout indistinguishable from a crash — so --stop-after-query-timeout was unreachable in any configuration, and timeouts were reported as EXECUTION_FAILED. Meanwhile run_query_process, which kills the whole process group and returns 124, was defined in the module with no callers at all.

The measurement that chose the design

I was about to build a long-lived worker subprocess with a query protocol, to preserve the load-once property the suite deliberately has. Measuring first, on a 39 MB artifact:

open store      0.015 s        LIMIT 1     12.719 s
re-open store   0.003 s        COUNT(*)    did not finish in 120 s

Opening is free. For COTTAS the artifact is the index — there is no load to amortise, so load-once does not apply to this engine and a process per query costs ~3 ms. The worker design would have been complexity bought with an assumption the measurement contradicts.

The same numbers say the hang is not a q05 bug: rdflib evaluates SPARQL over a custom Store by pulling triples through the Store API in Python. This fix bounds COTTAS; it does not make it fast.

Changes

  • CottasEngine.execute runs each query via run_query_process — its own process group, killed on timeout, exit 124. start() keeps the dependency check; stop() has nothing to close.
  • is_timeout_error distinguishes a genuine timeout (direct or wrapped in URLError) from a fault, and both HTTP engines now return 124 for one.

Verification

382 unit tests pass. The new ones cover the mechanism, not the report: that run_query_process returns 124 and that a forked grandchild does not outlive the group kill (the orphan case its docstring exists for), that CottasEngine no longer queries in-process, that timeouts classify correctly, and that something still produces the 124 the loop depends on.

End to end on the benchmark VM, against the exact artifact and query shape that hung:

elapsed   : 45.4 s   (bound was 45 s)
exitCode  : 124
error     : query exceeded 45s
VERDICT   : TIMEOUT ENFORCED

Note for the benchmark

With this, 06_equivalence completes and records q05 as a timeout for cottas while the other engines answer it. That is the honest cross-engine result rather than a hung run — but it means the COTTAS column will contain timeouts at this scale, which belongs in the limitations rather than being read as disagreement.

🤖 Generated with Claude Code

06_equivalence hung twice on the same query: 41 hours, then 10 hours WITH
--validation-query-timeout 1800 and --validation-time-budget 14400 on the
command line. Neither bound could fire, for two independent reasons.

CottasEngine queried in-process through rdflib, so nothing could interrupt it.
The call sits inside pycottas and no timeout reaches it; the time budget is
checked between queries, so it never gets a turn either.

And no engine ever produced exit 124, which is what the query loop keys on. The
HTTP engines caught every exception and returned 1, so a timeout was
indistinguishable from a crash, and --stop-after-query-timeout was unreachable
in any configuration. run_query_process -- which kills the whole process group
and returns 124 -- existed in the module with no callers at all.

So: each COTTAS query now runs in its own process through run_query_process, and
both HTTP engines classify a timeout as 124 via a new is_timeout_error rather
than lumping it in with genuine faults.

Measured before choosing the design, on a 39 MB artifact: opening the store
takes 3-15 ms, `LIMIT 1` takes 12.7 s, and COUNT(*) does not finish in two
minutes. Opening is free, so a process per query costs nothing and the
load-once property the other engines need does not apply here -- for COTTAS the
artifact is the index. A long-lived worker with a query protocol would have been
complexity bought with an assumption the measurement contradicts.

The numbers also say the hang is not a q05 bug: rdflib evaluates SPARQL over a
custom Store by pulling triples through the Store API in Python, so this is what
COTTAS costs at this scale. The fix bounds it; it does not make it fast.

Tests cover the mechanism rather than the report: that run_query_process returns
124 and that a forked grandchild does not outlive the group kill, that
CottasEngine no longer queries in-process, that timeouts classify correctly, and
that something in the module still produces the 124 the loop depends on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Sep 18, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 90.58824% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/validation/validation_runner.py 74.19% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

ecrum19 and others added 2 commits September 21, 2026 15:19
The pycottas path could not answer a genotype query at benchmark scale.
rdflib evaluates SPARQL over a custom Store by pulling triples through the
Store API in Python: on a 39 MB artifact, opening the store took 3-15 ms while
`SELECT ?s ?p ?o LIMIT 1` took 12.7 s and COUNT(*) did not finish in two
minutes. 06_equivalence hung for 41 hours, then for 10 more once bounded --
a timeout stops a query without making the engine able to answer it.

@elias.crum/query-sparql-cottas answers the same artifact through DuckDB over
the Parquet, and ships comunica-sparql-cottas-http. That means CottasEngine
becomes a sibling of HdtEngine rather than a special case: the same
ComunicaHttpEndpointMixin, one long-lived endpoint, the graph opened once, each
query charged only for itself, and the mixin's existing timeout handling
inherited rather than reimplemented.

Building the artifact still goes through pycottas' rdf2cottas; only querying
moved. The binary check runs before NativeArtifactEngine.start so a dead engine
does not first pay for rdf2cottas, which is the expensive step.

Indexing is SPO only for now. The engine finds sibling .posg/.ospg files by
naming convention if they exist and prunes patterns accordingly; none are built
here, so every pattern is answered from the one order. Discovery is silent, so
this is recorded in describe() rather than left to be inferred.

The image installs the package globally beside the other comunica engines, and
the build now fails if the binary will not run -- the DuckDB addon is a native
prebuild with no musl build and per-architecture artifacts, and that failure
would otherwise surface inside a benchmark cell hours later.

Verified against the published 0.1.0 inside the current image on the benchmark
host: it installs and answers a query on a real test-1k.cottas under Node
22.22.1, one patch below the version the integration guide states.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first guard ran `comunica-sparql-cottas --version`, which prints its
version table and then exits 1 -- so a working engine failed the build. The
addon cannot be probed with require() either, because @duckdb/node-api is ESM.
Both checks report a healthy engine as broken.

Build a one-triple .cottas with pycottas and query it instead. That settles the
question the guard is actually asking, and covers rdf2cottas as well, which is
the half of this engine that did not move to comunica.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ecrum19
ecrum19 merged commit 3b2b0a1 into main Sep 22, 2026
24 checks passed
@ecrum19
ecrum19 deleted the fix/cottas-query-timeout branch September 22, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants