Skip to content

Commit c38ee93

Browse files
committed
Remove SyncWorker, make ThreadWorker the only worker type
ThreadWorker with threads=1 behaves nearly identically to SyncWorker, so there's no need for two worker implementations. This also updates the CLI defaults to --workers auto --threads 4 and validates that threads >= 1.
1 parent cb5353b commit c38ee93

File tree

6 files changed

+30
-272
lines changed

6 files changed

+30
-272
lines changed

plain-dev/plain/dev/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,8 @@ def add_server(self) -> None:
338338
"'[%(levelname)s] %(message)s'",
339339
"--access-log-format",
340340
"'\"%(r)s\" status=%(s)s length=%(b)s time=%(M)sms'",
341+
"--workers",
342+
"1",
341343
"--reload", # Enable auto-reload for development
342344
]
343345

plain/plain/cli/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@ def parse_workers(ctx: click.Context, param: click.Parameter, value: str) -> int
2323
)
2424
@click.option(
2525
"--threads",
26-
type=int,
27-
default=1,
26+
type=click.IntRange(min=1),
27+
default=4,
2828
help="Number of threads per worker",
2929
show_default=True,
3030
)
3131
@click.option(
3232
"--workers",
3333
"-w",
3434
type=str,
35-
default="1",
35+
default="auto",
3636
envvar="WEB_CONCURRENCY",
3737
callback=parse_workers,
3838
help="Number of worker processes (or 'auto' for CPU count)",

plain/plain/server/README.md

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
**A production-ready WSGI HTTP server based on gunicorn.**
44

55
- [Overview](#overview)
6-
- [Worker types](#worker-types)
6+
- [Workers and threads](#workers-and-threads)
77
- [Configuration options](#configuration-options)
88
- [Environment variables](#environment-variables)
99
- [Signals](#signals)
@@ -19,44 +19,37 @@ You can run the built-in HTTP server with the `plain server` command.
1919
plain server
2020
```
2121

22-
By default, the server binds to `127.0.0.1:8000` and uses a single worker process. In production, you will typically want to increase the number of workers and optionally enable threading.
23-
24-
```bash
25-
# Run with 4 worker processes
26-
plain server --workers 4
27-
28-
# Auto-detect based on available CPUs
29-
plain server --workers auto
30-
31-
# Run with 2 workers and 4 threads each
32-
plain server --workers 2 --threads 4
33-
```
22+
By default, the server binds to `127.0.0.1:8000` with one worker process per CPU core and 4 threads per worker.
3423

3524
For local development, you can enable auto-reload to restart workers when code changes.
3625

3726
```bash
3827
plain server --reload
3928
```
4029

41-
## Worker types
30+
## Workers and threads
4231

43-
The server automatically selects the worker type based on your configuration.
32+
The server uses two levels of concurrency:
4433

45-
**Sync workers** handle one request at a time per worker. These are simple and predictable.
34+
- **Workers** are separate OS processes. Each worker runs independently with its own memory. The default is `auto`, which spawns one worker per CPU core.
35+
- **Threads** run inside each worker. Threads share memory within a worker and handle concurrent requests using a thread pool. The default is 4 threads per worker.
4636

47-
```bash
48-
# Single-threaded (uses sync worker)
49-
plain server --workers 4
50-
```
37+
Total concurrent requests = `workers × threads`. On a 4-core machine with the defaults, that's `4 × 4 = 16` concurrent requests.
38+
39+
**When to adjust workers:** Workers provide true parallelism since each is a separate process with its own Python GIL. More workers means more memory usage but better CPU utilization. Use `--workers auto` (the default) to match your CPU cores, or set an explicit number.
5140

52-
**Threaded workers** handle multiple concurrent requests per worker using a thread pool. These are useful when your application does blocking I/O.
41+
**When to adjust threads:** Threads are efficient for I/O-bound work (database queries, external API calls) since they release the GIL while waiting. Most web applications are I/O-bound, so the default of 4 threads works well. Increase threads if your application spends a lot of time waiting on I/O. Decrease to 1 if you need to avoid thread-safety concerns.
5342

5443
```bash
55-
# Multi-threaded (uses threaded worker)
56-
plain server --workers 2 --threads 8
57-
```
44+
# Explicit worker count
45+
plain server --workers 2
5846

59-
For advanced worker customization, see the [`SyncWorker`](./workers/sync.py#SyncWorker) and [`ThreadWorker`](./workers/thread.py#ThreadWorker) classes.
47+
# More threads for I/O-heavy apps
48+
plain server --threads 8
49+
50+
# Single-threaded workers (simplest, one request at a time per worker)
51+
plain server --threads 1
52+
```
6053

6154
## Configuration options
6255

@@ -65,8 +58,8 @@ All options are available via the command line. Run `plain server --help` to see
6558
| Option | Default | Description |
6659
| ------------------ | ---------------- | ----------------------------------------------------- |
6760
| `--bind` / `-b` | `127.0.0.1:8000` | Address to bind (can be used multiple times) |
68-
| `--workers` / `-w` | `1` | Number of worker processes (or `auto` for CPU count) |
69-
| `--threads` | `1` | Number of threads per worker |
61+
| `--workers` / `-w` | `auto` | Number of worker processes (or `auto` for CPU count) |
62+
| `--threads` | `4` | Number of threads per worker |
7063
| `--timeout` / `-t` | `30` | Worker timeout in seconds |
7164
| `--reload` | `False` | Restart workers when code changes |
7265
| `--certfile` | - | Path to SSL certificate file |

plain/plain/server/arbiter.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
from . import sock, util
2323
from .errors import AppImportError, HaltServer
2424
from .pidfile import Pidfile
25+
from .workers.thread import ThreadWorker
2526

2627
if TYPE_CHECKING:
2728
from .app import ServerApplication
2829
from .config import Config
2930
from .glogging import Logger
30-
from .workers.base import Worker
3131

3232

3333
class Arbiter:
@@ -48,7 +48,7 @@ class Arbiter:
4848
START_CTX: dict[int | str, Any] = {}
4949

5050
LISTENERS: list[sock.BaseSocket] = []
51-
WORKERS: dict[int, Worker] = {}
51+
WORKERS: dict[int, ThreadWorker] = {}
5252
PIPE: list[int] = []
5353

5454
# I love dynamic languages
@@ -101,7 +101,6 @@ def setup(self, app: ServerApplication) -> None:
101101

102102
self.log: Logger = Logger(self.cfg)
103103

104-
self.worker_class: type[Worker] = self.cfg.worker_class
105104
self.address: str = self.cfg.address
106105
self.num_workers = self.cfg.workers
107106
self.timeout: int = self.cfg.timeout
@@ -122,16 +121,13 @@ def start(self) -> None:
122121

123122
listeners_str = ",".join([str(lnr) for lnr in self.LISTENERS])
124123
self.log.info(
125-
"Plain server started address=%s pid=%s worker=%s version=%s",
124+
"Plain server started address=%s pid=%s version=%s",
126125
listeners_str,
127126
self.pid,
128-
self.cfg.worker_class_str,
129127
plain.runtime.__version__,
130128
)
131129

132-
# check worker class requirements
133-
if check_config := getattr(self.worker_class, "check_config", None):
134-
check_config(self.cfg, self.log)
130+
ThreadWorker.check_config(self.cfg, self.log)
135131

136132
def init_signals(self) -> None:
137133
"""\
@@ -468,7 +464,7 @@ def manage_workers(self) -> None:
468464

469465
def spawn_worker(self) -> int:
470466
self.worker_age += 1
471-
worker = self.worker_class(
467+
worker = ThreadWorker(
472468
self.worker_age,
473469
self.pid,
474470
self.LISTENERS,

plain/plain/server/config.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
from dataclasses import dataclass
1111

1212
from . import util
13-
from .workers.sync import SyncWorker
14-
from .workers.thread import ThreadWorker
1513

1614

1715
@dataclass
@@ -38,25 +36,6 @@ class Config:
3836
log_format: str
3937
access_log_format: str
4038

41-
@property
42-
def worker_class_str(self) -> str:
43-
# Auto-select based on threads
44-
if self.threads > 1:
45-
return "thread"
46-
return "sync"
47-
48-
@property
49-
def worker_class(self) -> type:
50-
# Auto-select based on threads
51-
if self.threads > 1:
52-
worker_class = ThreadWorker
53-
else:
54-
worker_class = SyncWorker
55-
56-
if hasattr(worker_class, "setup"):
57-
worker_class.setup()
58-
return worker_class
59-
6039
@property
6140
def address(self) -> list[tuple[str, int] | str]:
6241
return [util.parse_address(util.bytes_to_str(bind)) for bind in self.bind]

0 commit comments

Comments
 (0)