Repository navigation
Conversation
…gs to load extra apps and URLconfs CAPABILITY_POLICY names the class that decides and records the use of capabilities, as EMAIL_BACKEND does for mail. The default, GroupLimitPolicy, is the per-user override and group limit logic that lived on UserProfile, moved unchanged. UserProfile's three capability methods now delegate to it. - Usage counts in windows: UsageCounter and UsageEvent get window_start. Null is the one window that never ends, which every existing row and the default policy use. consume() locks the user's row first, so a user's requests serialize even when they count in different windows. - A failed $match reverses its own usage event, which lowers the counter it counted in and records reversed_units on the event. - Refusals are UsageRefused, whose detail is the 403 body as the policy built it. Usage reports send every field the policy's get_usage() returns. Under the default policy every response is unchanged. - OCL_EXTRA_APPS and OCL_EXTRA_URLCONFS, comma-separated in the environment and empty by default, add Django apps and URLconfs to a build. Extra URLconfs come after core's own routes. - A contract test runs a fake policy through every call site. Refs OpenConceptLab/ocl_online#352 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RK9RzEWK5NKU6Wj9WSYGFw
…EY UPDATE A plain FOR UPDATE blocks the foreign-key check of another transaction that inserted a counter for the same user, such as a staff refund creating the current window's counter, while consume() waits on that counter's unique entry: a deadlock. FOR NO KEY UPDATE still serializes a user's consumes and lets the foreign-key check through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RK9RzEWK5NKU6Wj9WSYGFw
…usal body Nothing reads its limit, used, requested or capability_name attributes since the body moved into detail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RK9RzEWK5NKU6Wj9WSYGFw
Django drops a column's default once AddField has filled existing rows, so code that predates the column, still serving while a deploy migrates, would fail every usage event insert with a not-null violation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RK9RzEWK5NKU6Wj9WSYGFw
Member
Author
|
Codex review, pass 1 (codex-cli 0.164.0, commit 03e82de) Verdict: 1 finding(s).
findings.json{
"reviewer": "codex-cli 0.164.0",
"perspective": "defects",
"mode": "full",
"commit": "03e82de8",
"verdict": "findings",
"prior": [],
"findings": [
{
"id": "F1.1",
"severity": "P2",
"tag": null,
"file": "core/capabilities/policy.py",
"line": 119,
"claim": "The new user-row lock can deadlock a first consume against a staff refund.",
"scenario": "When the current window has no counter, a concurrent staff refund can insert it before consume locks the user. Consume then waits on the refund's uncommitted unique entry; the refund's deferred user foreign-key check waits on consume's FOR UPDATE lock. PostgreSQL aborts one transaction, producing an uncaught 500. This follows from static lock-order analysis using PostgreSQL's [uniqueness checks](https://www.postgresql.org/docs/14/index-unique-checks.html) and [foreign-key locking](https://github.com/postgres/postgres/blob/REL_14_STABLE/src/backend/utils/adt/ri_triggers.c).",
"action": "Use select_for_update(no_key=True) for the user lock, which still serializes consumes while permitting foreign-key checks under PostgreSQL's [lock compatibility rules](https://www.postgresql.org/docs/14/explicit-locking.html#LOCKING-ROWS), or make refund acquire the user lock first. Add a PostgreSQL concurrency regression test for an absent counter.",
"lines_delta": null,
"confidence": "high"
}
]
}Response: F1.1 fixed in 3765046. consume() locks the user row FOR NO KEY UPDATE, which still serializes a user's consumes and lets the foreign-key check through. Pinned by test_consume_locks_the_account_without_blocking_foreign_key_checks. |
Member
Author
|
Codex review, pass 2 (codex-cli 0.164.0, commit 3765046) Verdict: clean. Earlier findings: F1.1 resolved. findings.json{
"reviewer": "codex-cli 0.164.0",
"perspective": "defects",
"mode": "verify",
"commit": "3765046e",
"verdict": "clean",
"prior": [
{
"id": "F1.1",
"status": "resolved"
}
],
"findings": []
} |
Member
Author
Member
Author
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
OpenConceptLab/ocl_online#532, with the public metering parts of OpenConceptLab/ocl_online#352.
What this does
CAPABILITY_POLICYnames the class that decides and records the use of capabilities, asEMAIL_BACKENDdoes for mail. The default,GroupLimitPolicy, is the per-user override and group limit logic that lived onUserProfile, moved unchanged.UserProfile.get_capability_limit,get_capability_usageandcheck_and_consume_capabilitynow delegate to it, so their callers don't change.UsageCounterandUsageEventgetwindow_start.nullis the one window that never ends; every existing row and the default policy use it. A policy can count in windows by overridingget_window_start(). A counter is unique per (user, capability, window), with a second, partial constraint because PostgreSQL 14 treats nulls as distinct. Idempotency keys stay unique per (user, capability) across windows.consume()locks the user's row before the counter, so a user's requests serialize even when they count in different windows.$matchreverses its own usage event (UsageEvent.reverse), which lowers the counter that event counted in and recordsreversed_units. It never takes back more than the event recorded. The staff refund (POST /capabilities/refund/) stays a counter correction, applied to the policy's current window.UsageRefused, whosedetailis the 403 body.$matchandPOST /capabilities/consume/send it as built, adding only their own sentence asdetailwhen the policy gave none.GET /user/?includeCapabilities=trueand the consume and refund responses send every field the policy'sget_usage()returns.OCL_EXTRA_APPSandOCL_EXTRA_URLCONFS, comma-separated dotted paths from the environment, empty by default, add Django apps and URLconfs to a build. Extra URLconfs are included after core's own routes, so those keep precedence.used,unitsandreversed_unitsareBigIntegerField.Behaviour under the default
No API response changes: the same limits, the same 403 bodies (
detail,error_code,limit,used, with the same sentences and codes), and the sameused. The migration gives existing rowswindow_start = nulland keeps everyused. The differences are the extra row lock inconsume(),reversed_unitsrecorded when a$matchsearch fails, and the Swagger schema typing the capabilitylimitandusedas integers.Tests
$matchintegration tests that patched the moved methods now patchGroupLimitPolicy, with every response assertion kept, and the users test that patchedget_capability_usagepatchesGroupLimitPolicy.get_usage.CapabilityPolicyContractTestruns a fake policy, set throughCAPABILITY_POLICY, through every call site: theUserProfiledelegates, the user's capabilities, consume (success and refusal),$match(refusal, a failed search that's reversed, a search that isn't), and the staff refund.GroupLimitPolicyWindowTest: each window counts from 0, a key replays in a later window, a reversal lowers the window its units counted in, and a reversal never takes back more than the event or the counter holds.GroupLimitPolicyDefaultWindowTest: the default counts in the window that never ends;consume()locks the user rowFOR NO KEY UPDATE; an event inserted without the new columns, as the previous release does while a deploy migrates, gets their defaults.ExtraURLConfsTest: an extra URLconf is included after core's routes.Run locally in an isolated compose stack, as CI runs them (
test --parallel=1 --keepdb,PYTHONHASHSEED=2): 2,720 tests OK on master (685f307) and 2,736 OK on this branch (d7cff42).pylint -j0 core/: 10.00, no messages.Review
Reviewed before opening, with Codex (codex-cli 0.164.0, xhigh) and two fresh Claude reviews. Each Codex pass is posted below.
FOR UPDATEcould deadlock a first consume against a staff refund creating the same counter. Fixed in 3765046 (FOR NO KEY UPDATE).CapabilityExceeded's unread attributes, applied in 777c70a. Codex check of that commit: clean.reversed_unitslost its database default after the migration, so the previous release would fail every usage event insert during a rolling deploy. Fixed in d7cff42 (db_default=0). Codex check of that commit: clean. One P2 was accepted: the int-to-bigint rewrite locks two small tables for milliseconds. Six P3s were recorded, not fixed: a rare deadlock between a consume and deleting that same user (on master the same race ends in a 500), and contract edges for other policies and extra URLconfs.🤖 Generated with Claude Code
https://claude.ai/code/session_01RK9RzEWK5NKU6Wj9WSYGFw