#279 Phase 3 — concurrent candidate solve + cache-key robustness (max_maker_fills / amount / trader) #324
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#324
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Phase 3 of the 0-LCD hybrid route-solver program (#279 parent). Two of the original #279 performance items that did not land in the schema (1a) or solver-rewire (1c) increments:
solve_global_best_executionwalks the (up to) 5 path candidates in a serialforloop,await-ing each candidate's optimize + simulate before starting the next. End-to-end latency is therefore the sum of all candidates. Run the per-candidateoptimize + maybe_simulatework concurrently under a sane concurrency cap so request latency tracks the slowest candidate, not the sum.hybrid_cache_keykeys on the rawmax_maker_fillsvalue and the raw normalized trader address. Honest variation in either (a caller passingmax_maker_fills=7vs8, or two distinct wallets on the same tier) produces a cache miss and forces a full re-solve, so the cache barely helps real traffic and is trivially bypassed. Drop/clampmax_maker_fillsand bucket the trader dimension so honest variation still hits the cache — without weakening the #283 discount-tier isolation.Per Plastik this lands after Phase 1c (#319): concurrency should be over DB-backed sims (cheap, CPU/Postgres-bound), not the current per-grid LCD fanout. Parallelizing LCD calls just multiplies the amplification surface #279 is trying to shrink.
Out of scope (explicitly): the #283 discount-tier cache keying — that already shipped in MR !751 and must be preserved exactly. Phase 2 (4-hop bump) and the DB-sim rewire itself (#319) are separate.
Current codebase
Serial candidate loop
indexer/src/api/best_execution.rs—solve_global_best_execution(line ~115). Afterenumerate_path_candidatesreturns up toMAX_PATH_CANDIDATESpaths, the body is afor cand in &candidates { … }loop (line ~130). Each iterationawaitshybrid_route_opt::optimize_multihop_hybrid_joint(line ~142) thenmaybe_simulate(line ~159) and folds the winner with the runningbest(line ~189). Nothing in the loop body depends on a previous iteration exceptbest(a max-by-output reduction) andlcd_queries(a saturating sum) — both are trivially mergeable after a concurrent fan-out.MAX_PATH_CANDIDATES: usize = 5(line 18);LCD_HYBRID_SIM_BUDGET(line ~26) is the documented worst-case sim countMAX_PATH_CANDIDATES * GET_DEFAULT_MAX_HOPS * (17 + 2*2*17).BestExecutionMeta(line ~39) carriespaths_considered,lcd_hybrid_queries,degraded,any_book_leg— all surfaced on the response.tokio::task::spawn_blockinginenumerate_path_candidates(line ~94, #286); the per-candidate work after it is still serial.Cache key
indexer/src/api/route_solver.rs—hybrid_cache_key(line ~532) builds"{SOLVER_VERSION}|{token_in}|{token_out}|{amount_bucket}|{max_maker_fills}|{trader_key}|t{discount_tier}".trader_keyis the raw lowercased trader address or"none"(line ~540);max_maker_fillsgoes in verbatim.GRID_POINTS = 17,hybrid_route_opt.rs:48), not caller-controllable — so #279 item-3's "grid size bounded" clause is already satisfied in code; this issue only adds the candidate-count cap + truncation flag.discount_tieris resolved viaresolve_discount_tier(line ~517) fromtraders.tier_idand is already part of the key per #283 — keep it. The unit testhybrid_cache_key_distinguishes_discount_tier(line ~857) pins both tier isolation and same-tier sharing; must stay green.amount_cache_key(line ~502) already bucketsamount_inbyAMOUNT_CACHE_BUCKET = 1_000_000(line 35) before keying, so the amount dimension is mostly handled — re-check the bucket is coarse enough that normal-range amount variation reuses entries.max_maker_fillsto8when omitted (solve_route_bestline ~660,solve_routeline ~698) and clamp to>= 1viamax_makers = max_maker_fills.max(1)inexecute_hybrid_route_solve(line ~601). The cache key sees the post-clamp value.route_hybrid_cache(line ~497),cache_get/cache_putwithROUTE_CACHE_TTL = 12sandROUTE_CACHE_MAX_ENTRIES = 512.Concurrency precedents / tooling
futures/futures-utilis not a direct dependency (indexer/Cargo.toml).tokiois onfeatures = ["full"], sotokio::task::JoinSetis available without a new crate;futures::stream::iter(...).buffer_unordered(n)would require addingfutures.oracle::run_oracle_loop,trader_tracker::run_tier_sync_loop, both spawned inindexer/src/indexer/poller.rs) are the snapshot/loop precedents for Phase 1b, not for in-request fan-out — they're sequentialsleep-driven loops, so they're not a concurrency template here. The fan-out pattern is new to this issue.Why this is needed
max_maker_fillsand raw trader address means the cache fragments under perfectly honest traffic and is bypassed by trivially varying either field — the same amplification concern #279 raises, one layer up from LCD. Clampingmax_maker_fillsto a few discrete values and bucketing the trader dimension restores real hit rates while #283's tier key still prevents cross-tier quote leakage.MAX_PATH_CANDIDATESabove the cap), the response must say so. Today nothing would flag a partially-searched result, and #279's whole theme is honest labeling over optimistic quotes.Constraints and guardrails
hybrid_simulationLCD calls. If 1c is not merged when this is picked up, hold.discount_tierstays inhybrid_cache_keyexactly as shipped in MR !751;hybrid_cache_key_distinguishes_discount_tierandhybrid_cache_key_includes_trader_or_nonemust still pass (the latter may need updating if trader bucketing changes its semantics — update it deliberately, don't delete the tier assertions).const SOLVE_CONCURRENCYor reuseMAX_PATH_CANDIDATES), not unboundedjoin_allover an arbitrary candidate count. Prefertokio::task::JoinSet(already available) over adding thefuturescrate unlessbuffer_unorderedis clearly cleaner — call the choice out in the MR.estimated_amount_out, with a stable tie-break (the serial loop keeps the first-seen max viaout_u > *prev_out). Preserve that ordering so results don't flap between requests.paths_considered,lcd_hybrid_queries/db_hybrid_queries,degraded,any_book_legmust aggregate correctly across the concurrent set (sum the query counts, OR the degraded flags) — same values the serial path would produce.optimize/simulateerror out of the whole request (?onlcd_gateway_err/maybe_simulate). Decide and document: fail-fast on first candidate error (preserve current behavior) vs. drop the failed candidate and continue. Don't silently swallow a sim failure into a degraded best.query_hybrid_simulationsemantics before deciding drop vs. clamp. Keep the existing>= 1floor.discount_tier(since the only quote-affecting property oftrader/senderis the resolved tier) — confirm no other trader-dependent branch exists in the solve before removing it.BestExecutionMetaand reflect it inhybrid_notes/ a response field — never return a truncated search as if it were complete.Recommended direction
solve_global_best_executioninto anasync fn evaluate_candidate(state, cand, …) -> Result<(RouteSolveResponse, u128, BestExecutionMeta), (StatusCode, String)>that does the optimize +apply_hybrid_by_hop+maybe_simulatefor one candidate and returns its scored result + per-candidate meta.candidateswith a boundedJoinSet(orbuffer_unordered(cap)iffuturesis added), cap =MAX_PATH_CANDIDATES(or a dedicatedSOLVE_CONCURRENCY). Collect results, then reduce: pick maxout_uwith the existing first-seen tie-break, sum query counts, OR thedegradedflags.hybrid_cache_key, replace the rawmax_maker_fillssegment with a clamped/bucketed value (clamp_maker_fills(max_maker_fills)), and droptrader_keyin favor of the already-presentdiscount_tier(or bucket it). KeepSOLVER_VERSION,amount_bucket, anddiscount_tier. Phase 3 inherits theSOLVER_VERSIONbump #319 ships; bump again only if this issue changes the key shape post-1c (droppingtrader_key/ clampingmax_maker_fills), so stale pre-Phase-3 cache entries don't serve under the new keying.search_truncated: bool(or similar) toBestExecutionMeta, set it when the candidate count exceeds the concurrency cap and not all were evaluated, and thread it intohybrid_notes_for_global/ the response.maybe_simulateexactly once per candidate as today; this issue does not change the fidelity guard (#319's job).Acceptance criteria
solve_global_best_executionevaluates path candidates concurrently under a bounded cap; for N independent candidates of comparable cost, measured request latency is ~max(candidate) not ~sum(candidate) (assert via a timed test with an artificially delayed per-candidate sim mock).estimated_amount_outas the serial implementation for a fixed seeded input (golden/differential test), including the first-seen tie-break on equal outputs.paths_considered, query-count,degraded, andany_book_legmetadata aggregate to the same values the serial path produced.hybrid_cache_key: requests differing only inmax_maker_fillswithin the normal range map to the same cache key (clamped/bucketed); a unit test asserts e.g.7and8share a key while an out-of-range/distinct bucket does not.hybrid_cache_key: two distinct same-tier trader addresses map to the same key; the #283 cross-tier isolation test (hybrid_cache_key_distinguishes_discount_tier) still passes unchanged.hybrid_notes); no truncated result is returned as complete.futuresdependency unless justified in the MR; ifJoinSetis used, no extra crate added.cargo testroute-solve + cache-key tests green;make lint/ indexer lib tests green.Test plan
estimated_amount_outdegradedOR'd, query counts summed,paths_consideredcorrectmax_maker_fillscache hitmmf=7thenmmf=8max_maker_fillsdistinct buckethybrid_noteswarns; result not labeled completemaybe_simulatereturns 500amount_inwithin one bucketAMOUNT_CACHE_BUCKETbehavior preserved)Related
mentioned in issue #279
mentioned in issue #319
marked as related to #319
mentioned in commit 806456a49713b69bd3b20378bdbbc8477c18ed1a
mentioned in merge request !809
Implementation complete in !809.
Summary: Concurrent path-candidate evaluation via JoinSet; cache-key bucketing for max_maker_fills + tier-only discount_bps; search_truncated flag.
Verification:
cd indexer && cargo test --lib— 127 passed. Integration tests need Postgres (SKIP in Cloud Agent).mentioned in commit
864e072ec1mentioned in commit
04d38a98d8mentioned in issue #335
mentioned in merge request !818
mentioned in issue #323
#324 verified — checked on current main (merged commit
864e072). Indexer-layer: source + unit + Postgres-backed route/cache integration tests, including the ones the agent marked skipped. Suites green, gate script verify-issue-324.sh is 3/3. I also did the serial-vs-concurrent parity check by hand — the serial loop was removed in this MR, so I pulled it out of git (864e072^) and compared against the new merge; they produce identical results.Acceptance criteria:
out_u > prev_outand surfaced the winner's snapshotted meta; merge_candidate_evaluations does the same. merge_picks_max_output_with_first_seen_tie_break + merge_equal_output_keeps_first_seen_candidate pin it.Net: 8 of 9 fully covered + serial parity confirmed by hand; the one soft spot is the concurrency timing — proven on the fan-out primitive, not a wired delayed-sim timed solve. Your call whether that's enough to close or you'd want a wired timed test on solve_global_best_execution first. #319 dep is merged so the gate's satisfied. @PlasticDigits
We dont need a wired timed test. Agent can verify without that
#324 verification complete — all acceptance criteria PASS
Verified on branch
main(current checkout) via automated suites and source review. Issue: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/324Commands run
Acceptance criteria
run_concurrent_candidate_evaluationsusestokio::task::JoinSetcapped atSOLVE_CONCURRENCY(=MAX_PATH_CANDIDATES= 5).concurrent_fanout_latency_tracks_max_not_sumasserts elapsed < 400ms for 5×80ms tasks (serial floor) and ≥ 80ms. Per maintainer note: fan-out primitive timing is sufficient; wired delayed-sim solve test not required.estimated_amount_outas serial (incl. first-seen tie-break)merge_candidate_evaluationsmirrors removed serial loop (out_u > prev_outtie-break).merge_picks_max_output_with_first_seen_tie_break,merge_equal_output_keeps_first_seen_candidate.paths_considered, query counts,degraded,any_book_leg)merge_winner_at_end_uses_cumulative_queries,merge_picks_max_output_with_first_seen_tie_break; merge logic atbest_execution.rs:296-309.max_maker_fillsclamped/bucketed (7 & 8 share key; distinct buckets differ)cache_key_maker_fills: ≤8→8, ≤16→16, else 30.hybrid_cache_key_same_tier_traders_share_key,hybrid_cache_key_maker_fills_distinct_buckets.discount_bpssegment (d{bps}).hybrid_cache_key_distinguishes_discount_bps. Integration:route_solve_get_cache_tier_isolation,route_solve_get_cache_same_tier_reuses_lcd.hybrid_noteswhen cap truncates searchsearch_truncatedonBestExecutionMeta+RouteSolveResponse.merge_sets_search_truncated_flag,hybrid_notes_warn_when_search_truncated.?behavior).concurrent_eval_fail_fast_aborts_remaining_tasks.futuresdependencyindexer/Cargo.tomlhas nofutures*direct dep; usestokio::task::JoinSet.Dependency #319 (Phase 1c)
PASS —
route_solver_db_hybridconfig +SOLVER_VERSION_DB(global_v4) present; DB hybrid integration tests (api_route_solve_db_hybrid) green. Concurrency runs over DB-backed sims, not per-grid LCD fanout.Test plan mapping (11 scenarios)
concurrent_fanout_latency_tracks_max_not_sum)merge_equal_output_keeps_first_seen_candidate)merge_winner_at_end_uses_cumulative_queries, etc.)max_maker_fillscache hit (7 vs 8)hybrid_cache_key_same_tier_traders_share_key)hybrid_cache_key_maker_fills_distinct_buckets)discount_bps)hybrid_cache_key_distinguishes_discount_bps+ HTTP integration)merge_sets_search_truncated_flag,hybrid_notes_warn_when_search_truncated)concurrent_eval_fail_fast_aborts_remaining_tasks)hybrid_cache_key_amount_bucket_regression)Net: 9/9 acceptance criteria PASS; 11/11 test-plan scenarios PASS.
Closing — implementation in !809 verified on current main.
mentioned in issue #337
mentioned in commit
022ed0283fmentioned in commit
bbe9ae1f3ementioned in issue #361
mentioned in merge request !885
mentioned in issue #420
mentioned in issue #485