Skip to content

feat: scale mode — throttle CPU/memory instead of stopping containers#908

Merged
acouvreur merged 8 commits into
mainfrom
add-resource-scaling
May 14, 2026
Merged

feat: scale mode — throttle CPU/memory instead of stopping containers#908
acouvreur merged 8 commits into
mainfrom
add-resource-scaling

Conversation

@acouvreur

@acouvreur acouvreur commented May 14, 2026

Copy link
Copy Markdown
Member

What is scale mode?

Scale mode is a new alternative to the default stop/start lifecycle. Instead of shutting down a container when a session expires, Sablier throttles its CPU and memory to a configurable idle profile. When a new session arrives the resources are restored immediately — no container restart, no cold-start latency.

It is related a bit related to #882 but it does not really implement the feature

How to enable it

Add the four new instance labels to your container/service/workload:

Label Description
sablier.idle.cpu CPU limit applied when the session expires
sablier.idle.memory Memory limit applied when the session expires
sablier.active.cpu CPU limit restored when a new session is requested
sablier.active.memory Memory limit restored when a new session is requested

You can set any subset of the four labels. A missing label means "no limit".

Docker / Podman (Compose example)

services:
  myapp:
    image: myapp:latest
    labels:
      - "sablier.enable=true"
      - "sablier.group=myapp"
      - "sablier.idle.cpu=0.1"
      - "sablier.idle.memory=64m"
      - "sablier.active.cpu=2.0"
      - "sablier.active.memory=512m"

CPU values are decimal fractions of one core (0.5 = half a core). Memory values use Docker-style suffixes (b, k, m, g).

Note: When a memory label is set, Sablier also updates MemorySwap to the same value in the same docker update call. This satisfies Docker's memswap_limit ≥ memory constraint and effectively disables swap for the container.

Docker Swarm

services:
  myapp:
    image: myapp:latest
    deploy:
      labels:
        - "sablier.enable=true"
        - "sablier.group=myapp"
        - "sablier.idle.cpu=0.1"
        - "sablier.idle.memory=64m"
        - "sablier.active.cpu=2.0"
        - "sablier.active.memory=512m"

Kubernetes (Deployment)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    sablier.enable: "true"
    sablier.group: myapp
    sablier.idle.cpu: "100m"
    sablier.idle.memory: "64Mi"
    sablier.active.cpu: "2000m"
    sablier.active.memory: "512Mi"

Kubernetes uses standard quantity notation (100m = 0.1 core, 64Mi = 64 mebibytes). Resource limit changes trigger a rolling restart — the service stays available during the transition.

Note: Proxmox LXC is not supported (uses tag-based configuration rather than key-value labels).

Try the example

cd examples/scale-mode
make up    # start Sablier + whoami
make demo  # trigger a session, wait for expiry, watch resources throttle
make down

What changed

Core

  • pkg/sablier/instance.go — new ScaleConfig / ResourceProfile types; ScaleConfigFromLabels() helper; PopulateEnabledAndGroup now populates ScaleConfig

Providers

  • Dockercontainer_scale.go (new); container_stop.go / container_start.go check for scale labels before stopping/starting
  • Docker Swarmservice_scale.go (new); service_stop.go / service_start.go updated
  • Kubernetesresource_scale.go (new, strategic-merge-patch approach); instance_stop.go / instance_start.go updated
  • Podman — inherits Docker automatically (wraps Docker provider)

Tests

  • Unit tests for CPU/memory parsers (Docker, Swarm)
  • Unit tests for Kubernetes strategic-merge-patch builder
  • Unit tests for ScaleConfigFromLabels and PopulateEnabledAndGroup
  • Integration tests (dind) for Docker scale stop/start

Docs

  • docs/configuration.md — new Scale Mode section with per-provider examples
  • examples/scale-mode/ — runnable Docker Compose example

Instead of stopping a container when a session expires, scale mode
throttles its CPU and memory to a configurable idle profile. When a
new session is requested the resources are restored to the active
profile immediately — no container restart, no cold-start latency.

Four new instance labels control the behaviour:

  sablier.idle.cpu     – CPU limit applied on session expiry
  sablier.idle.memory  – memory limit applied on session expiry
  sablier.active.cpu   – CPU limit restored on new session
  sablier.active.memory – memory limit restored on new session

Provider support
----------------
* Docker / Podman  – docker update (NanoCPUs + Memory/MemorySwap)
* Docker Swarm     – service update resource limits (NanoCPUs + MemoryBytes)
* Kubernetes       – strategic-merge-patch on Deployment / StatefulSet
  container resource limits (resource.Quantity)
* Proxmox LXC      – not supported (tag-based config, no key-value labels)

Implementation notes
--------------------
* ScaleConfig / ResourceProfile types added to pkg/sablier/instance.go;
  ScaleConfigFromLabels() reads labels in both InstanceStop and InstanceStart.
* Docker: MemorySwap is always set equal to Memory in the same update call
  to satisfy Docker's memswap >= memory constraint.
* No changes to Sablier core session/request logic; providers handle
  scale internally.

Tests
-----
* Unit tests for CPU/memory parsers (Docker, Swarm)
* Unit tests for Kubernetes strategic-merge-patch builder
* Unit tests for ScaleConfigFromLabels and PopulateEnabledAndGroup
* Integration tests (dind) for Docker scale stop/start

Docs & example
--------------
* docs/configuration.md – new Scale Mode section with per-provider examples
* examples/scale-mode/  – Docker Compose example with whoami + Sablier
@github-actions github-actions Bot added documentation Improvements or additions to documentation provider Issue related to a provider labels May 14, 2026
@github-actions

github-actions Bot commented May 14, 2026

Copy link
Copy Markdown
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ Diff between sablier and sablier                                                      │
├─────────┬─────────────────────────────────────────────┬──────────┬──────────┬─────────┤
│ PERCENT │ NAME                                        │ OLD SIZE │ NEW SIZE │ DIFF    │
├─────────┼─────────────────────────────────────────────┼──────────┼──────────┼─────────┤
│ +16.87% │ github.com/sablierapp/sablier               │ 325 kB   │ 380 kB   │ +55 kB  │
│ +0.03%  │ k8s.io/client-go                            │ 14 MB    │ 14 MB    │ +3.9 kB │
│ +1.69%  │ time                                        │ 157 kB   │ 160 kB   │ +2.7 kB │
│ +0.78%  │ go.yaml.in/yaml/v3                          │ 312 kB   │ 314 kB   │ +2.4 kB │
│ +46.44% │ github.com/docker/go-units                  │ 3.3 kB   │ 4.8 kB   │ +1.5 kB │
│ +0.13%  │ <autogenerated>                             │ 948 kB   │ 950 kB   │ +1.2 kB │
│ +0.46%  │ go.opentelemetry.io/auto/sdk                │ 89 kB    │ 90 kB    │ +409 B  │
│ +0.01%  │ runtime                                     │ 3.2 MB   │ 3.2 MB   │ +236 B  │
│ +0.51%  │ vendor/golang.org/x/net/http2/hpack         │ 35 kB    │ 35 kB    │ +178 B  │
│ +0.11%  │ golang.org/x/crypto                         │ 91 kB    │ 91 kB    │ +101 B  │
│ +0.05%  │ encoding/json                               │ 172 kB   │ 172 kB   │ +91 B   │
│ +0.04%  │ k8s.io/klog/v2                              │ 124 kB   │ 124 kB   │ +47 B   │
│ +0.11%  │ vendor/golang.org/x/net/idna                │ 22 kB    │ 22 kB    │ +25 B   │
│ +0.00%  │ k8s.io/kube-openapi                         │ 466 kB   │ 466 kB   │ +17 B   │
│ +0.00%  │ github.com/google/gnostic-models            │ 1.6 MB   │ 1.6 MB   │ +13 B   │
│ +0.00%  │ text/template                               │ 292 kB   │ 292 kB   │ +1 B    │
│ +0.00%  │ github.com/emicklei/go-restful/v3           │ 133 kB   │ 133 kB   │ +1 B    │
│ -0.01%  │ github.com/pmezard/go-difflib               │ 17 kB    │ 17 kB    │ -1 B    │
│ -0.01%  │ sort                                        │ 29 kB    │ 29 kB    │ -2 B    │
│ -0.00%  │ go.mongodb.org/mongo-driver/v2              │ 672 kB   │ 672 kB   │ -2 B    │
│ -0.00%  │ github.com/spf13/pflag                      │ 302 kB   │ 302 kB   │ -2 B    │
│ -0.00%  │ html                                        │ 136 kB   │ 136 kB   │ -2 B    │
│ -0.00%  │ k8s.io/api                                  │ 17 MB    │ 17 MB    │ -2 B    │
│ -0.00%  │ github.com/moby/moby/api                    │ 149 kB   │ 149 kB   │ -2 B    │
│ -0.00%  │ os                                          │ 210 kB   │ 210 kB   │ -2 B    │
│ -0.00%  │ k8s.io/apimachinery                         │ 1.8 MB   │ 1.8 MB   │ -3 B    │
│ -0.01%  │ unique                                      │ 34 kB    │ 34 kB    │ -4 B    │
│ -0.01%  │ github.com/prometheus/common                │ 68 kB    │ 68 kB    │ -4 B    │
│ -0.00%  │ google.golang.org/protobuf                  │ 1.7 MB   │ 1.7 MB   │ -4 B    │
│ -0.00%  │ sigs.k8s.io/structured-merge-diff/v6        │ 275 kB   │ 275 kB   │ -6 B    │
│ -0.05%  │ embed                                       │ 12 kB    │ 12 kB    │ -6 B    │
│ -0.00%  │ golang.org/x/text                           │ 162 kB   │ 162 kB   │ -7 B    │
│ -0.06%  │ github.com/spf13/afero                      │ 21 kB    │ 21 kB    │ -12 B   │
│ -0.00%  │ net                                         │ 1.7 MB   │ 1.7 MB   │ -18 B   │
│ -0.00%  │ crypto                                      │ 1.9 MB   │ 1.9 MB   │ -46 B   │
│ -0.14%  │ k8s.io/utils                                │ 32 kB    │ 32 kB    │ -46 B   │
│ -0.14%  │ vendor/golang.org/x/crypto/chacha20poly1305 │ 71 kB    │ 71 kB    │ -101 B  │
│ -0.06%  │ sigs.k8s.io/json                            │ 173 kB   │ 173 kB   │ -108 B  │
│ -0.03%  │ golang.org/x/net                            │ 789 kB   │ 789 kB   │ -219 B  │
│ -0.02%  │ github.com/quic-go/quic-go                  │ 1.3 MB   │ 1.3 MB   │ -273 B  │
│ -0.10%  │ go.opentelemetry.io/otel                    │ 407 kB   │ 407 kB   │ -415 B  │
│ -0.20%  │ go.yaml.in/yaml/v2                          │ 275 kB   │ 275 kB   │ -544 B  │
│ -0.62%  │ gopkg.in/yaml.v3                            │ 305 kB   │ 304 kB   │ -1.9 kB │
├─────────┼─────────────────────────────────────────────┼──────────┼──────────┼─────────┤
│ +19.19% │ .rodata                                     │ 2.1 MB   │ 2.5 MB   │ +409 kB │
│ +0.01%  │ .noptrdata                                  │ 451 kB   │ 451 kB   │ +32 B   │
├─────────┼─────────────────────────────────────────────┼──────────┼──────────┼─────────┤
│ +0.81%  │ sablier                                     │ 59 MB    │ 59 MB    │ +475 kB │
│         │ sablier                                     │          │          │         │
└─────────┴─────────────────────────────────────────────┴──────────┴──────────┴─────────┘

@github-actions

github-actions Bot commented May 14, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed! | 377 tests in 80.557s

View HTML Test Report

@github-actions github-actions Bot added the ci label May 14, 2026
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
5.7% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@acouvreur acouvreur merged commit d39804f into main May 14, 2026
7 of 8 checks passed
@acouvreur acouvreur deleted the add-resource-scaling branch May 14, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci documentation Improvements or additions to documentation provider Issue related to a provider

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant