app.services.rate_limit_counter_backend module

Pluggable counter backends used by AbusePreventionService.

The DB-backed backend writes to abuse_limit_counter (preserves the historical behavior and keeps existing migrations relevant). The Redis-backed backend uses INCR + SET NX EX which is atomic, ~50 us per request, and naturally distributed across API instances – the cost an UPDATE on a hot row in Postgres does not pay.

Selection is driven by configs.ABUSE_PREVENTION_BACKEND and wired in app/core/container.py.

class app.services.rate_limit_counter_backend.RateLimitCounterBackend(*args, **kwargs)[source]

Bases: Protocol

Increment a per-bucket counter and return its new value.

async increment_and_get(scope_type, scope_value, window_name, window_start, ttl_seconds)[source]

Increment the bucket’s counter and return its new value.

Parameters:
  • scope_type (str)

  • scope_value (str)

  • window_name (str)

  • window_start (datetime)

  • ttl_seconds (int)

Return type:

int

class app.services.rate_limit_counter_backend.DatabaseRateLimitCounterBackend(repository)[source]

Bases: object

Backend that persists counters in abuse_limit_counter.

ttl_seconds is unused because the row’s bucket key already encodes window boundaries; expired rows can be reaped offline (a future periodic job, not the request path).

Parameters:

repository (AbuseLimitCounterRepository)

async increment_and_get(scope_type, scope_value, window_name, window_start, ttl_seconds)[source]

Increment a DB-backed rate-limit counter and return its value.

ttl_seconds is ignored: the bucket’s window_start already encodes window boundaries, so expiry is handled offline.

Parameters:
  • scope_type (str) – Dimension being limited.

  • scope_value (str) – Concrete value within scope_type.

  • window_name (str) – Name of the time window/bucket.

  • window_start (datetime) – Start of the bucket window.

  • ttl_seconds (int) – Ignored by this backend.

Returns:

int – The counter value after the increment.

Return type:

int

class app.services.rate_limit_counter_backend.RedisRateLimitCounterBackend(client, key_prefix='game:rl:')[source]

Bases: object

Backend that uses Redis INCR + SET NX EX for per-bucket counters.

Each bucket maps to one key with a TTL slightly longer than the window. SET key 0 EX ttl NX plants a zeroed counter with a TTL atomically on the first request of a bucket; subsequent INCR calls just bump it. The pipeline below batches both commands into a single round-trip and runs under MULTI/EXEC so other clients cannot observe a key without a TTL.

Parameters:

key_prefix (str)

async increment_and_get(scope_type, scope_value, window_name, window_start, ttl_seconds)[source]

Increment a Redis-backed rate-limit counter and return its value.

Uses a single MULTI/EXEC pipeline of SET key 0 EX ttl NX (plant a TTL’d zero on the first hit) followed by INCR so a key is never visible without a TTL.

Parameters:
  • scope_type (str) – Dimension being limited.

  • scope_value (str) – Concrete value within scope_type.

  • window_name (str) – Name of the time window/bucket.

  • window_start (datetime) – Start of the bucket window.

  • ttl_seconds (int) – Key TTL (clamped to ≥ 1 second).

Returns:

int – The counter value after the increment.

Return type:

int

app.services.rate_limit_counter_backend.build_redis_client_from_url(url)[source]

Build an async Redis client from a connection URL.

Imported lazily so the redis package is only required when the Redis backend is actually selected – callers running the DB backend never pay the dependency cost.

Parameters:

url (str)

Return type:

Any

app.services.rate_limit_counter_backend.build_rate_limit_counter_backend(repository, backend_name, redis_url, redis_key_prefix)[source]

Select the configured backend. Falls back to the DB backend with a warning when Redis is requested but REDIS_URL is missing – so a misconfigured deploy still rate-limits (just slower) instead of opening the floodgates.

Parameters:
Return type:

RateLimitCounterBackend