Skip to content

feat: durable per-service Docker resource limits#95

Merged
GeiserX merged 2 commits into
mainfrom
feat/service-resource-limits
Jul 5, 2026
Merged

feat: durable per-service Docker resource limits#95
GeiserX merged 2 commits into
mainfrom
feat/service-resource-limits

Conversation

@GeiserX

@GeiserX GeiserX commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What

Adds optional per-service Docker resource limits declared in the service YAML (the single source of truth) and threads them through the whole deploy chain down to containers.run(). This makes currently-ephemeral cgroup limits durable across container recreates.

Today the worker's deploy_raw calls containers.run(...) with no memory limit or oom_score_adj, so earner containers have no durable limits — the production ones were set out-of-band and revert whenever a container is recreated. Part of the 2026-07-05 fleet OOM remediation.

How it flows

services/<svc>.yml (docker.resources) → UI deploy route builds spec["resources"] → worker DeploySpec.resources (validated) → deploy_raw(resources=...)containers.run(mem_limit=..., oom_score_adj=...).

  • services/_schema.yml — documents the optional docker.resources block (mem_limit, optional mem_reservation, oom_score_adj).
  • worker_api.py — new ResourceSpec model on DeploySpec; _validate_deploy_spec rejects a bad mem_limit/mem_reservation (must match ^\d+[bkmgBKMG]?$) or an oom_score_adj outside [-1000, 1000] with a 400.
  • orchestrator.deploy_raw — new resources param, normalized to a dict; mem_limit / mem_reservation / oom_score_adj are passed to containers.run() only when set (Docker defaults preserved otherwise). memory-swap is deliberately left unset — at create time mem_limit alone avoids the cgroup-v2 swap-validation issue.
  • UI deploy route (main.py) — reads resources from the service YAML and includes it in the spec only when present.

docker-py 7.1.0 (pinned) supports all three kwargs directly on the high-level containers.run() (they're in RUN_HOST_CONFIG_KWARGS / HostConfig).

Right-sizing rationale

mem_limit is a ceiling, so it is safe on every host (the RAM-constrained host also has earlyoom as a backstop). oom_score_adj biases the kernel OOM killer: negative = sacrificed last, positive = sacrificed first.

Service mem_limit oom_score_adj Why
storj 2g -100 revenue + node reputation critical
anyone-protocol 1536m -100 relay reputation critical
mysterium 768m -100 revenue + node identity critical
honeygain 256m 200 expendable earner
earnfm, bitping 128m 200 expendable earner
proxyrack, earnapp 256m 300 expendable, sacrifice first
iproyal, packetstream, proxylite, repocket, traffmonetizer, urnetwork, proxybase 128m 300 expendable, sacrifice first

Revenue/reputation-critical services are protected with a negative oom_score_adj; expendable earners get a positive one so the kernel kills them first under pressure.

Tests

  • tests/test_worker_resources.py (new): ResourceSpec/DeploySpec validation (valid + invalid mem_limit, mem_reservation, oom_score_adj), deploy_rawcontainers.run() kwargs (pydantic + dict inputs, None dropped, omitted when unset), and the worker /deploy endpoint threading resources through (incl. 400s on invalid input).
  • tests/test_main_deploy_routes.py: UI route forwards YAML resources into the spec, omits when absent, and a real-catalog storj guard against YAML key renames.

ruff check . clean; full pytest green (995 passed).

Notes

  • Surgical: no service logic hardcoded in app/; YAML stays the source of truth.
  • Compose export (compose_generator.py) is intentionally untouched — this PR scopes to the worker deploy path.
  • Please do not merge yet — CI + CodeRabbit will run.

Summary by CodeRabbit

  • New Features

    • Added support for optional container resource limits during deployment, including memory limits, memory reservations, and OOM score adjustments.
    • Updated several service configurations to include predefined resource settings for more controlled runtime behavior.
  • Bug Fixes

    • Deployment now preserves and forwards configured resource settings instead of dropping them.
    • Invalid resource values are now rejected up front, helping prevent failed or misconfigured deploys.

Declare optional mem_limit / mem_reservation / oom_score_adj per service in
the YAML (the single source of truth) and thread them through the deploy
chain to containers.run(), so earner containers keep durable cgroup limits
across recreates instead of relying on out-of-band settings that revert.

- services/_schema.yml: document the optional docker.resources block
- 15 service YAMLs: right-sized mem_limit + oom_score_adj. Revenue/reputation
  -critical services (storj, mysterium, anyone-protocol) protected with
  negative oom_score_adj; expendable earners positive so the kernel sacrifices
  them first.
- worker_api: ResourceSpec on DeploySpec + validation (mem size regex,
  oom_score_adj in [-1000,1000]); forwarded to deploy_raw.
- orchestrator.deploy_raw: normalize resources and pass mem_limit /
  mem_reservation / oom_score_adj to containers.run() only when set (memswap
  left unset to avoid the cgroup-v2 swap-validation issue at create time).
- main deploy route: forward YAML resources into the worker spec when present.
- tests: spec->deploy_raw->containers.run flow + DeploySpec validation.

Part of the 2026-07-05 fleet OOM remediation.
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@GeiserX, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 350abd1c-fc50-4378-9f0d-f5cdce7baee8

📥 Commits

Reviewing files that changed from the base of the PR and between b2225ae and 5c5b711.

📒 Files selected for processing (1)
  • tests/test_main_deploy_routes.py
📝 Walkthrough

Walkthrough

This PR adds optional Docker container resource limits (mem_limit, mem_reservation, oom_score_adj) that flow from service YAML definitions through the deploy endpoint, worker API validation, and orchestrator container recreation, with schema documentation, per-service configuration, and new tests.

Changes

Docker resource limits pipeline

Layer / File(s) Summary
ResourceSpec model and validation
app/worker_api.py
Adds ResourceSpec model, extends DeploySpec with resources, and validates mem_limit/mem_reservation regex format and oom_score_adj range, raising HTTP 400 on invalid input.
Orchestrator normalization and container run kwargs
app/orchestrator.py
Adds _normalize_resources helper and updates deploy_raw to build resource_kwargs and forward them into containers.run() only when explicitly set.
Deploy endpoint forwarding
app/main.py
api_deploy reads docker_conf["resources"] and includes it in the spec sent to the worker.
Schema docs and per-service resource configs
services/_schema.yml, services/bandwidth/*.yml, services/depin/anyone-protocol.yml, services/storage/storj.yml
Documents the optional resources schema and sets mem_limit/oom_score_adj values for each service.
Tests
tests/test_main_deploy_routes.py, tests/test_worker_resources.py
Adds tests covering deploy-route resource forwarding, ResourceSpec validation, deploy_raw kwarg building, and worker deploy endpoint behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Related issues: None specified.

Related PRs: None specified.

Suggested labels: enhancement, worker-api, orchestrator, config

Suggested reviewers: GeiserX

Poem

A rabbit hops through YAML fields so neat,
Capping memory so containers don't overeat.
"mem_limit," "oom_score," a tidy little spec,
Validated, forwarded, no more OOM wreck.
Hop, deploy, and rest — resources now in check! 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: durable per-service Docker resource limits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/service-resource-limits

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.09%. Comparing base (ed31c64) to head (5c5b711).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #95      +/-   ##
==========================================
+ Coverage   91.59%   92.09%   +0.49%     
==========================================
  Files          30       30              
  Lines        3082     3085       +3     
==========================================
+ Hits         2823     2841      +18     
+ Misses        259      244      -15     
Files with missing lines Coverage Δ
app/main.py 92.70% <100.00%> (+1.02%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
tests/test_main_deploy_routes.py (1)

183-277: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tests are correct and consistent with app/main.py's resources-forwarding logic.

Minor DRY nit: the captured = {} / _capture_deploy boilerplate is now duplicated across four tests in this class (including test_deploy_repocket_emits_rp_env_keys). Consider extracting a small fixture/helper to reduce repetition.

♻️ Optional fixture extraction
`@pytest.fixture`
def capture_deploy():
    captured: dict = {}

    async def _capture(worker_id, slug, spec):
        captured["spec"] = spec
        return {"container_id": "abc123"}

    return captured, _capture
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_main_deploy_routes.py` around lines 183 - 277, The deploy tests
repeat the same captured-state and async deploy stub setup across multiple
methods, including `_capture_deploy` and the `captured` dict. Extract that
boilerplate into a small shared fixture or helper in the test class (for
example, a capture fixture returning the dict and callback), then update the
tests like `test_deploy_forwards_resources_from_yaml`,
`test_deploy_omits_resources_when_absent`, and
`test_deploy_storj_real_catalog_carries_resources` to use it consistently.
app/orchestrator.py (1)

170-177: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant re-check of None in resource_kwargs.

_normalize_resources already drops None values (line 119), so res.get(key) is not None is always true for keys present in res. Harmless, just a minor readability nit.

♻️ Simplify
     res = _normalize_resources(resources)
-    resource_kwargs = {
-        key: res[key] for key in ("mem_limit", "mem_reservation", "oom_score_adj") if res.get(key) is not None
-    }
+    resource_kwargs = {key: res[key] for key in ("mem_limit", "mem_reservation", "oom_score_adj") if key in res}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/orchestrator.py` around lines 170 - 177, resource_kwargs in the
orchestration resource setup is redundantly re-checking for None even though
_normalize_resources already removes null values. Simplify the dict
comprehension in the resource handling block to rely on the presence of keys
from res (using the existing _normalize_resources helper and resource_kwargs
construction) instead of testing res.get(key) is not None, while keeping the
same mem_limit, mem_reservation, and oom_score_adj behavior.
app/worker_api.py (2)

262-273: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

ResourceSpec silently swallows typo'd/unknown keys.

Pydantic v2 defaults extra='ignore'. A typo in the service YAML resources block (e.g. oom_score_adjust instead of oom_score_adj) would pass validation silently, with the intended limit never applied and no error surfaced anywhere in the deploy pipeline.

🛡️ Proposed fix: forbid unexpected keys
 class ResourceSpec(BaseModel):
     """Optional Docker resource limits applied when the container is created.

     mem_limit / mem_reservation follow Docker's size syntax ("768m", "2g");
     oom_score_adj biases the kernel OOM killer (-1000 = sacrificed last).
     """

+    model_config = ConfigDict(extra="forbid")
+
     mem_limit: str | None = None
     mem_reservation: str | None = None
     oom_score_adj: int | None = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/worker_api.py` around lines 262 - 273, ResourceSpec currently accepts
unknown fields silently because BaseModel defaults to ignoring extras, so typos
in the service YAML resources block can be missed. Update the ResourceSpec model
to reject unexpected keys by configuring Pydantic to forbid extra fields, and
keep the validation behavior centered on ResourceSpec so deploy-time schema
mistakes surface immediately instead of being ignored.

311-324: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No cross-field validation that mem_reservation < mem_limit.

Docker requires the soft limit to be lower than the hard limit; the daemon rejects (or behaves unexpectedly for) a reservation that meets or exceeds the limit. Since both fields pass independent regex checks here, a service YAML that sets mem_reservation >= mem_limit would clear worker validation but fail at containers.run(), surfacing as a generic 500 instead of the clearer 400 this validator is meant to provide. None of the current service YAMLs in this PR set mem_reservation, so this is latent rather than actively triggered.

🛡️ Proposed fix
     if resources.oom_score_adj is not None and not (-1000 <= resources.oom_score_adj <= 1000):
         raise HTTPException(
             status_code=400,
             detail=f"Invalid oom_score_adj '{resources.oom_score_adj}': must be between -1000 and 1000",
         )
+    if (
+        resources.mem_limit is not None
+        and resources.mem_reservation is not None
+        and _parse_mem(resources.mem_reservation) >= _parse_mem(resources.mem_limit)
+    ):
+        raise HTTPException(
+            status_code=400,
+            detail="mem_reservation must be lower than mem_limit",
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/worker_api.py` around lines 311 - 324, The _validate_resources validation
only checks mem_limit and mem_reservation format independently, so add a
cross-field check in _validate_resources to ensure mem_reservation is strictly
lower than mem_limit before containers.run() is called. Use the existing
ResourceSpec fields and the _MEM_LIMIT_RE parsing logic to compare the two
values after validation, and raise HTTPException with a 400 status when the
reservation is equal to or greater than the limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/orchestrator.py`:
- Around line 170-177: resource_kwargs in the orchestration resource setup is
redundantly re-checking for None even though _normalize_resources already
removes null values. Simplify the dict comprehension in the resource handling
block to rely on the presence of keys from res (using the existing
_normalize_resources helper and resource_kwargs construction) instead of testing
res.get(key) is not None, while keeping the same mem_limit, mem_reservation, and
oom_score_adj behavior.

In `@app/worker_api.py`:
- Around line 262-273: ResourceSpec currently accepts unknown fields silently
because BaseModel defaults to ignoring extras, so typos in the service YAML
resources block can be missed. Update the ResourceSpec model to reject
unexpected keys by configuring Pydantic to forbid extra fields, and keep the
validation behavior centered on ResourceSpec so deploy-time schema mistakes
surface immediately instead of being ignored.
- Around line 311-324: The _validate_resources validation only checks mem_limit
and mem_reservation format independently, so add a cross-field check in
_validate_resources to ensure mem_reservation is strictly lower than mem_limit
before containers.run() is called. Use the existing ResourceSpec fields and the
_MEM_LIMIT_RE parsing logic to compare the two values after validation, and
raise HTTPException with a 400 status when the reservation is equal to or
greater than the limit.

In `@tests/test_main_deploy_routes.py`:
- Around line 183-277: The deploy tests repeat the same captured-state and async
deploy stub setup across multiple methods, including `_capture_deploy` and the
`captured` dict. Extract that boilerplate into a small shared fixture or helper
in the test class (for example, a capture fixture returning the dict and
callback), then update the tests like
`test_deploy_forwards_resources_from_yaml`,
`test_deploy_omits_resources_when_absent`, and
`test_deploy_storj_real_catalog_carries_resources` to use it consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f628b6b8-f050-4611-a3b6-6b9a7b2cd633

📥 Commits

Reviewing files that changed from the base of the PR and between 059d0be and b2225ae.

📒 Files selected for processing (21)
  • app/main.py
  • app/orchestrator.py
  • app/worker_api.py
  • services/_schema.yml
  • services/bandwidth/bitping.yml
  • services/bandwidth/earnapp.yml
  • services/bandwidth/earnfm.yml
  • services/bandwidth/honeygain.yml
  • services/bandwidth/iproyal.yml
  • services/bandwidth/mysterium.yml
  • services/bandwidth/packetstream.yml
  • services/bandwidth/proxybase.yml
  • services/bandwidth/proxylite.yml
  • services/bandwidth/proxyrack.yml
  • services/bandwidth/repocket.yml
  • services/bandwidth/traffmonetizer.yml
  • services/bandwidth/urnetwork.yml
  • services/depin/anyone-protocol.yml
  • services/storage/storj.yml
  • tests/test_main_deploy_routes.py
  • tests/test_worker_resources.py

Replace the duplicated captured / _capture_deploy boilerplate across the
api_deploy tests with a shared `deploy_capture` pytest fixture. No behavior
change. Addresses the CodeRabbit DRY nit on PR #95.
@GeiserX GeiserX merged commit d0fc13b into main Jul 5, 2026
8 checks passed
@GeiserX GeiserX deleted the feat/service-resource-limits branch July 5, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant