Persist streamed quotes#4579
Conversation
|
Reminder: Please consider backward compatibility when modifying the API specification.
Caused by: |
|
Claude finished @fleupold's task in 1m 52s —— View job PR Review: Persist streamed quotesReviewed the diff against I left one inline comment on the new persistence block in
Both are worth a conscious decision, but not blocking. The latency / DB-pressure tradeoffs are already acknowledged in the PR description, so I won't restate them. Nothing else stood out — the mock setup ( |
There was a problem hiding this comment.
Code Review
This pull request updates the streaming quote endpoint to persist generated quotes and return their corresponding IDs, aligning its behavior with the one-shot quote endpoint. The changes span the OpenAPI documentation, quote handler, streaming logic, and tests. Feedback was provided to terminate the stream immediately upon a database save failure rather than continuing, as a database error is likely a persistent infrastructure issue.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
squadgazzz
left a comment
There was a problem hiding this comment.
I have the following concerns:
- The stored quotes set now depends on the requesting client's timeout. A stream that closes early persists only the fast solvers' quotes and drops slower solvers, some of which would have returned a better price. Those partial results then sit in the shared pool, and unrelated orders gonna reuse them through
find. Wouldn't one client's timeout choice leak into another user's quote quality? The stored quote becomes the order's reference quote. quotes::findmight get slower for orders placed without a quote id. On prod, the planner runs it as a full seq scan, so the cost tracks the total row count. WETH/USDC alone is already ~500 of ~5000 non-expired quotes. Storing one row per solver would inflate that significantly.
| assert!( | ||
| response.id.is_none(), | ||
| "streaming quotes should have id == null, got {:?}", | ||
| response.id.is_some(), |
There was a problem hiding this comment.
Does anything prove that /order with that id hits the stored quote instead of re-quoting?
There was a problem hiding this comment.
Good catch, added that to quote_stream_smoke.
fleupold
left a comment
There was a problem hiding this comment.
Wouldn't one client's timeout choice leak into another user's quote quality? The stored quote becomes the order's reference quote.
find_quote is only invoked when an order is placed without a quote id (in which case you are right we are trying to find an existing quote with the exact same amounts). I don't see how this is an issue in practice though. A good integration should only place orders with quote ids.
Also note, that already today someone requesting an optimal quote with a very short timeout will create an entry that another order may hit when being placed without a quote id.
quotes::find might get slower for orders placed without a quote id. On prod, the planner runs it as a full seq scan, so the cost tracks the total row count.
It's true that right now it's a SeqScan, but an index exists so the reason for this is likely that the current table size is too small to justify using the index today. Once it grows (max 20x), I'm pretty confident the index will protect us from getting too slow. In any case we have existing metrics around db latency, which we can monitor.
| assert!( | ||
| response.id.is_none(), | ||
| "streaming quotes should have id == null, got {:?}", | ||
| response.id.is_some(), |
There was a problem hiding this comment.
Good catch, added that to quote_stream_smoke.
Oh, that's right, I completely forgot about it. |
And currently, this I used this query to get some stats for orders that were created w/ and w/o quote id: https://victorialogs.dev.cow.fi/goto/bfqs2t7z0k4jkf?orgId=1 So the number of orders with no id is quite high and we probably need to adjust the |
b265656 to
a650c69
Compare
Yes, makes sense. I adjusted the find quote logic to do so. |
MartinquaXD
left a comment
There was a problem hiding this comment.
I think this is a reasonable fix for now but I feel ultimately we need to get the quote verification involved in this. The original quoting logic already computes the best-bang-for-buck and also considers whether quotes were verified.
Since the new logic does not do that yet solvers don't really need to provide working quotes to get someone to place an order. I don't know if quotes have to be verified to be eligible for quote rewards but I don't think it's the case at the moment.
In any case that would have to be solved in a different layer (e.g. in the CompetitionPriceEstimator) so this PR IMO already implements some reasonable logic (assuming the other thing gets addressed before the endpoint becomes heavily used). Just wanted to make you aware that the quotes yielded from the stream are subtly different to the HTTP endpoint.
Co-authored-by: Martin Magnus <martin.beckmann@protonmail.com>
|
I believe I addressed all concerns, @squadgazzz will leave it sitting over the weekend in case you want to do another pass. |
squadgazzz
left a comment
There was a problem hiding this comment.
LGTM. Given that nobody other than FE will use this endpoint for now, we shouldn't expect any load on it. Quotes verification can be done in follow-up PRs, as @MartinquaXD suggested.
Description
Streamed quotes currently don't carry a quote ID making it impossible to reuse them during order placement. This is problematic for a couple of reasons
Changes
How to test
Existing tests
Alternatives considered
https://app.notion.com/p/cownation/RFC-Quote-Persistence-for-Streaming-Quotes-38f8da5f04ca81a98f0fc016a3a37e81 contains two alternative AI generated options:
The first option leads to having no quote id unless we wait for the full timeout. This defeats the purpose of being able to quickly place orders as soon as we have a quote that is "good enough". Especially with the high deadlines suggested in cowprotocol/docs#638 I think we would not end up getting the final event in a good amount of cases.
In the second option, if the client terminates the connection prematurely there may be subjectivity around which quotes have been received (and therefore what the client saw as "best").
The main downside of the approach in this PR is that it adds a small amount of latency on the critical path (for the storage write), which in the grand scheme of current quote latency is likely irrelevant. It will also create more pressure on the DB, however since unused quotes are very short lived I don't expect this to be a concern in practice. It strikes me as the simplest approach therefore.