fix(api): stop full-model updates from clobbering targeted-write columns#6639
fix(api): stop full-model updates from clobbering targeted-write columns#6639otavio wants to merge 3 commits into
Conversation
|
Claude finished @otavio's task in 10m 30s —— View job Code Review CompleteReviewed 22 files across code quality, security, testing, language patterns, and architecture. The core fix is solid — the One issue survived validation but could not be posted inline (the line isn't part of this PR's diff), plus one cross-repo heads-up: Additional notes1. Missed clobber site —
user.Preferences.PreferredNamespace = ""
if err := s.store.UserUpdate(ctx, user); err != nil { ... }Now that In the common case it's masked by if err := s.store.UserUpdatePreferredNamespace(ctx, req.UserID, ""); err != nil {
log.WithError(err).
WithField("tenant_id", req.TenantID).
WithField("user_id", req.UserID).
Error("failed to reset user's preferred namespace")
}2. Cross-repo mock regeneration (cloud) This PR adds (Minor, not blocking: the leftover To request another review round, comment |
Route LeaveNamespace's preferred-namespace clear through the targeted UserUpdatePreferredNamespace: preferred_namespace_id is now skipupdate, so the prior full-model UserUpdate silently failed to clear it. Drop the dead preferred-namespace assignment in CreateUserToken (persisted separately via the targeted write).
|
Addressed in ffad49c:
|
|
/review |
|
Claude finished @otavio's task in 2m 23s —— View job Code Review CompleteReviewed 24 files across code quality, security, testing, language patterns, and architecture — no new issues found. The code looks good as-is. The two items from the previous round were correctly addressed in
Spot-checks that passed on re-review:
Reminder (already tracked by the author): the paired To request another review round, comment |
Several *Update methods in api/store/pg issued full-model bun updates that rewrote every mapped column from an in-memory snapshot. When a column is maintained by a separate targeted write (an atomic counter, the heartbeat, a NULL clear), a full-model update carrying a stale value silently overwrote the concurrently-maintained value — a lost update visible only under concurrency. Mark the affected columns with bun's ,skipupdate so they stay out of every generated UPDATE while INSERT and explicit .Set writes still work: namespace device counters, device custom_fields/last_seen/disconnected_at, user preferred_namespace_id, active_sessions created_at, and the invitations counter. Route the mixed-writer paths through targeted writes: the device online transition via DeviceHeartbeat, a new DeviceOffline for OfflineDevice, and a new UserUpdatePreferredNamespace for login. Add testcontainers regression tests that interleave a targeted write with a stale full-model update per case; the mocked-store service tests cannot catch this class of bug. Fixes: #6637
Route LeaveNamespace's preferred-namespace clear through the targeted UserUpdatePreferredNamespace: preferred_namespace_id is now skipupdate, so the prior full-model UserUpdate silently failed to clear it. Drop the dead preferred-namespace assignment in CreateUserToken (persisted separately via the targeted write).
ffad49c to
cef0d6a
Compare
|
/review |
|
Claude finished @otavio's task in 6m 10s —— View job Code Review CompleteReviewed 24 files across code quality, security, testing, language patterns, and architecture — no new issues found. The code looks good as-is. The two items from the prior round remain correctly addressed in
Spot-checks that passed on this round:
To request another review round, comment |
The reconnect path now brings the device online through DeviceHeartbeat after DeviceUpdate, so TestAuthDevice_RemoteAddr's reconnect subtests must set up that expectation.
|
/review |
|
Claude finished @otavio's task in 7m 16s —— View job Code Review CompleteReviewed 24 files across code quality, security, testing, language patterns, and architecture — no new issues found. The code looks good as-is. Spot-checks that passed on this round:
To request another review round, comment |
What
Several
*Updatemethods inapi/store/pgissued full-model bun updates(
db.NewUpdate().Model(x).WherePK()with no allowlist) that rewrote everymapped column from an in-memory snapshot. When a table has a column maintained
by a separate targeted write, a full-model update carrying a stale value
silently overwrote the concurrently-maintained value.
Why
Closes #6637.
The failure is a lost update that only manifests under concurrency — a counter
increment or heartbeat landing between the
resolveand thesaveof afull-model update. It passes the mocked-store service tests and is easy to miss
in review.
Changes
,skipupdatetags: keep a column out of every generated UPDATEwhile still allowing INSERT and explicit
.Set(...)writes. Applied to thefour namespace device counters, device
custom_fields/last_seen/disconnected_at, userpreferred_namespace_id,active_sessions.created_at,and the
user_invitations.invitationscounter. Using the entity tag (ratherthan a per-method column allowlist) means every current and future update path
is protected by one declaration.
last_seen/disconnected_atare nowowned by
DeviceHeartbeat. The agent-auth online transition routes throughDeviceHeartbeat, andOfflineDeviceuses a new targetedDeviceOfflinestore method instead of a full-model update (which also drops a redundant
DeviceResolve).UserUpdatePreferredNamespacestore method; the membership-removal clear inmember.gois now a single conditionalSet(... = NULL)instead of aselect-then-model-update.
UserInvitationUpdate'sColumn(...)list (now redundant with the entity tag).
api/store/storetest/clobber_tests.goreproduces eachclobber by interleaving a targeted write with a stale full-model update and
asserting the targeted column survives.
Testing
go test ./store/pg/... ./services/...inside the api container. The newstore-level tests need testcontainers (a real PostgreSQL), which the
mocked-store service tests cannot substitute for this class of bug. Removing any
one
,skipupdatetag makes the corresponding regression test fail.