Skip to content

RateLimiter database storage follow-ups: atomic upsert, schema discriminator, RMW locking, engine docs #2911

Description

@bpamiri

Follow-up scope deliberately deferred from the database-backed rate-limit enforcement fix (internal framework review 2026-06-09, finding T8). The fix made storage="database" actually enforce on every engine; these items close the remaining (bounded, disclosed) gaps.

1. Dialect-aware atomic upsert for the fixedWindow first-insert race

$dbIncrement() (vendor/wheels/middleware/RateLimiter.cfc) uses UPDATE-first + SELECT MAX + insert-on-miss + one re-read. Concurrent first requests for the same key/window can each insert a row, producing a bounded one-time-per-window undercount (at most N-1 extra requests for N racing nodes, once per window). A real fix is a per-dialect atomic upsert:

  • MySQL: INSERT ... ON DUPLICATE KEY UPDATE counter = counter + 1
  • PostgreSQL / SQLite: INSERT ... ON CONFLICT (store_key) DO UPDATE SET counter = wheels_rate_limits.counter + 1
  • SQL Server: MERGE (with HOLDLOCK) or the UPDATE-then-INSERT retry-on-2627 idiom
  • Oracle: MERGE INTO

All of these require a UNIQUE index on store_key, which item 2 currently blocks.

2. Schema redesign with a row-type discriminator

A UNIQUE(store_key) index cannot be added today because the slidingWindow strategy inserts one row per request into the same wheels_rate_limits table (counter rows and event rows share the schema). Proposed: add a row-type discriminator column (e.g. row_type in counter|event|bucket) and scope uniqueness to counter/bucket rows (partial/filtered unique index where supported, or split tables). This is a schema migration for existing wheels_rate_limits tables, so it needs an upgrade path (the table is auto-created, so probe-and-alter or rename-and-recreate are both viable).

3. Locking for DB tokenBucket / slidingWindow read-modify-write

$dbTokenBucket() and $dbSlidingWindow() do unguarded read-modify-write sequences; the in-process cflock only serializes within one node, so multi-node deployments can interleave (token over-grant / event-count skew). Options: SELECT ... FOR UPDATE where supported, optimistic concurrency via a version column, or single-statement atomic forms (UPDATE ... SET tokens = LEAST(...)).

Neither residual race is a sustainable attacker bypass — the undercount is bounded and once-per-window — but they should be closed for strict multi-node accounting.

4. Document engine support for storage="database"

$detectDatabaseType() maps MySQL, PostgreSQL, SQL Server, Oracle, H2, SQLite (and falls back to a generic dialect). The rate-limiting guide (web/sites/guides/src/content/docs/v4-0-0/digging-deeper/rate-limiting.mdx) should state which engines are supported/CI-verified for database storage and what the fallback behavior is.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions