Skip to content

Quota Ledger

Doc Version: 1.0.0 Last Updated: 2026-09-06 Git Commit: - Author: Lincoln

Overview

Starting with v3.1.0, JAiRouter ships a quota ledger: it tracks request counts and token usage across multiple time windows (minute / hour / day / month) in the request path, providing the data foundation for future limit enforcement.

  • Off by default (opt-in): jairouter.quota.enabled defaults to false — zero behavior change. No accumulation, no database access, no Redis access; behavior is identical to v3.0.x
  • Pluggable counter backend: defaults to in-process LocalCounterBackend (LongAdder); only when distributed.enabled=true is RedisCounterBackend assembled as the cross-instance authoritative counter
  • Purely additive capability: when the ledger is disabled, all endpoints return immediately without touching the database or Redis

Quick Start

jairouter:
  quota:
    enabled: true                  # enable the quota ledger (default false)
    fail-open: true                # allow requests when the ledger fails (default true)
    flush-interval-seconds: 60     # in-memory snapshot flush interval in seconds (default 60)
    windows:                       # enabled windows (default: all four)
      - MINUTE
      - HOUR
      - DAY
      - MONTH
    retention:                     # per-window retention periods (defaults shown)
      minute: 1d
      hour: 2d
      day: 35d
      month: 13mo
    distributed:
      enabled: false               # enable Redis distributed counting (default false)
      key-prefix: "jairouter:quota"  # Redis key prefix (default jairouter:quota)
      timeout: 50ms                # per-command Redis timeout (default 50ms, clamped to 1ms~5s)
      degrade-to-local: true       # fall back to local counting when Redis is unavailable (default true)

Configuration Reference

All properties live under the jairouter.quota prefix. Key naming follows Spring relaxed binding (kebab-case in YAML, camelCase in Java).

PropertyDefaultDescription
enabledfalseMaster switch; when off, ledger reads and writes short-circuit with zero overhead
fail-opentrueWhether to allow requests when the ledger encounters an error: true = allow and mark degraded; false = reject
flush-interval-seconds60In-memory snapshot flush interval in seconds (minimum 1)
windows[MINUTE, HOUR, DAY, MONTH]Enabled window list; omitted windows produce no accounting
retention.minute1dMinute window retention period
retention.hour2dHour window retention period
retention.day35dDay window retention period
retention.month13moMonth window retention period
distributed.enabledfalseEnable Redis distributed counting; when false, counting stays entirely in-process
distributed.key-prefixjairouter:quotaDistributed counting Redis key prefix
distributed.timeout50msPer-command Redis timeout (clamped to 1ms~5s)
distributed.degrade-to-localtrueFall back to local counting when Redis is unavailable

Retention duration units: mo (month), d (day), h (hour), m (minute), s (second); values without a unit are interpreted as days, and the minimum amount is 1.

Runtime Configuration (No Restart)

The management console reads quota configuration and applies runtime changes via the following endpoints:

GET  /api/config/quota        # configuration snapshot
PUT  /api/config/quota        # runtime partial update

GET Configuration Snapshot

Returns the currently active quota configuration (including in-memory overrides) and labels which fields are hot-editable:

  • enabled / failOpen / windows — hot-editable fields, currently active runtime values
  • flushIntervalSeconds / retention / distributed — read-only display (restart-required, not modifiable via PUT)
  • backendName — current counter backend name (local or redis)
  • hotEditableFields — list of hot-editable field names (enabled / failOpen / windows)
  • restartRequiredFields — list of fields that require a restart (includes flushIntervalSeconds)

Requires the config:quota:read permission.

PUT Runtime Configuration

Partial update — only non-null fields in the request body are modified; null fields remain unchanged.

Hot-editable fields (take effect immediately, in-memory override):

FieldTypeDescription
enabledBooleanEnable or disable the quota ledger
failOpenBooleanAllow requests when the ledger fails
windowsString[]Enabled window list (e.g. ["MINUTE", "HOUR", "DAY"])

Restart-required fields (if any non-null value is sent, the request is rejected with HTTP 400 + errorCode=RESTART_REQUIRED; the message lists the specific field names):

FieldDescription
distributedEnabledEnable distributed counting
distributedKeyPrefixDistributed Redis key prefix
distributedTimeoutMsDistributed Redis command timeout (milliseconds)
distributedDegradeToLocalFall back to local when Redis is unavailable
retentionPer-window retention periods
flushIntervalSecondsSnapshot flush interval in seconds

An all-null request body results in HTTP 400 + errorCode=INVALID_REQUEST.

On success the response data has the same shape as the GET response. Requires the config:quota:write permission.

Known Limitations

  • Runtime changes are in-memory overrides and reset to the yaml values on restart or configuration refresh (same policy as response cache). For persistent changes, edit the jairouter.quota yaml.
  • flushIntervalSeconds is not hot-editable: this field is classified as restart-required, and PUT requests carrying a non-null value are rejected. The reason: the backend uses @Scheduled(fixedDelayString = "${jairouter.quota.flush-interval-seconds:60}"), which is resolved once when Spring registers the scheduled task; mutating the QuotaProperties bean field has no effect on the already-registered schedule. To change the flush interval, update the yaml and restart.
  • Setting enabled to true via hot-edit causes the ledger to start accumulating for new requests immediately; setting it to false stops accumulation, but any in-memory data will still be flushed on the next flush cycle.

Observability

GET /api/monitoring/quota/status   # runtime status
GET /api/monitoring/quota/usage    # usage query

Runtime Status

GET /api/monitoring/quota/status returns the quota ledger's runtime information:

FieldDescription
enabledWhether the ledger is enabled
backendNameCurrent counter backend name (local or redis)
degradedWhether in a degraded state
degradedReasonDegradation reason (empty string when not degraded)
failOpenCurrent fail-open setting
windowsCurrently enabled window list
distributedDistributed config object (enabled / keyPrefix / timeoutMs / degradeToLocal)
redisProbePresent only when distributed.enabled=true: reachable (bool) / status (healthy / degraded / error) / optional reason
counterMetricsPresent only when distributed.enabled=true: degradationCount (cumulative degradation count)

Requires the monitoring:quota:read permission.

Usage Query

GET /api/monitoring/quota/usage queries quota usage by dimension and window (read-only, no write side effects).

Query parameters (all optional, default to empty string):

ParameterDescription
tenantIdTenant ID
apiKeyIdAPI Key ID
userIdUser ID
serviceTypeService type (e.g. chat)
modelModel name
windowWindow type (MINUTE / HOUR / DAY / MONTH; invalid values return 400)

Response: data is an array where each entry contains:

FieldDescription
dimensionsDimension object (tenantId / apiKeyId / userId / serviceType / model)
windowWindow type
windowStartWindow start time
requestCountRequest count
tokenCountToken count

When the quota ledger is disabled, data=[] and message=配额账本未启用.

Requires the monitoring:quota:read permission.

Permission Codes

The quota feature introduces 3 new permission codes:

Permission codeDescription
config:quota:readQuota configuration status query
config:quota:writeQuota runtime configuration update
monitoring:quota:readQuota runtime status and usage query

The ADMIN role includes all codes by default; OPERATOR includes read/write codes; VIEWER includes read codes.

Limitations and Roadmap

Current version boundaries:

  • Distributed: when distributed.enabled=true, Redis is the authoritative counter and local degrades to a mirror + fallback; when distributed.enabled=false (default), counting is only precise within a single instance
  • Snapshot flushing: in-memory deltas are flushed to the database every 60 seconds by default; the @Scheduled interval is fixed at startup and cannot be changed at runtime (flushIntervalSeconds is a restart-required field)
  • Limit enforcement: the ledger provides the data foundation; actual limit enforcement (QuotaEnforcementService) endpoints and UI will be delivered in a later PR
  • Async path: the hot path uses Mono.block(timeout) to synchronously block Redis calls (default 50ms); full async integration into the Reactor chain is deferred to a later PR