A tier reorder that commits during a conferral records the wrong kind of tier change #213
Closed
opened 2026-10-10 23:03:45 +00:00 by cgalo5758
·
1 comment
Labels
Clear labels
accessibility
area/billing
area/catalog
area/discourse
area/domains
area/entitlements
area/fedwiki
area/identity
area/integrations
area/licensing
area/member-ui
area/meta
area/operator-ui
area/ops
area/testing
duplicate
good-first-issue
invalid
privacy
security
upstream
wontfix
A barrier for people using assistive technology or a keyboard alone.
The Stripe mirror, checkout, subscriptions, invoices, fulfillment.
Products, prices, plan ladders, purchasability.
The Discourse integration.
The domains registry, claims, placements, the certificate ask.
Entitlement sets, rules, grants, pools, provisioning.
The Federated Wiki integration and farm sync.
Sign-in, sessions, persons, organizations, workspaces, roles.
The provider registry, outbox and webhooks in general.
Licenses, the contributor agreement, SPDX headers.
Member pages.
The repository itself, its contributing guide, CI, the tracker and the workflow.
Operator pages, forms, lists, the design system.
Deployment, configuration, migrations, workflows, instance settings.
The test stack, screens, lint, walkthroughs.
Closed because another issue already covers it.
Small, self-contained, and explained enough to be a first contribution.
Closed because it is not a ticket for this repository.
Touches what a person's data reveals.
Touches authentication, authorization, secrets or data exposure.
Waits on another repository or project before it can move.
Closed because it will not be done, with the reason in the last comment.
kind
bug
The software does something other than what it promises; closed when it again does what it promises.
kind
debt
Code, tests or tooling to clean up with nothing visible changing; closed when they are cleaner.
kind
design
A question to settle before work can be defined; closed when the decision is written down.
kind
docs
Documentation that is wrong or missing; closed when it says the right thing.
kind
enhancement
Something the software does not do yet; closed when it does.
priority
critical
Blocks the active milestone or harms members now.
priority
high
Next in line inside the active milestone.
priority
low
Inside the active milestone, when nothing else is left.
priority
medium
Inside the active milestone, after the high ones.
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: wiki-cafe/member-console#213
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What happens
A conferral can record a tier change with the wrong type when an operator reorders the same ladder's tiers at the same moment. In the case seen, an organization grandfathered on its product by a default change is recorded as upgraded from rank 0 to rank 1 instead of transferred at the same rank. Its entitlements are right: one legacy grant and one live position, on the product it already had. The ledger row is wrong, and it is permanent. Because history rows name tiers by the ladder's current order (#211), Tier changes and Recent activity then show an upgrade from a product the organization never held.
core.conferreads ranks in separate statements: the incumbent's rank (from_rank), the same rank again for theendrow, and the new product's rank throughcore.product_conferral_shapes(to_rank). It classifies the change by comparing them. Materializing transactions run at READ COMMITTED, so each statement sees what has committed by the time it starts.The reorder's renumbering takes only the shared materialization rendezvous and no pool lock, so nothing orders it against a conferral running in another transaction. When it commits between the first and the last rank read, the same product reads as rank 0 and then as rank 1, and the conferral records
upgrade. The pool lock thatTestTierReorder_CommitRacesDefaultChangeCommit's comment relies on orders the two organization enactments against each other; the renumbering never takes it.That test catches the race intermittently with "transfer transitions = 0, want exactly 1": about 1 run in 25 under load, and now and then on an idle machine. A probe that pauses the conferral between its rank reads makes it fail every time.
The same window is open for any conferral that runs while a tier delete or a tier removal renumbers the ladder: a subscription reconcile, a grant, or the default floor. That follows from the code; it has not been reproduced.
What should happen
A tier change is classified against one order of the ladder, the order before the reorder or the order after it, never both. A grandfather on the same product records
transfer.Opening the renumbering transactions with
BeginRuleChange(the exclusive rendezvous, which orders a rule change against every materializing transaction) does this. With it on the reorder, the paused probe recordstransfer, and the natural race failed 0 of 600 runs against 17 of 600 without it. The cost is that a reorder can wait up to the rendezvous lock timeout (5s) behind conferrals in flight. The tier entries indocs/database-locks.mdand the test's comment change with it. Reading every rank once insidecore.conferis the larger alternative: a migration replacing the function, and it would not cover the default floor's own rank-0 read.Where
internal/server/operator_plan_ladders.go:ReorderPlanLadderTiers,DeletePlanLadderTierandCommitPlanLadderTierRemoval, which open their transactions withentitlements.BeginMaterializing;core.conferininternal/db/migrations/00023_grant_resumption.sql.Steps
Why it matters
The tier-change ledger is permanent, and a wrong type in it shows operators an upgrade that never happened, naming a product the organization never held.
Fixed in
a3041242. The tier add, the tier delete, the tier removal and the reorder's renumbering now open their transactions with the exclusive materialization lock (BeginRuleChange). A conferral reads a ladder's ranks and a product's shape in several statements; it now finishes before a tier change or starts after it, so it classifies every tier change against one order of the ladder. The tier add is included because it grows the added product's shape, which the conferral also reads more than once.TestTierChangesWaitForAConferralholds a conferral open and checks that each of the four tier changes waits for it, and the race test passed 500 runs idle and 500 under load. The entitlements spec now names the four tier changes among the exclusive holders.The cost: a tier change waits for every conferral, grant, reconcile and recomputation in flight, up to the 5 s lock timeout, and new ones wait behind it while it runs. When that wait times out, the add, the delete and the reorder still answer with their generic failure copy; #216 tracks that.