Repository navigation
fix(config/db): first slice of the 2026-07-28 review findings (hidden_by min_count, RI-exchange limits, credential not-found, DB env parsing) - #1297
Conversation
The list filter drops a rec on five override dimensions, but the rec-detail explainer checked only four, so a rec hidden by a min_count floor opened with an empty hidden_by. Both now walk one overrideDimensions table, so a new dimension cannot reach only one. Refs LeanerCloud#120 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GlobalConfig.Validate never checked ri_exchange_max_per_exchange_usd, ri_exchange_max_daily_usd or ri_exchange_utilization_threshold, so a negative money cap or an out-of-range threshold sent to PUT /api/config was stored as is. Reject negative or non-finite caps and a threshold outside [0, 100], the bounds PUT /api/ri-exchange/config already applies. Refs LeanerCloud#120 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng row
A missing credential returned ("", nil), the same pair a stored empty
blob returns. Return ErrNotFound instead, before a caller reads the
empty result as "use the ambient identity".
Refs LeanerCloud#120
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… defaults getEnvInt, getEnvBool and getEnvDuration discarded parse errors, so a typo such as DB_MAX_CONNECTIONS="100 " silently became the default and passed Validate. LoadFromEnv now fails and names every set but unparseable variable. Refs LeanerCloud#120 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… case TestGlobalFiltersPersistedAmbientAndRegistered/minimum asserted an empty hidden_by for count-1 recs that the same subtest shows the list hiding. Every other row in the table, all global service-config policies, expects its dimension in hidden_by. The empty expectation dates from before the explainer knew about min_count, and min_count can only come from the service config (AccountServiceOverride has no MinCount), so this row encoded the list/detail contradiction the fix removes. Refs LeanerCloud#120 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in main's CI fixes (LeanerCloud#1236, LeanerCloud#1252) and CI changes since, so this PR gets a fresh CI run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rB6. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Local verification at |
|
Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 1 approving independent review(s) on |
First slice of #120's medium/low
internal/configand DB findings. I re-checked every finding against current main (a14dc8f2). This PR fixes four of them, each in its own commit with a fail-before/pass-after test. The rest are deferred below with reasons, mostly because an open PR or a queued branch already edits the same lines.Refs #120
Per-finding status
hidden_byomitsmin_countd7c2b4b2)d716b6b6). The coercion half and the DB CHECKs are deferred (see below)external_idorphans purchase historytotal_usagesummed as zero in rollupsfix/issue-151(migration 000147savings_rollups_propagate_unknown)ri_utilization_cachePK has no account dimensionGetAccountCredentialreturns("", nil)for a missing row2ad49ecb)CleanupOldExecutionsexpiry branch deletes rows in any statusfix/issue-47TryAdvisoryLockoverwrites a pinned connection1eb95c5f)None of the eleven was already fixed on main.
git log --grep "#120"only matchesab3bac1e, which is about an older #120 (inline Plan button). #1173 fixed finding 9's bug, but only incmd/cleanup-lambda(deleteExpired, for issue 998). The store'sCleanupOldExecutions, whichinternal/server/handler.go:310calls, still has the unrestricted branch.What changed
1.
hidden_byandmin_count(internal/scheduler/scheduler_overrides.go,scheduler.go).filterRecsByResolvedConfigsdropped a rec on five dimensions, butoverrideHiddenReasonschecked only four. So a rec under amin_countfloor disappeared from the list and then opened by ID withhidden_by = nil. Following the issue's fix direction, both functions now walk oneoverrideDimensionstable, andmin_countkeeps the Savings Plans carve-out from #1146. A future dimension cannot reach only one of the two. Doc comments onGetRecommendationByIDandapi.RecommendationDetailResponse.HiddenBynow listmin_count.This PR changes one row of an existing test:
TestGlobalFiltersPersistedAmbientAndRegistered/minimum(b4587190). It expected an emptyhidden_byfor the count-1 recs that the same subtest shows the list hiding. I think that row recorded the bug rather than an intended rule:hidden_byexplains global service-config filters, not only per-account overrides. Every other row in that table is a global policy, and each expects its dimension inhidden_by.handler_recommendations_global_filters_integration_test.golikewise expects["enabled=false"]from a global config.min_countcan only come from the service config.AccountServiceOverridehas noMinCount, and the resolver copiesbaseline.MinCount. So "report min_count only when an override set it" would never report it.min_count. 585c491 addedmin_countto the filter without touching the explainer and gave no reason for leaving it out.2. RI-exchange limits on PUT /api/config (
internal/config/validation.go).GlobalConfig.Validate, the only validation PUT /api/config runs on its whole-struct unmarshal, now rejects:ri_exchange_max_per_exchange_usdorri_exchange_max_daily_usdri_exchange_utilization_thresholdoutside[0, 100]These are the bounds PUT /api/ri-exchange/config's body already enforces, so both routes now accept the same values. 0 stays valid for all three: a 0 cap is the "not configured" state the exchange guard refuses to run under.
8.
GetAccountCredential(internal/config/store_postgres.go,interfaces.go). A missing row now returnsErrNotFoundinstead of("", nil), which is what a stored empty blob returns. The method still has no production callers, so no behavior changes today. The existing pgxmock test asserted the old("", nil)and now assertsErrNotFound.11. DB env parsing (
internal/database/config.go).getEnvInt,getEnvBoolandgetEnvDurationare now methods on a smallenvParserthat records every set but unparseable value.LoadFromEnvreturns oneerrors.Joinerror naming all of them, soDB_MAX_CONNECTIONS="100 ",DB_MAX_CONN_LIFETIME=1h30orDB_AUTO_MIGRATE=yesfail at boot instead of quietly becoming 25, 1h and false. Unset variables still use the defaults. I checked every value the IaC sets (Terraform for AWS Lambda and Fargate, GCP Cloud Run and GKE, Azure AKS and Container Apps, the cleanup functions), plus docker-compose, the Dockerfile and CI. All of them parse:"8s","5432",tostring(bool),tostring(number).Deferred, and why
GlobalConfig(internal/config/types.go, which many queued branches edit) and an owner decision on what a threshold of 0 should mean. The CHECK constraints need a migration, and for existing rows a repair step orNOT VALID. Since validation now covers both write routes, I left both parts for a follow-up slice.external_idchange orphans history. This needs an owner decision: reject the change when history exists, or rewritepurchase_history.account_idandsavings_snapshots.account_idin the same transaction. It also overlaps chore(db): backfill purchase_history.cloud_account_id from (provider, external_id) #1201 (fix/issue-22, backfillcloud_account_id) and fix(history): set cloud_account_id on every purchase_history insert #1244 (fix/issue-1235, setcloud_account_idon every insert), which shrink the affected set.total_usagerollups. Already being fixed on the queued branchfix/issue-151(migration 000147).appendAccountPredicateandsortedProviderKeysinstore_postgres.go. Queued branchesfix/issue-24(edits those functions),fix/issue-674andfix/issue-33(new callers) touch the same code. It should land after them.ri_utilization_cachekey. The issue itself says this is latent, not exploitable: the cache uses one ambient identity. The fix needs a migration plus an API cache-key change, so it belongs in its own slice.ListStoredRecommendationsis being reworked by perf(api): page recommendations from an ID-ordered keyset scan instead of the full list #1240 (fix/issue-981, keyset paging). Silently clampingListActiveSuppressionsto 1000 would be unsafe:applySuppressionswould stop hiding suppressed recs past the cap. It needs paging or an error, not a truncation.CleanupOldExecutionsexpiry branch. The fix edits the query and the "Branch 3" comment right next to lines that queued branchfix/issue-47changes (the cancelled/canceled contraction). The right fix is to reuse fix(cleanup): only delete settled executions when their TTL lapses #1173's predicate (a TTL lapse deletes onlyTerminalExecutionStatusesminusHealthScoredExecutionStatuses). Per that function's own doc, current code never setsexpires_at, so the branch only affects legacy rows.TryAdvisoryLockoverwrite. I could not find a path that reaches it. A pinned*pgxpool.Connstays acquired untilReleaseAdvisoryLock, so the pool cannot hand the same backend to a secondTryAdvisoryLock(lockID)while the map entry exists. Any other session'spg_try_advisory_lockon that key returns false. A failed unlock now closes the connection (fix(server): advisory lock released on the request context strands a task type in permanent already_running #105/chore: minor follow-ups from the #373/#376/#380/#383 reviews #394). SoStorenever replaces a live entry, and a defensiveLoadOrStorewould have no fail-before test. I left it alone.Cross-PR note
#1100 (fix/issue-262) adds an OpenAPI
enum: [enabled=false, engine, region, resource_type]forhidden_by. Whichever of #1100 and this PR merges second should addmin_countto that enum. Main has nohidden_byschema inopenapi.yamlyet, so I could not add it here.Verification
Unit and mock evidence, plus one real-Postgres test (shared container via
testhelpers.RequireMigratedDatabase). All runs usedAWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=trueand Go 1.26.9.Fail-before: I swapped in origin/main's version of the source file under test, with the new tests in place, then restored it.
go test ./internal/scheduler/ -run 'TestGetRecommendationByID_HiddenByMinCount|TestOverrideHiddenReasons'. On main,HiddenByMinCountfailed withexpected: []string{"min_count"} actual: []string(nil).AgreesWithFilterfailed for every mask with the min_count bit set, including "filter and hidden_by disagree". After the fix, both pass, as doesTestOverrideHiddenReasons_MinCountExemptsSavingsPlans. Real Postgres: with main's scheduler sources, the updatedTestGlobalFiltersPersistedAmbientAndRegistered/minimumfailed with[]string(nil) does not contain "min_count". On the branch,go test -tags integration -count=1 -timeout 60m -run 'GlobalFilters|HiddenBy|Override' ./internal/scheduler/passes.TestGlobalConfig_Validate_RIExchangeLimitsfailed 7 of 9 subtests (all the rejection cases).TestUpdateConfig_RejectsOutOfRangeRIExchangeLimits(internal/api, a router-level PUT /api/config with anexecute:ri-exchangeprincipal, so only validation can refuse) failed all 4: on main the negative cap or out-of-range threshold reachedSaveGlobalConfig. After the fix, both pass.go test -tags integration -count=1 -timeout 60m -run 'TestGetAccountCredential_MissingRowIsNotFoundPostgres' ./internal/config/failed (real Postgres) withExpected error with "not found" in chain but got nil, andTestPGXMock_GetAccountCredential_NotFoundalso failed. After the fix, both pass (--- PASS: TestGetAccountCredential_MissingRowIsNotFoundPostgres (30.81s)). The integration test also checks that a stored empty blob still reads back as("", nil)and a deleted credential asErrNotFound. No concurrency test: this is a single read with no read-modify-write.LoadFromEnvwithDB_MAX_CONNECTIONS="100 ",DB_MAX_CONN_LIFETIME=1h30,DB_AUTO_MIGRATE=yesloggedmain accepted malformed env: MaxConnections=25 MaxConnLifetime=1h0m0s AutoMigrate=falseand failedrequire.Error. After the fix,TestLoadFromEnv/returns_error_naming_every_malformed_variablepasses. TheTestGetEnvInt/Bool/Durationinvalid-value cases now also assert that the error names the variable.Checks on the branch head:
go build ./...,go vet ./...,go vet -tags integration ./...: cleango test -count=1on./internal/scheduler/,./internal/config/,./internal/api/,./internal/database/and./internal/mocks/, one package at a time: all okgolangci-lintv2.10.1run --allow-parallel-runners --build-tags integrationon scheduler, config, api and database: 0 issuesgocyclo -over 10 -ignore "_test\.go"on the changed non-test files: no outputCombined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree:
queue/platform@936996ad= origin/maina14dc8f2plus these heads, merged in this order:fix/issue-72andfix/issue-312conflict on one line ininternal/api/handler_dashboard.go(both change thecalculateCommitmentMetricscall). The tree used #72's side with #312's validatedprovidervariable; whichever PR merges second takes that same resolution.Linux, Go 1.26.9,
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers:936996ad:go build ./...,go vet ./...,go vet -tags integration ./...and the fullgo test -race -short -count=1 ./....936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without--build-tags integration.tests/e2eis a separate module, so it was vetted under its owne2etag.gocyclo -over 10is silent on the changed non-test files.-tags integration -count=1, 60-minute local timeout because the VM was shared):936996ad, these pass:internal/server,internal/credentials,internal/scheduler,internal/database/postgres/migrations,internal/purchaseandinternal/api.internal/configpasses exceptTestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck, which fails the same way on unmodified main (fixed by fix(config): compare resolve checked_at at the database's microsecond precision #1252).26795376, same branches at earlier heads):internal/analytics,internal/auth,internal/commitmentopts,internal/databaseandcmd/lambda/clear-rate-limitall pass. The later fixes don't touch those packages.httpToLambdaRequestsignature change;scripts/setup-git-secrets.shpatterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Behavior notes for reviewers
LoadFromEnvnow fails boot on a malformedDB_*tuning variable. Every value in the repo's IaC parses. A hand-setadditional_env_varsoverride with a typo would now stop the service at boot instead of running on defaults, which is the intended change.No frontend change is needed: the UI types
hidden_byasstring[]and does not map reason strings.Maintainer: please mirror #120's triage labels on this PR:
impact/internal,effort/l,type/chore,triaged,priority/p3,severity/low,urgency/eventually.🤖 Generated with Claude Code