Skip to content

Commit 18f1128

Browse files
authored
feat #189: introduce max_workers key in app config (#201)
* feat #189: introduce max_workers key in app config Signed-off-by: sushant-suse <[email protected]> * refactor #189: use keyword mapping and deterministic mocks for max_workers Signed-off-by: sushant-suse <[email protected]> * refactor #189: modified mock_cpu_count Signed-off-by: sushant-suse <[email protected]> --------- Signed-off-by: sushant-suse <[email protected]>
1 parent b7e5328 commit 18f1128

3 files changed

Lines changed: 88 additions & 1 deletion

File tree

src/docbuild/cli/defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
DEFAULT_APP_CONFIG = {
1313
"debug": False,
1414
"role": "production",
15+
"max_workers": "half",
1516
"paths": {
1617
"config_dir": "/etc/docbuild",
1718
"repo_dir": "/data/docserv/repos/permanent-full/",

src/docbuild/models/config/app.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
from collections.abc import Sequence # Added Sequence for internal use
44
from copy import deepcopy
5+
import os
56
from typing import Any, Literal, Self
67

7-
from pydantic import BaseModel, ConfigDict, Field, model_validator
8+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
89

910
from docbuild.config.app import (
1011
CircularReferenceError,
@@ -188,8 +189,46 @@ class AppConfig(BaseModel):
188189
description="Configuration for the application's logging system.",
189190
)
190191

192+
# Added max_workers with support for ints and descriptive strings
193+
max_workers: int | str = Field(
194+
default="half",
195+
description="Max concurrent workers. Supports integers or 'all', 'all2'/'half'.",
196+
)
197+
191198
model_config = ConfigDict(extra="allow")
192199

200+
@field_validator("max_workers")
201+
@classmethod
202+
def _resolve_worker_count(cls, v: int | str) -> int:
203+
"""Resolve keywords 'all', 'half', 'all2' into concrete integers."""
204+
cpu_count = os.cpu_count() or 1
205+
206+
keyword_map = {
207+
"all": lambda cpu: cpu,
208+
"half": lambda cpu: max(1, cpu // 2),
209+
"all2": lambda cpu: max(1, cpu // 2),
210+
}
211+
212+
if isinstance(v, str):
213+
val = v.lower()
214+
if val in keyword_map:
215+
# Return immediately once resolved
216+
return keyword_map[val](cpu_count)
217+
218+
if val.isdigit():
219+
v = int(val)
220+
else:
221+
raise ValueError(
222+
f"Invalid max_workers value: '{v}'. "
223+
"Use an integer, 'all', or 'half'/'all2'."
224+
)
225+
226+
# At this point, v is guaranteed to be an int
227+
if v < 1:
228+
raise ValueError("max_workers must be at least 1")
229+
230+
return v
231+
193232
@model_validator(mode="before")
194233
@classmethod
195234
def _resolve_placeholders(cls, data: dict[str, Any]) -> dict[str, Any] | None:
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from docbuild.models.config import app
6+
from docbuild.models.config.app import AppConfig
7+
8+
9+
# os.cpu_count() returns None if the count is indeterminate.
10+
@pytest.fixture(params=[1, 2, 8, None])
11+
def mock_cpu_count(request):
12+
"""Mock os.cpu_count in app module to ensure deterministic tests."""
13+
cpu_count = request.param
14+
with patch.object(app, "os") as mock_os:
15+
mock_os.cpu_count.return_value = cpu_count
16+
yield cpu_count
17+
18+
# Separate test cases for better clarity
19+
def test_max_workers_resolution_all(mock_cpu_count):
20+
"""Verify 'all' keyword resolves to the full CPU count (min 1)."""
21+
expected = mock_cpu_count or 1
22+
conf = AppConfig(max_workers="all")
23+
assert conf.max_workers == expected
24+
25+
def test_max_workers_resolution_half(mock_cpu_count):
26+
"""Verify 'half' and 'all2' resolve to 50% CPU count (min 1)."""
27+
cpu = mock_cpu_count or 1
28+
expected_half = max(1, cpu // 2)
29+
30+
assert AppConfig(max_workers="half").max_workers == expected_half
31+
assert AppConfig(max_workers="all2").max_workers == expected_half
32+
33+
def test_max_workers_resolution_explicit_values():
34+
"""Verify that specific integers/strings override CPU-based logic.
35+
We don't need the fixture here because these values are
36+
independent of the host hardware.
37+
"""
38+
assert AppConfig(max_workers=4).max_workers == 4
39+
assert AppConfig(max_workers="8").max_workers == 8
40+
41+
def test_max_workers_validation_errors():
42+
"""Verify error handling for invalid inputs."""
43+
with pytest.raises(ValueError, match="at least 1"):
44+
AppConfig(max_workers=0)
45+
46+
with pytest.raises(ValueError, match="Invalid max_workers"):
47+
AppConfig(max_workers="infinite")

0 commit comments

Comments
 (0)