Skip to main content

Governor Limits

Query Studio enforces per-user, per-tenant, and global limits on every Workbench query path (Quick + Deep + Expert + scheduled). The WorkbenchGovernorService reads each knob from the standard Billerang provider.application settings, which means all values are hot-reloadable without restarting WildFly.

Knob reference

Setting keyDefaultPurpose
workbench.governor.enabledtrueMaster switch. Set to false to short-circuit all checks (NOT recommended in production).
workbench.governor.maxConcurrentSyncPerUser3In-flight sync Quick / Deep queries per user.
workbench.governor.maxConcurrentAsyncPerUser3In-flight async Quick / Deep / Expert queries per user.
workbench.governor.maxQueriesPerMinutePerUser30Sliding-window rate, per-node in-memory.
workbench.governor.maxQueryDurationSyncSec30Sync statement_timeout.
workbench.governor.maxQueryDurationAsyncSec600Async job timeout.
workbench.governor.maxRowsSync1000Sync row hard cap. Quick + Deep results are silently truncated to this.
workbench.governor.maxRowsAsync1000000Async row hard cap. Forces LIMIT for Expert if the user requests more.
workbench.exports.retentionDays30Retention for files under {providerRoot}/exports/queryStudio/.
workbench.exports.cleanupEnabledtrueMaster switch on the nightly retention sweep.
workbench.templates.configDir(unset)Optional override directory for YAML system templates.

V2 candidates (not enforced today): per-tenant + global concurrency caps, Expert-specific concurrency cap, and per-day rate limit. Reliable enforcement across a multi-node cluster requires Redis / Infinispan for the rate counters and an explicit tenant column on query_execution_result for the concurrency queries. Both are tracked as V2 backlog. The per-user knobs above ARE enforced today and are sufficient for single-tenant deployments.

How concurrency is computed

Concurrency is shared-DB correct: the count comes from a single SQL query against query_execution_result filtered on endDate IS NULL and auditable.creator = :user (or tenant). This means a user who hits their per-user cap on node A can't sneak past it on node B — the count is global.

SELECT COUNT(qer)
FROM QueryExecutionResult qer
WHERE qer.endDate IS NULL
AND qer.auditable.creator = :user

How rate is computed

Rate is per-node in-memory for simplicity. The bucket counter resets every 60 s for per-minute and every 24 h for per-day. In a multi-node cluster the per-tenant rate may diverge slightly across nodes — this is an accepted soft-cap behaviour.

For a strict global rate, run the rate counter through Redis or Infinispan; that's a V2 change and is gated on someone needing it.

What happens when a limit hits

The governor throws GovernorViolationException. The resource layer maps it to:

  • HTTP 429 Too Many Requests
  • Retry-After: <seconds> header
  • Body:
    {
    "code": "GOVERNOR_LIMIT_EXCEEDED",
    "limit": "maxConcurrentAsyncPerUser",
    "current": 3,
    "max": 3,
    "retryAfterSeconds": 30,
    "message": "Async query concurrency limit reached (current=3, max=3)"
    }

The frontend's GovernorViolationToast reads this body and shows an amber toast with the specific limit, current / max counts, and the retry-after countdown. It auto-dismisses after the retry-after window or on click.

Tuning guidance

Single-tenant deployment

The defaults are conservative for a single-tenant deployment with a few power users:

  • Bump maxConcurrentSyncPerUser to 10 if the team works fast and the DB has headroom.
  • Bump maxRowsSync to 5000 if exports under 5 k rows are routine.
  • Drop maxQueriesPerMinutePerUser to 60 if you see legitimate "too many queries" 429s.

Multi-tenant SaaS

The per-user knobs are not sufficient for multi-tenant clusters because a single tenant with many users could starve the cluster. Multi-tenant guardrails (per-tenant / global / Expert-specific concurrency, per-day rate) are V2 backlog. Until they ship, the recommendation for multi-tenant deployments is:

  • Withhold the queryExpert role entirely (the trust model covers why).
  • Tighten maxConcurrentSyncPerUser and maxConcurrentAsyncPerUser proportional to your DB connection pool — start with 1 per user if you have many tenants.
  • Lower maxRowsSync and maxRowsAsync to constrain blast radius.
  • Lower maxQueryDurationSyncSec and maxQueryDurationAsyncSec so any one query frees its connection sooner.

Cluster with read-replicas

If you provision a read-only replica via the optional MeveoAdminReadOnly persistence unit (see WorkbenchEntityManagerProducer.java), Quick + Deep + Expert reads route there automatically. Then:

  • The sync timeout can be longer (maxQueryDurationSyncSec=60) — the replica isn't on the user-facing transaction path.
  • The async timeout can be much longer (maxQueryDurationAsyncSec=3600) — long analytical queries no longer compete with billing transactions.

Per-tenant override

Standard Billerang settings support per-provider override. To override maxRowsSync for tenant pluxee_be only, set pluxee_be.workbench.governor.maxRowsSync=5000 in the per-provider settings store.

Disabling the governor

Setting workbench.governor.enabled=false short-circuits all assertCanRun* calls. Do not do this in production — there's no upper bound on the database load Query Studio can generate. The flag exists for diagnostic purposes during incident response only.