Quota module
The quota module enforces per-tenant (and optionally per-env,
per-stack) limits on compute, storage, and object counts, plus the
usage bookkeeping that backs those checks.
Source: modules/quota/ + the enforcement hooks called from
install_flow and update_flow.
Responsibilities
Section titled “Responsibilities”- Read limits from config, scoped to
(tenant, env?, stack?). - Track current usage in the
usagestable (UsageModel), one row per scope. - Enforce pre-flight on stack create, scale, and upgrade — reject if delta would exceed the limit.
- Reconcile periodically so drift between the DB counters and reality (containers killed by the kernel, volumes deleted out-of-band) heals automatically.
- Enforce node/pool capacity separately — a stack that fits in tenant quota but not on any node still gets rejected.
- Expose CLI/API to operators to set limits, view usage, and force re-scans.
Limit types
Section titled “Limit types”QuotaLimits (types.go):
| Field | Unit | What it caps |
|---|---|---|
cpu_millicores | millicores | Sum of deploy.resources.limits.cpus * replicas |
memory_bytes | bytes | Sum of deploy.resources.limits.memory * replicas |
disk_bytes | bytes | Volume + backup storage footprint |
max_stacks | count | Top-level stacks (not vApp instances) |
max_environments | count | Distinct environment values in use |
max_apps | count | Stacks + vApp instances |
max_vapp_instances | count | vApp instances only |
All limits are None-able — a None limit is “unenforced”. SHC
ships with no default quotas; nothing is limited until an
operator writes a quota: block.
Scopes
Section titled “Scopes”Quota checks traverse three scopes, most-specific first. A violation at any scope rejects the operation:
tenant ← always checked └── tenant/env ← checked if env is set on ctx └── tenant/env/stack ← checked if stack is set on ctxUsage rows (UsageModel) are keyed by (tenant_name, environment, stack_name) with NULL environment/stack meaning “tenant roll-up”
and NULL stack only meaning “tenant/env roll-up”. The unique
constraint uq_usage_scope prevents duplicates.
The composite key scope_name renders as:
acme— tenant onlyacme/env:staging— tenant + envacme/blog— tenant + stackacme/blog/default— tenant + stack + env
Resolution of limits
Section titled “Resolution of limits”QuotaService.get_limits(ctx) reads config.get("quota") — i.e.
the resolved config at the ctx’s scope, which lets operators set:
# global default (/etc/shc/config.yaml)quota: cpu: "4" memory: "8g" max_stacks: 10…and then override per tenant via shc config set … --scope tenant.
Config resolution is documented in
reference/configuration.
Multipliers on top of that let an app or an operator widen the reservation ↔ limit gap:
quota.reservation_multiplier(default1.0) — how much composereservationscount against quotaquota.limit_multiplier(default1.5) — allowance for over-subscription againstlimits
Resolution order is CLI flag > app.yaml > config > built-in defaults
(get_multipliers).
Parse grammar (CPU + bytes)
Section titled “Parse grammar (CPU + bytes)”modules/quota/state.go (ParseCPU / ParseBytes):
CPU cores or millicores "0.5" → 500 "1" → 1000 "2500m" → 2500
Bytes IEC-ish suffixes (case-insensitive, no decimals) "512m" → 536_870_912 "8g" → 8_589_934_592 "1t" → 1_099_511_627_776Invalid input raises X400126BYTVAL or X400127CPUVAL.
Pre-flight enforcement
Section titled “Pre-flight enforcement”Callers use quota_service.enforce_quota(db, ctx, delta), which:
- Computes
new = current_usage + deltaper dimension. - Walks each limit; violations are collected as
QuotaCheckResult.violationsstrings. - If any non-empty, raises the first specific error code
(
X403001CPUEXC,X403002MEMEXC,X403003DSKEXC,X403004STKEXC,X403005ENVEXC,X403006APPEXC,X403007VAPEXC).
The helpers in enforcement.go package this up for stack lifecycle
callers:
| Helper | Used by |
|---|---|
enforce_stack_creation_quota | install_flow before compose render |
update_usage_on_stack_creation | install_flow after stack row inserted |
update_usage_on_stack_deletion | uninstall_flow after compose down |
update_usage_on_scale | shc stack scale / replica change in update |
Each of these derives (cpu, memory, disk) from the rendered
compose (extract_resources_from_compose) so quota uses the exact
values Docker is asked to honor, not just app-declared hints.
Node & swarm capacity checks
Section titled “Node & swarm capacity checks”Tenant quota is necessary but not sufficient — the stack also has
to fit somewhere. enforcement.go adds two physical checks:
enforce_node_capacity(node, cpu_mc, mem_b)— rejects withX403010NODCPU/X403011NODMEMif the single-node reservation exceeds the node’s knownresources.cpu_cores/resources.memory_bytes. Nodes without advertised capacity are ignored (quota only, no physical gate).enforce_swarm_pool_capacity(members, cpu_total, mem_total)— for swarm runtimes, rejects withX403012SWMPOLif the pool can’t hold the full stack, and withX403013SWMREPif any single replica wouldn’t fit on the largest eligible node.
These are computed against NodeModel.resources, which is populated
by the node-agent heartbeat — see node.
Usage tracking
Section titled “Usage tracking”Every delta goes through quota_service.update_usage(...), which
upserts into UsageModel at all three scopes:
- tenant row (always)
- tenant/env row (if env given)
- tenant/stack row (if stack given)
- tenant/stack/env row (if both)
Adds and subtracts are idempotent from the caller’s perspective; negatives on delete are symmetric.
Disk usage is separate because filesystem introspection is slow:
set_disk_usage(...) writes the measurement + disk_measured_at
timestamp directly, bypassing the delta path. See below.
Disk measurement strategy
Section titled “Disk measurement strategy”modules/quota/disk.go — DiskMeasurement picks the cheapest
accurate method for the underlying filesystem:
- xfs with project quotas →
xfs_quotareport (O(1)) - zfs →
zfs list -o used(O(1)) - btrfs with qgroups →
btrfs qgroup show(O(1)) - anything else → shell
du -sbwith a short timeout, falling back to a Go-based walker ifduis missing or times out
The measurement result is cached by path inside DiskMeasurement
for the lifetime of a single scan to avoid re-walking the same
tree for overlapping scopes (tenant ⊇ tenant/env ⊇ tenant/stack).
Background reconciliation
Section titled “Background reconciliation”Two cron jobs in tasks.go run on the leader:
scan_all_disk_usage— everymaintenance_disk_scan_interval(see configuration — maintenance). Walks every(tenant, env, stack)scope and re-measures disk. Heartbeats viarecord_heartbeat("disk_scan")soshc doctorcan flag staleness.reconcile_all_usage— everymaintenance_usage_reconcile_interval. Recomputesstack_count,vapp_instance_count,environment_count, andapp_countdirectly fromDeploymentModel. Protects against drift from crashed deploys that bypassed normal lifecycle hooks.
Neither job ever raises from the scheduler: failures are logged
as X500909DSKSCA / X500913USGREC and the next tick retries.
Errors
Section titled “Errors”| Code | Raised when |
|---|---|
X403001CPUEXC | tenant/env/stack CPU limit exceeded |
X403002MEMEXC | memory limit exceeded |
X403003DSKEXC | disk limit exceeded |
X403004STKEXC | stack count limit exceeded |
X403005ENVEXC | environment count limit exceeded |
X403006APPEXC | app count limit exceeded |
X403007VAPEXC | vApp instance count limit exceeded |
X403010NODCPU | single-node reservation larger than any node’s CPU |
X403011NODMEM | single-node reservation larger than any node’s memory |
X403012SWMPOL | swarm pool cannot hold the stack |
X403013SWMREP | swarm largest replica doesn’t fit on the largest eligible node |
X400126BYTVAL | unparseable byte string |
X400127CPUVAL | unparseable CPU string |
X500909DSKSCA | background disk scan failed |
X500913USGREC | usage reconcile failed |
CLI / API surface
Section titled “CLI / API surface”- CLI:
shc quota {set,unset,status,scan}— see reference/cli/quota - HTTP:
modules/quota/router.goexposes CRUD at/api/v1/quota/...— see reference/api/quota
Related
Section titled “Related”- Flow: install — where
enforce_stack_creation_quotafires - Flow: upgrade — where
update_usage_on_scalefires - Tenant — the primary quota scope
- Node — source of node/pool capacity data
- Health — consumes
record_heartbeatemitted here