[rid/store] Fail ISA search instead of silently truncating the result list - #1640
[rid/store] Fail ISA search instead of silently truncating the result list#1640hdimer wants to merge 2 commits into
Conversation
… list SearchISAs capped its result list to dssmodels.MaxResultLimit and returned it with no error, so a client asking about a dense area got a non-exhaustive answer and no way to know it. Per the decision recorded in interuss#1120, return BadRequest (400) when more than MaxResultLimit areas match, so the client can narrow its query instead. The two backends also disagreed on the boundary: memstore appended before checking and returned MaxResultLimit+1 items, while sqlstore's LIMIT returned MaxResultLimit. Both now return up to MaxResultLimit and error beyond that. The RID and SCD subscription, operational intent and constraint searches truncate the same way; those are left for follow-up PRs.
|
|
mickmis
left a comment
There was a problem hiding this comment.
Hi @hdimer,
Thanks for your contribution!
In this case I believe it is worth going a step further and pushing this logic down in fetchISAs so that we can deduplicate it. What I mean by that would be e.g.:
fetchISAs: add alimitRows boolparameter- implement logic within
fetchISAsfor identifying that the maximum number of rows have been reached (appending theLIMITto the SQL query), and return an error there if so - callsites: remove logic for identifying maximum number of rows reached, use new flag where needed
We will certainly want to do the same for the others similar cases (fetchConstraints, process, fetchOperationalIntents, etc.), but that is outside the scope of this PR. A mention of that in #1120 would be desirable though.
In addition I have two open points:
- Error code to be returned: I think that returning a 400 is technically incorrect: the request sent by the client is totally valid. A more appropriate code would IMO be 422. But we do not have the infra to return that at the moment. We probably don't want to build that now, especially since we are slowly moving away from this store. Would a 500 be more accurate to be returned here? Any opinion @BenjaminPelletier ?
- About having this behavior in the memstore: maybe we want to actually remove that completely from the memstore. The original reason why we has that was because of OOM kills (c.f. #1120). But with the memstore everything is in memory anyway... Maybe we should not touch that in this PR and remove everything in another one? Opinion @the-glu ?
The cap and the over-limit error now live in fetchISAs behind a limitRows flag, so SearchISAs no longer builds its own LIMIT or checks the row count. ListExpiredISAs opts out and keeps its own limit: truncation is the wanted behaviour for the evict sweep. Adds a datastore-free test pinning the query fetchISAs builds for each flag value, which the existing sqlstore tests cannot cover since they skip without a live datastore.
|
Done, pushed.
Two things worth flagging on the shape:
I also added a datastore-free test for the query construction. The existing sqlstore tests all skip without a live datastore, so nothing was covering the part this commit actually changes; a fake The other sites are listed in #1120 (#1120 (comment)), with one gotcha for whoever picks them up: the SCD loops stop on On your two open points I have changed nothing and I am happy with either answer:
Still open from my side, the last section of the description: |
Yes, the goal was to first make the memstore 'equivalent' and then remove it, when we have the test part done in memstore. I wouldn't touch it for now. Also for those PR: This is a big behavior change, I'm not sure it's worth it doing it in the 'probably soon old' sql store, since we're going to remove the limits for the raft/memstore. What do you think? |
Fixes the RID identification service area case of #1120.
SearchISAscapped its result list todssmodels.MaxResultLimitand returned it witherr == nil, so a client querying a dense area received a non-exhaustive list with no indication that anything had been dropped. Both backends now returndsserr.BadRequestonce more thanMaxResultLimitareas match, which the v1 and v2 ISA handlers already map to a 400 with the message, so no handler change was needed.This follows the approach @barroco recorded from the 2024-09-17 contributors sync call in #1120 (comment): fail with a 400 and ask clients to scope their request more tightly, rather than paginate.
What changed
pkg/rid/store/memstore/identification_service_area.go— thebreak(and its// This mimics sqlstore behaviour, but it's not very good.TODO) becomes aBadRequest.pkg/rid/store/sqlstore/identification_service_area.go—LIMIT MaxResultLimit + 1, then the sameBadRequestwhen that extra row comes back. Selecting one row past the limit is what lets an over-limit result set be distinguished from one that exactly fills it.MaxResultLimitmatches still return the full list with no error.Incidentally this removes a real divergence between the two backends.
memstoreappended before checkinglen(out) > MaxResultLimit, so it returned 10001 items wheresqlstore'sLIMIT 10000returned 10000; I measured both before changing anything. The old TODO claimed memstore mimicked sqlstore, and it didn't quite. Both now agree.ListExpiredISAsis deliberately left alone: it is the operator-runevictsweep, where processing at mostMaxResultLimitentries per run and picking up the rest next time is the desired behaviour, not a lie to a client.One thing I would like your call on
App.SearchISAshas six call sites and only two are the search endpoint. The other four are subscription create/update (pkg/rid/server/v{1,2}/subscription_handler.go), which call it to populateservice_areasin the response — and the subscription is already committed by then. So in an area with more thanMaxResultLimitISAs,PUT/PATCHsubscription now returns 400 after the write, and a client that retries the same ID gets 409. Previously that branch was unreachable, sinceSearchISAscould only fail there on empty cells.That truncated
service_areaslist is arguably the same bug as #1120 — a client bootstrapping its notification state gets a silently incomplete picture — so failing seems consistent with the recorded decision. But the write-then-400 ordering is not great. Moving theSearchISAscall above the insert/update at those four sites would fix it cleanly (inserting a subscription cannot change which ISAs match; different tables), and I am happy to do that here or in a follow-up. I did not want to touch four handlers uninvited on a PR this size. The release note covers the behaviour either way.Testing
TestStoreSearchISAsResultLimitin the memstore pins both halves of the boundary: exactlyMaxResultLimitmatches succeed with a full list, one more errors withBadRequest. I ran it against the unpatched store first and confirmed it fails on the over-limit assertion for the right reason. It also fails if>is loosened to>=, if the truncated list is returned alongside the error, or if the error is raised without theBadRequestcode (which would surface as a 500 rather than a 400).go test ./pkg/... ./cmds/...green,gofmt -s -l .clean,golangci-lint run(v2.12.2, per the Makefile) reports 0 issues.make test-go-units-crdblocally, so the sqlstore half is not covered by a test I executed — the existingTestStoreSearchISAsin that package will confirm in CI that the query still works with the new limit, but nothing reaches the new branch. I left it that way on purpose rather than adding a crdb test that inserts 10001 rows into the shared CI suite for logic already pinned in memstore. Say the word and I will add one.Follow-ups (not in this PR)
The identical pattern is still in
pkg/rid/store/{memstore,sqlstore}/subscriptions.goandpkg/scd/store/{memstore,sqlstore}/{subscriptions,operational_intents,constraints}.go—memstore/subscriptions.goeven carries the same TODO comment. Worth noting for whoever picks those up: the SCD sites break on>=, so they need the samelimit + 1treatment rather than a copy of the predicate from here. Happy to take them one at a time if this shape is what you want.Disclosure: this change was prepared with AI assistance (Claude Code). The repro and tests described above were run before it was opened.