feat(indexer): bound route DFS with an expansion budget + truncated flag (#286) #745

Merged
Brouie merged 2 commits from qa/286-route-dfs-expansion-budget into main 2026-06-04 12:54:29 +00:00
Brouie commented 2026-06-04 05:39:16 +00:00 (Migrated from gitlab.com)

Bounds route DFS without lossy truncation by using a goal-distance reachability gate, admissible DFS pruning, and shortest-first iterative deepening. Additional optimization in this update keeps same-hop route capping deterministic by sorting adjacency, avoids max(asset_id)-sized visited allocations for sparse IDs, skips impossible shallow deepening passes, and removes allocation-heavy path sort keys.

This MR intentionally does not add a close keyword for #286: the original issue asked for a truncated flag, but review concluded truncation is the wrong strategy. The implementation now avoids truncation entirely, so the dense-unreachable abuse case is bounded without dropping unknown valid routes.

Verification checklist

Item Command / check Result
MR review: no unsafe truncation or truncated response flag glab mr view 27 -R PlasticDigits/cl8y-dex-terraclassic --comments PASS
Dense unreachable graph is bounded and route-path regressions pass cargo test --manifest-path indexer/Cargo.toml route_paths --lib PASS
Full indexer library callers still pass cargo test --manifest-path indexer/Cargo.toml --lib PASS
Touched Rust file is formatted and linted rustfmt --edition 2021 --check indexer/src/api/route_paths.rs && cargo clippy --manifest-path indexer/Cargo.toml --lib PASS
GitLab CI failures GitLab MR API head_pipeline PASS - no head pipeline/failing jobs reported

Notes

  • No local stack was started; the changed layer is pure in-memory route enumeration over indexed pair rows.
  • GitLab #286 remains referenced but not closed by this description because the literal truncated-flag acceptance criterion is intentionally replaced by the reviewed no-truncation design.
Bounds route DFS without lossy truncation by using a goal-distance reachability gate, admissible DFS pruning, and shortest-first iterative deepening. Additional optimization in this update keeps same-hop route capping deterministic by sorting adjacency, avoids max(asset_id)-sized visited allocations for sparse IDs, skips impossible shallow deepening passes, and removes allocation-heavy path sort keys. This MR intentionally does **not** add a close keyword for #286: the original issue asked for a truncated flag, but review concluded truncation is the wrong strategy. The implementation now avoids truncation entirely, so the dense-unreachable abuse case is bounded without dropping unknown valid routes. ## Verification checklist | Item | Command / check | Result | |---|---|---| | MR review: no unsafe truncation or truncated response flag | `glab mr view 27 -R PlasticDigits/cl8y-dex-terraclassic --comments` | PASS | | Dense unreachable graph is bounded and route-path regressions pass | `cargo test --manifest-path indexer/Cargo.toml route_paths --lib` | PASS | | Full indexer library callers still pass | `cargo test --manifest-path indexer/Cargo.toml --lib` | PASS | | Touched Rust file is formatted and linted | `rustfmt --edition 2021 --check indexer/src/api/route_paths.rs && cargo clippy --manifest-path indexer/Cargo.toml --lib` | PASS | | GitLab CI failures | GitLab MR API `head_pipeline` | PASS - no head pipeline/failing jobs reported | ## Notes - No local stack was started; the changed layer is pure in-memory route enumeration over indexed pair rows. - GitLab #286 remains referenced but not closed by this description because the literal truncated-flag acceptance criterion is intentionally replaced by the reviewed no-truncation design.
PlasticDigits commented 2026-06-04 08:08:04 +00:00 (Migrated from gitlab.com)

Per comment on #286 - need to review truncation strategy & if truncation is necessary. If it is necessary, then we need a way to determine which paths are higher priority vs lower ones - such as paths from a list of priority intermediate tokens

Per comment on #286 - need to review truncation strategy & if truncation is necessary. If it is necessary, then we need a way to determine which paths are higher priority vs lower ones - such as paths from a list of priority intermediate tokens
Brouie commented 2026-06-04 11:29:45 +00:00 (Migrated from gitlab.com)

Reworked this from scratch — you're right, the expansion budget was the wrong call. Truncation can drop a route that actually exists and we can't say which one, so I pulled it out entirely. No truncated flag anymore.

New approach, nothing dropped:

  1. Reachability gate. One BFS from the goal precomputes each token's minimum hop-distance to the goal (O(V+E)). If the input token can't reach the output token within max_hops, we return "no route" immediately without enumerating anything. That's exactly the abuse case from the issue (dense graph + unreachable goal) and it's now linear instead of O(branching^max_hops). It also covers your point (1): an input/output token with no pairs is just unreachable, so it bails instantly.

  2. Admissible pruning on the walk. The DFS only steps into a neighbor whose own shortest distance to the goal still fits the remaining hops. This provably can't drop a valid route — any real continuation has to fit the hop budget anyway — so it only ever skips dead ends.

  3. Shortest-first via iterative deepening (the priority part). Instead of taking the first 5 paths the walk happens to hit, it deepens one hop at a time and keeps the K shortest routes. Fewest hops is the natural priority (lower fees, less slippage), so a short/direct route can't get crowded out by longer ones. Found a real bug here while doing it: the old max_paths cap could fill all 5 candidate slots with 2-hop routes and miss a 1-hop direct pair entirely — added a regression test that fails on the prior code and passes now. If you want an explicit priority-intermediate-token list on top of this later, this is the right place to hang it, but shortest-hop already gives a principled ordering with zero dropped paths.

  4. Off the executor (your point 3). The enumeration now runs under spawn_blocking so it's off the async worker thread. With the work bounded to O(V+E) it's cheap regardless, but this keeps a big legit pair graph from ever stalling the runtime.

Demonstration (your point 2): added a self-contained test that builds a complete graph on 50 nodes with an unreachable goal. The unpruned reference walk enumerates >100k node expansions — that's the blowup — while the gated version does 0 expansions and returns empty. A second test proves a reachable goal in that same dense graph still resolves and stays bounded (~a dozen expansions), and the crowd-out test above proves the direct route is always kept.

route_paths 8/8, full indexer lib suite green. This is the source + unit-test layer — pure in-memory path enumeration over the pair set, so there's no chain or indexer-state dependency to exercise live. The route_search_truncated field is gone from the response (it was never released, so nothing downstream depended on it).

Branch force-pushed. @PlasticDigits

Reworked this from scratch — you're right, the expansion budget was the wrong call. Truncation can drop a route that actually exists and we can't say which one, so I pulled it out entirely. No truncated flag anymore. New approach, nothing dropped: 1. Reachability gate. One BFS from the goal precomputes each token's minimum hop-distance to the goal (O(V+E)). If the input token can't reach the output token within max_hops, we return "no route" immediately without enumerating anything. That's exactly the abuse case from the issue (dense graph + unreachable goal) and it's now linear instead of O(branching^max_hops). It also covers your point (1): an input/output token with no pairs is just unreachable, so it bails instantly. 2. Admissible pruning on the walk. The DFS only steps into a neighbor whose own shortest distance to the goal still fits the remaining hops. This provably can't drop a valid route — any real continuation has to fit the hop budget anyway — so it only ever skips dead ends. 3. Shortest-first via iterative deepening (the priority part). Instead of taking the first 5 paths the walk happens to hit, it deepens one hop at a time and keeps the K shortest routes. Fewest hops is the natural priority (lower fees, less slippage), so a short/direct route can't get crowded out by longer ones. Found a real bug here while doing it: the old max_paths cap could fill all 5 candidate slots with 2-hop routes and miss a 1-hop direct pair entirely — added a regression test that fails on the prior code and passes now. If you want an explicit priority-intermediate-token list on top of this later, this is the right place to hang it, but shortest-hop already gives a principled ordering with zero dropped paths. 4. Off the executor (your point 3). The enumeration now runs under spawn_blocking so it's off the async worker thread. With the work bounded to O(V+E) it's cheap regardless, but this keeps a big legit pair graph from ever stalling the runtime. Demonstration (your point 2): added a self-contained test that builds a complete graph on 50 nodes with an unreachable goal. The unpruned reference walk enumerates >100k node expansions — that's the blowup — while the gated version does 0 expansions and returns empty. A second test proves a reachable goal in that same dense graph still resolves and stays bounded (~a dozen expansions), and the crowd-out test above proves the direct route is always kept. route_paths 8/8, full indexer lib suite green. This is the source + unit-test layer — pure in-memory path enumeration over the pair set, so there's no chain or indexer-state dependency to exercise live. The route_search_truncated field is gone from the response (it was never released, so nothing downstream depended on it). Branch force-pushed. @PlasticDigits
Brouie commented 2026-06-04 11:30:04 +00:00 (Migrated from gitlab.com)

added 1 commit

  • e3eb5ec9 - feat(indexer): bound route DFS via reachability gate + shortest-path iterative deepening (#286)

Compare with previous version

added 1 commit <ul><li>e3eb5ec9 - feat(indexer): bound route DFS via reachability gate + shortest-path iterative deepening (#286)</li></ul> [Compare with previous version](/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/27/diffs?diff_id=1834474164&start_sha=fc678b1d2bbbf3366cac63b5418a2e925b22d6a1)
Brouie commented 2026-06-04 11:38:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #286

mentioned in issue #286
PlasticDigits commented 2026-06-04 12:36:18 +00:00 (Migrated from gitlab.com)

Excellent. Reviewing for additional optimizations

Excellent. Reviewing for additional optimizations
PlasticDigits commented 2026-06-04 12:45:54 +00:00 (Migrated from gitlab.com)

added 1 commit

  • 26a6d5bf - fix(indexer): tighten route path enumeration

Compare with previous version

added 1 commit <ul><li>26a6d5bf - fix(indexer): tighten route path enumeration</li></ul> [Compare with previous version](/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/27/diffs?diff_id=1834628822&start_sha=e3eb5ec9bcef4bb53e2fb2f4543c525463b6b9d2)
PlasticDigits commented 2026-06-04 12:48:43 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-04 12:49:07 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-04 12:54:30 +00:00 (Migrated from gitlab.com)

mentioned in commit 679cc56fac

mentioned in commit 679cc56fac77db4ff2b51b35d0e7844bb85a976d
PlasticDigits (Migrated from gitlab.com) merged commit 679cc56fac into main 2026-06-04 12:54:30 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic!745
No description provided.