uhttp-workers 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uhttp_workers-1.0.0/.github/workflows/publish.yml +35 -0
- uhttp_workers-1.0.0/.github/workflows/tests.yml +28 -0
- uhttp_workers-1.0.0/.gitignore +8 -0
- uhttp_workers-1.0.0/PKG-INFO +453 -0
- uhttp_workers-1.0.0/README.md +439 -0
- uhttp_workers-1.0.0/examples/simple_workers.py +147 -0
- uhttp_workers-1.0.0/examples/static/index.html +65 -0
- uhttp_workers-1.0.0/pyproject.toml +31 -0
- uhttp_workers-1.0.0/setup.cfg +4 -0
- uhttp_workers-1.0.0/tests/__init__.py +0 -0
- uhttp_workers-1.0.0/tests/test_api_handler.py +178 -0
- uhttp_workers-1.0.0/tests/test_decorators.py +166 -0
- uhttp_workers-1.0.0/tests/test_dispatcher.py +417 -0
- uhttp_workers-1.0.0/tests/test_pattern_matching.py +153 -0
- uhttp_workers-1.0.0/tests/test_request_response.py +126 -0
- uhttp_workers-1.0.0/tests/test_worker.py +230 -0
- uhttp_workers-1.0.0/tests/test_worker_pool.py +170 -0
- uhttp_workers-1.0.0/uhttp/workers.py +1138 -0
- uhttp_workers-1.0.0/uhttp_workers.egg-info/PKG-INFO +453 -0
- uhttp_workers-1.0.0/uhttp_workers.egg-info/SOURCES.txt +21 -0
- uhttp_workers-1.0.0/uhttp_workers.egg-info/dependency_links.txt +1 -0
- uhttp_workers-1.0.0/uhttp_workers.egg-info/requires.txt +1 -0
- uhttp_workers-1.0.0/uhttp_workers.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.14"
|
|
21
|
+
|
|
22
|
+
- name: Install build dependencies
|
|
23
|
+
run: pip install build
|
|
24
|
+
|
|
25
|
+
- name: Install package
|
|
26
|
+
run: pip install .
|
|
27
|
+
|
|
28
|
+
- name: Run unit tests
|
|
29
|
+
run: python -m unittest discover -v tests/
|
|
30
|
+
|
|
31
|
+
- name: Build package
|
|
32
|
+
run: python -m build
|
|
33
|
+
|
|
34
|
+
- name: Publish to PyPI
|
|
35
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.14"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install package
|
|
25
|
+
run: pip install .
|
|
26
|
+
|
|
27
|
+
- name: Run unit tests
|
|
28
|
+
run: python -m unittest discover -v tests/
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uhttp-workers
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Multi-process worker dispatcher built on uhttp-server
|
|
5
|
+
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pavelrevak/uhttp
|
|
8
|
+
Project-URL: Repository, https://github.com/pavelrevak/uhttp
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: POSIX
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: uhttp-server
|
|
14
|
+
|
|
15
|
+
# uhttp-workers
|
|
16
|
+
|
|
17
|
+
Multi-process worker dispatcher built on [uhttp-server](https://github.com/cortexm/uhttp-server).
|
|
18
|
+
|
|
19
|
+
Single dispatcher process handles all connections, N worker processes handle business logic in parallel. Communication via `multiprocessing.Queue` with efficient `select()` integration (POSIX only).
|
|
20
|
+
|
|
21
|
+
## Architecture
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌─────────────┐
|
|
25
|
+
│ Worker 0 │
|
|
26
|
+
│ Worker 1 │
|
|
27
|
+
request_queue A │ Worker 2 │
|
|
28
|
+
┌────────────────► │ Worker 3 │ ComputeWorker
|
|
29
|
+
│ └──────┬──────┘
|
|
30
|
+
│ │
|
|
31
|
+
┌─────────────┤ │
|
|
32
|
+
│ │ ┌──────┴──────┐
|
|
33
|
+
│ Dispatcher │ request_queue B │ Worker 0 │
|
|
34
|
+
│ (main) ├────────────────► │ Worker 1 │ StorageWorker
|
|
35
|
+
│ │ └──────┬──────┘
|
|
36
|
+
│ - static │ │
|
|
37
|
+
│ - sync │ response_queue │
|
|
38
|
+
│ - auth │◄───────────────────────────┘
|
|
39
|
+
│ │ (shared)
|
|
40
|
+
└─────────────┘
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Key design decisions:**
|
|
44
|
+
|
|
45
|
+
- Sockets never leave the dispatcher — only serializable data goes through queues
|
|
46
|
+
- Each worker pool has its own request queue, all pools share one response queue
|
|
47
|
+
- Workers send heartbeats via response queue — dispatcher detects stuck/dead workers
|
|
48
|
+
- Each worker has a private control queue for config updates and stop signals
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install uhttp-workers
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import uhttp.workers as _workers
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ItemWorker(_workers.Worker):
|
|
63
|
+
def setup(self):
|
|
64
|
+
self.items = {}
|
|
65
|
+
self.next_id = 1
|
|
66
|
+
|
|
67
|
+
@_workers.api('/api/items', 'GET')
|
|
68
|
+
def list_items(self, request):
|
|
69
|
+
return {'items': list(self.items.values())}
|
|
70
|
+
|
|
71
|
+
@_workers.api('/api/item/{id:int}', 'GET')
|
|
72
|
+
def get_item(self, request):
|
|
73
|
+
item = self.items.get(request.path_params['id'])
|
|
74
|
+
if not item:
|
|
75
|
+
return {'error': 'Not found'}, 404
|
|
76
|
+
return item
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class MyDispatcher(_workers.Dispatcher):
|
|
80
|
+
def do_check(self, client):
|
|
81
|
+
api_key = client.headers.get('x-api-key')
|
|
82
|
+
if api_key not in VALID_KEYS:
|
|
83
|
+
client.respond({'error': 'unauthorized'}, status=401)
|
|
84
|
+
raise _workers.RejectRequest()
|
|
85
|
+
|
|
86
|
+
@_workers.sync('/health')
|
|
87
|
+
def health(self, client, path_params):
|
|
88
|
+
client.respond({'status': 'ok'})
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
dispatcher = MyDispatcher(
|
|
93
|
+
port=8080,
|
|
94
|
+
pools=[
|
|
95
|
+
_workers.WorkerPool(
|
|
96
|
+
ItemWorker, num_workers=4,
|
|
97
|
+
routes=['/api/**'],
|
|
98
|
+
timeout=30,
|
|
99
|
+
),
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
dispatcher.run()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == '__main__':
|
|
106
|
+
main()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Multiple Worker Pools
|
|
110
|
+
|
|
111
|
+
Route different endpoints to different worker pools with independent scaling:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
dispatcher = MyDispatcher(
|
|
115
|
+
port=8080,
|
|
116
|
+
pools=[
|
|
117
|
+
_workers.WorkerPool(
|
|
118
|
+
ComputeWorker, num_workers=4,
|
|
119
|
+
routes=['/api/compute/**'],
|
|
120
|
+
timeout=60,
|
|
121
|
+
),
|
|
122
|
+
_workers.WorkerPool(
|
|
123
|
+
StorageWorker, num_workers=2,
|
|
124
|
+
routes=['/api/items/**', '/api/item/**'],
|
|
125
|
+
timeout=10,
|
|
126
|
+
),
|
|
127
|
+
_workers.WorkerPool(
|
|
128
|
+
GeneralWorker, num_workers=1,
|
|
129
|
+
), # no routes = default/fallback pool
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Request routing order:
|
|
135
|
+
1. **Static files** — served directly by dispatcher
|
|
136
|
+
2. **Sync handlers** — run in dispatcher process
|
|
137
|
+
3. **Worker pools** — first pool with matching route prefix, or fallback pool
|
|
138
|
+
4. **404** — no match
|
|
139
|
+
|
|
140
|
+
## API Handlers
|
|
141
|
+
|
|
142
|
+
Group related endpoints under a common URL prefix using `ApiHandler`:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import uhttp.workers as _workers
|
|
146
|
+
|
|
147
|
+
class UserHandler(_workers.ApiHandler):
|
|
148
|
+
PATTERN = '/api/user'
|
|
149
|
+
|
|
150
|
+
@_workers.api('', 'GET')
|
|
151
|
+
def list_users(self, request):
|
|
152
|
+
return {'users': self.worker.db.list_users()}
|
|
153
|
+
|
|
154
|
+
@_workers.api('/{id:int}', 'GET')
|
|
155
|
+
def get_user(self, request):
|
|
156
|
+
return self.worker.db.get_user(request.path_params['id'])
|
|
157
|
+
|
|
158
|
+
@_workers.api('/{id:int}', 'DELETE')
|
|
159
|
+
def delete_user(self, request):
|
|
160
|
+
self.worker.db.delete_user(request.path_params['id'])
|
|
161
|
+
return {'deleted': request.path_params['id']}
|
|
162
|
+
|
|
163
|
+
class OrderHandler(_workers.ApiHandler):
|
|
164
|
+
PATTERN = '/api/order'
|
|
165
|
+
|
|
166
|
+
@_workers.api('', 'GET')
|
|
167
|
+
def list_orders(self, request):
|
|
168
|
+
return {'orders': []}
|
|
169
|
+
|
|
170
|
+
@_workers.api('/{id:int}', 'GET')
|
|
171
|
+
def get_order(self, request):
|
|
172
|
+
return {'id': request.path_params['id']}
|
|
173
|
+
|
|
174
|
+
class MyWorker(_workers.Worker):
|
|
175
|
+
HANDLERS = [UserHandler, OrderHandler]
|
|
176
|
+
|
|
177
|
+
def setup(self):
|
|
178
|
+
self.db = Database(self.kwargs['db_login'])
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`@api` patterns on handlers are automatically prefixed with the handler's `PATTERN`. Handlers access the worker instance via `self.worker`.
|
|
182
|
+
|
|
183
|
+
You can also define `@api` methods directly on the worker class — useful for simple workers that don't need handler grouping.
|
|
184
|
+
|
|
185
|
+
Handlers support inheritance — a subclass inherits all routes from its parent, using the subclass `PATTERN` as prefix.
|
|
186
|
+
|
|
187
|
+
## Static Files
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
dispatcher = Dispatcher(
|
|
191
|
+
port=8080,
|
|
192
|
+
static_routes={
|
|
193
|
+
'/static/': './static/',
|
|
194
|
+
'/images/': '/var/data/images/',
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Static files are served directly by the dispatcher process with path traversal protection. Directory requests automatically serve `index.html` if present.
|
|
200
|
+
|
|
201
|
+
## Sync Handlers
|
|
202
|
+
|
|
203
|
+
Lightweight handlers that run directly in the dispatcher process — no queue overhead. Define them as methods on the dispatcher class with the `@sync` decorator:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import uhttp.workers as _workers
|
|
207
|
+
|
|
208
|
+
class MyDispatcher(_workers.Dispatcher):
|
|
209
|
+
@_workers.sync('/health')
|
|
210
|
+
def health(self, client, path_params):
|
|
211
|
+
client.respond({
|
|
212
|
+
'status': 'ok',
|
|
213
|
+
'pools': [pool.status() for pool in self._pools],
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
@_workers.sync('/version')
|
|
217
|
+
def version(self, client, path_params):
|
|
218
|
+
client.respond({'version': '1.0.0'})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Use sync handlers for fast, non-blocking responses only — long operations block the entire dispatcher.
|
|
222
|
+
|
|
223
|
+
## Worker Lifecycle
|
|
224
|
+
|
|
225
|
+
### Setup
|
|
226
|
+
|
|
227
|
+
`setup()` is called once when a worker process starts. Use it to initialize resources that cannot be shared across processes (database connections, models, etc.):
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
class MyWorker(_workers.Worker):
|
|
231
|
+
def setup(self):
|
|
232
|
+
self.db = Database(self.kwargs['db_login'])
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Extra keyword arguments from `WorkerPool(...)` are available as `self.kwargs`.
|
|
236
|
+
|
|
237
|
+
### Configuration Updates
|
|
238
|
+
|
|
239
|
+
Dispatcher can send configuration to workers at runtime via per-worker control queues:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
# dispatcher side
|
|
243
|
+
for pool in dispatcher._pools:
|
|
244
|
+
pool.send_config({'rate_limit': 100})
|
|
245
|
+
|
|
246
|
+
# worker side
|
|
247
|
+
class MyWorker(_workers.Worker):
|
|
248
|
+
def on_config(self, config):
|
|
249
|
+
self.rate_limit = config['rate_limit']
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Health Monitoring
|
|
253
|
+
|
|
254
|
+
Workers send heartbeats automatically via the shared response queue. When a worker takes a request, it reports which `request_id` it is working on. If a worker stops responding:
|
|
255
|
+
|
|
256
|
+
- **Dead worker** (segfault, crash) — detected via `is_alive()`, restarted immediately
|
|
257
|
+
- **Stuck worker** (infinite loop in C code) — detected via heartbeat timeout, killed and restarted
|
|
258
|
+
- **Too many restarts** — pool marked as degraded, returns 503
|
|
259
|
+
|
|
260
|
+
### Request Handling
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
@_workers.api('/process/{id:int}', 'POST')
|
|
264
|
+
def process(self, request):
|
|
265
|
+
# request.request_id — internal ID for dispatcher pairing
|
|
266
|
+
# request.method — 'POST'
|
|
267
|
+
# request.path — '/process/42'
|
|
268
|
+
# request.path_params — {'id': 42}
|
|
269
|
+
# request.query — {'page': '1'} or None
|
|
270
|
+
# request.data — dict (JSON), bytes (binary), or None
|
|
271
|
+
# request.headers — dict
|
|
272
|
+
# request.content_type — 'application/json'
|
|
273
|
+
|
|
274
|
+
# return data (status 200)
|
|
275
|
+
return {'result': 'ok'}
|
|
276
|
+
|
|
277
|
+
# return data with status
|
|
278
|
+
return {'error': 'not found'}, 404
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## URL Patterns
|
|
282
|
+
|
|
283
|
+
Dispatcher uses prefix matching to route requests to pools:
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
_workers.WorkerPool(MyWorker, routes=['/api/users/**']) # matches /api/users/anything
|
|
287
|
+
_workers.WorkerPool(MyWorker, routes=['/api/status']) # exact match only
|
|
288
|
+
_workers.WorkerPool(MyWorker) # fallback — catches everything else
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Workers use full pattern matching with type conversion:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
@_workers.api('/user/{id:int}', 'GET') # id converted to int
|
|
295
|
+
@_workers.api('/price/{amount:float}', 'GET') # amount converted to float
|
|
296
|
+
@_workers.api('/tag/{name}') # name as str, all methods
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Authentication
|
|
300
|
+
|
|
301
|
+
Override `do_check()` on the dispatcher — runs before any request is queued:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
class MyDispatcher(_workers.Dispatcher):
|
|
305
|
+
def __init__(self, valid_keys, **kwargs):
|
|
306
|
+
super().__init__(**kwargs)
|
|
307
|
+
self.valid_keys = valid_keys
|
|
308
|
+
|
|
309
|
+
def do_check(self, client):
|
|
310
|
+
api_key = client.headers.get('x-api-key')
|
|
311
|
+
if api_key not in self.valid_keys:
|
|
312
|
+
client.respond({'error': 'unauthorized'}, status=401)
|
|
313
|
+
raise _workers.RejectRequest()
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`do_check()` is only called for requests going to worker pools — static files and sync handlers are not affected.
|
|
317
|
+
|
|
318
|
+
## Post-Response Hook
|
|
319
|
+
|
|
320
|
+
Override `on_response()` on the dispatcher to post-process after a response is sent to the client — e.g., forward data to another worker pool:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
class MyDispatcher(_workers.Dispatcher):
|
|
324
|
+
def on_response(self, response, pending):
|
|
325
|
+
if response.status == 200 and pending.pool.name == 'LprWorker':
|
|
326
|
+
storage_pool = self._find_pool('/internal/storage')
|
|
327
|
+
storage_pool.request_queue.put(_workers.Request(
|
|
328
|
+
request_id=-1,
|
|
329
|
+
method='POST',
|
|
330
|
+
path='/internal/storage/save',
|
|
331
|
+
data={
|
|
332
|
+
'image': pending.client.data,
|
|
333
|
+
'result': response.data,
|
|
334
|
+
}))
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`pending` is a `_PendingRequest` with `client` (original connection) and `pool` (source pool).
|
|
338
|
+
Requests with `request_id=-1` are ignored by the dispatcher when the worker responds.
|
|
339
|
+
|
|
340
|
+
## Dispatcher Idle Hook
|
|
341
|
+
|
|
342
|
+
Override `on_idle()` on the dispatcher for periodic background tasks — called on each `select()` timeout (every `SELECT_TIMEOUT` seconds, default 1s):
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
class MyDispatcher(_workers.Dispatcher):
|
|
346
|
+
def on_idle(self):
|
|
347
|
+
# periodic cleanup, monitoring, etc.
|
|
348
|
+
pass
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Workers have their own `on_idle()` hook, called on each `heartbeat_interval` timeout.
|
|
352
|
+
|
|
353
|
+
## Graceful Shutdown
|
|
354
|
+
|
|
355
|
+
On `SIGTERM` or `SIGINT`:
|
|
356
|
+
|
|
357
|
+
1. Stop accepting new connections
|
|
358
|
+
2. Wait for pending responses (up to `shutdown_timeout`)
|
|
359
|
+
3. Respond 503 to remaining pending requests
|
|
360
|
+
4. Send stop signal to all workers via control queues
|
|
361
|
+
5. Wait for workers to finish, kill after timeout
|
|
362
|
+
|
|
363
|
+
## Monitoring
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
class MyDispatcher(_workers.Dispatcher):
|
|
367
|
+
@_workers.sync('/monitor')
|
|
368
|
+
def monitor(self, client, path_params):
|
|
369
|
+
client.respond({
|
|
370
|
+
'pools': [pool.status() for pool in self._pools],
|
|
371
|
+
'pending': len(self._pending),
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Pool status includes per-worker info: alive, last seen, current request ID, queue size.
|
|
376
|
+
|
|
377
|
+
## Logging
|
|
378
|
+
|
|
379
|
+
Workers have a built-in `Logger` accessible via `self.log`:
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
class MyWorker(_workers.Worker):
|
|
383
|
+
@_workers.api('/item/{id:int}', 'GET')
|
|
384
|
+
def get_item(self, request):
|
|
385
|
+
item_id = request.path_params['id']
|
|
386
|
+
# %-style (Python logging compatible)
|
|
387
|
+
self.log.info("Getting item %d", item_id)
|
|
388
|
+
# {}-style (kwargs)
|
|
389
|
+
self.log.info("Getting item {id}", id=item_id)
|
|
390
|
+
return {'id': item_id}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Log messages are sent to the dispatcher via the shared response queue and printed in the dispatcher process — no interleaved output from multiple processes.
|
|
394
|
+
|
|
395
|
+
**Log levels:** `LOG_DEBUG` (10), `LOG_INFO` (20), `LOG_WARNING` (30), `LOG_ERROR` (40), `LOG_CRITICAL` (50)
|
|
396
|
+
|
|
397
|
+
Set minimum level per pool:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
_workers.WorkerPool(
|
|
401
|
+
MyWorker, num_workers=4,
|
|
402
|
+
log_level=_workers.LOG_INFO, # default: LOG_WARNING
|
|
403
|
+
)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Output format** — auto-detected at dispatcher init:
|
|
407
|
+
- **Terminal:** ANSI colors — bold red (critical), red (error), yellow (warning), dim (debug)
|
|
408
|
+
- **systemd:** Syslog priority prefixes (`<3>`, `<4>`, etc.) — journalctl colors by priority
|
|
409
|
+
|
|
410
|
+
Override `Dispatcher.on_log(name, level, message)` to customize output or forward to a logging framework.
|
|
411
|
+
|
|
412
|
+
Errors are logged automatically:
|
|
413
|
+
- Handler exceptions → ERROR with full traceback (returns 500 to client)
|
|
414
|
+
- `setup()` crash → CRITICAL with traceback (worker exits and restarts)
|
|
415
|
+
- Worker restart → ERROR with reason (died/stuck)
|
|
416
|
+
- Request timeout → WARNING with request ID and timeout value
|
|
417
|
+
|
|
418
|
+
## Configuration
|
|
419
|
+
|
|
420
|
+
### Dispatcher
|
|
421
|
+
|
|
422
|
+
| Parameter | Default | Description |
|
|
423
|
+
|-----------|---------|-------------|
|
|
424
|
+
| `port` | 8080 | Listen port |
|
|
425
|
+
| `address` | `'0.0.0.0'` | Listen address |
|
|
426
|
+
| `pools` | `[]` | List of `WorkerPool` instances |
|
|
427
|
+
| `static_routes` | `{}` | URL prefix → filesystem path |
|
|
428
|
+
| `shutdown_timeout` | 10 | Seconds to wait on shutdown |
|
|
429
|
+
| `max_pending` | 1000 | Max pending requests (503 when exceeded) |
|
|
430
|
+
| `ssl_context` | `None` | `ssl.SSLContext` for HTTPS |
|
|
431
|
+
|
|
432
|
+
### WorkerPool
|
|
433
|
+
|
|
434
|
+
| Parameter | Default | Description |
|
|
435
|
+
|-----------|---------|-------------|
|
|
436
|
+
| `worker_class` | — | `Worker` subclass |
|
|
437
|
+
| `num_workers` | 1 | Number of worker processes |
|
|
438
|
+
| `routes` | `None` | Prefix patterns (`None` = fallback pool) |
|
|
439
|
+
| `timeout` | 30 | Request timeout in seconds (504) |
|
|
440
|
+
| `stuck_timeout` | 60 | Heartbeat timeout before kill |
|
|
441
|
+
| `heartbeat_interval` | 1 | Seconds between worker heartbeats |
|
|
442
|
+
| `log_level` | `LOG_WARNING` | Minimum log level for worker loggers |
|
|
443
|
+
| `max_restarts` | 10 | Max restarts per `restart_window` |
|
|
444
|
+
| `restart_window` | 300 | Time window for restart counting (seconds) |
|
|
445
|
+
| `queue_warning` | 100 | Log warning when queue size exceeds this (0 = disable) |
|
|
446
|
+
|
|
447
|
+
Extra `**kwargs` on `WorkerPool` are passed to worker constructor (accessible as `self.kwargs`).
|
|
448
|
+
|
|
449
|
+
## Requirements
|
|
450
|
+
|
|
451
|
+
- Python >= 3.10
|
|
452
|
+
- POSIX system (Linux, macOS) — uses `select()` with `queue._reader`
|
|
453
|
+
- [uhttp-server](https://github.com/cortexm/uhttp-server)
|