Skip to content

Native Select.timeout() is still a silent no-op on the vector k-NN path (executeVector) #6873

Closed

Description

@robfrank

Summary

Follow-up to #6815, spun out of the review on #6872.

#6815 fixed Select.timeout() for the four sources SelectExecutor.buildIterator() can return, by moving the deadline to the consumer (SelectExecutor.checkForTimeout()). The vector k-NN search is a fifth plan shape that never goes through buildIterator() at all, and it consults no deadline whatsoever.

Where

engine/src/main/java/com/arcadedb/query/select/SelectExecutor.java - executeVector() calls neither startTimeout() nor checkForTimeout().

Select.timeout() returns this and nearestTo() is declared on Select, so this compiles and silently ignores the timeout:

db.select().fromType("Product")
    .timeout(1, TimeUnit.MILLISECONDS, true)     // silently dropped
    .nearestTo("embedding", query, 25)
    .vertices();

SelectVectorBuilder.vertices()/documents() go straight to new SelectExecutor(select).executeVector(), bypassing every path #6815 touched.

Suggested fix

Arm startTimeout() at the head of executeVector() and check the deadline in its two loops: the per-index neighbour search, and the result-assembly loop (each iteration loads a record and may run the post-filter WHERE).

Note the granularity limit: a single LSMVectorIndex.findNeighborsFromVector() call is not interruptible from executeVector(), so the deadline can only be honoured between indexes and per assembled result. That is still the difference between a no-op and a bound, but it will not cut short one long search inside a single index.

Why this was not fixed in #6872

The fix itself is three lines, but I could not write a regression test for it that is stable under this repo's timing discipline, so it was pulled back out rather than shipped unverified.

executeVector() is eager - it returns a fully materialized List. The iterator-path tests in #6872 stay stable because the consumer paces the lazy iterator with its own Thread.sleep(), making the elapsed time deterministic and the assertion one-sided in the safe direction. No such hook exists here: the caller cannot slow an eager call down, so any "a 1 ms budget expired" assertion is a straight wall-clock race that a faster machine turns red.

Measured on the PR branch (warm JVM, best of 10 runs, dim 8, k == n, with a post-filter WHERE):

products best elapsed
50 0.25 ms
1,000 1.29 ms
5,000 3.67 ms

A 50-vector fixture finishes in a quarter of a 1 ms budget - a candidate test written that way passed 5/5 in isolation on a cold JVM and then failed in the warm full-suite run, which is exactly the failure mode CLAUDE.md forbids. Even 5,000 vectors only buys a ~3.7x margin, which is not enough to be safe, and paying a much larger fixture for a bigger margin is a poor trade.

So this needs either:

  • a deterministic hook that lets a test control the elapsed time inside the measured window (e.g. driving the deadline through a seam rather than the wall clock), or
  • an accepted decision that the vector timeout ships with the granularity caveat and only the load-invariant assertions (a generous timeout must not truncate; no timeout must return the full k).

Scope / confidence

High on the defect. executeVector() is the only SelectExecutor entry point with no deadline, and the chain above is reachable from the public fluent API.

Activity

  1. added this to the 26.9.1 milestone on Aug 28, 2026
  2. self-assigned this
    on Aug 28, 2026
  3. added a commit that references this issue on Aug 28, 2026
  4. robfrank commented on Aug 28, 2026

    @robfrank
    CollaboratorAuthor

    Fixed in #6877, merged to main.

    The vector k-NN search was the one query shape that never consulted the deadline at all. #6815 moved the native Select timeout onto the consumer side, which covered the four ordinary result sources, but nearestTo() runs down a separate path that was never wired up - so select().timeout(...).nearestTo(...) compiled, returned normally, and silently ignored the budget you asked for.

    The vector path now arms the deadline and checks it in three places: once per bucket index searched, once between the last index search and the merge/sort of the candidates, and once per result assembled. exceptionOnTimeout keeps its usual meaning - either throw a TimeoutException or stop and return what has already been assembled. One caveat carried over from the report: a single index search is not interruptible from the outside, so the deadline is honoured between indexes and per result, not in the middle of one long search inside a single index. That is still the difference between no bound at all and a bound.


    ❤️ Thanks for helping make ArcadeDB better! ArcadeDB is free and open source, sustained by its community. If it's useful to you or your company, please consider becoming a sponsor to keep development going.

  5. removed
    in progressThe team is actively working on this issue
    on Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions