Pyjoyment 0.1.0__py3-none-any.whl
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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/Server/Prefork.py
ADDED
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Server.Prefork - Pre-forking non-blocking I/O HTTP and WebSocket server
|
|
3
|
+
============================================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Server.Prefork import Prefork
|
|
7
|
+
|
|
8
|
+
prefork = Prefork(listen=['http://*:8080'])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@prefork.unsubscribe('request').on('request')
|
|
12
|
+
def request(prefork, tx):
|
|
13
|
+
# Request
|
|
14
|
+
method = tx.req.method
|
|
15
|
+
path = tx.req.url.path
|
|
16
|
+
|
|
17
|
+
# Response
|
|
18
|
+
tx.res.code = 200
|
|
19
|
+
tx.res.headers.content_type = 'text/plain'
|
|
20
|
+
tx.res.body = f'{method} request for {path}!'.encode()
|
|
21
|
+
|
|
22
|
+
# Resume transaction
|
|
23
|
+
tx.resume()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
prefork.run()
|
|
27
|
+
|
|
28
|
+
# Or import the module, as in Google Python Style Guide
|
|
29
|
+
|
|
30
|
+
import Pyjo.Server.Prefork
|
|
31
|
+
|
|
32
|
+
prefork = Pyjo.Server.Prefork.new(listen=['http://*:8080'])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@prefork.unsubscribe('request').on('request')
|
|
36
|
+
def request(prefork, tx):
|
|
37
|
+
# Request
|
|
38
|
+
method = tx.req.method
|
|
39
|
+
path = tx.req.url.path
|
|
40
|
+
|
|
41
|
+
# Response
|
|
42
|
+
tx.res.code = 200
|
|
43
|
+
tx.res.headers.content_type = 'text/plain'
|
|
44
|
+
tx.res.body = f'{method} request for {path}!'.encode()
|
|
45
|
+
|
|
46
|
+
# Resume transaction
|
|
47
|
+
tx.resume()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
prefork.run()
|
|
51
|
+
|
|
52
|
+
:mod:`Pyjo.Server.Prefork` is a full featured, UNIX optimized, pre-forking
|
|
53
|
+
non-blocking I/O HTTP and WebSocket server, built around the very well tested
|
|
54
|
+
and reliable :mod:`Pyjo.Server.Daemon`, with IPv6, TLS, SNI, UNIX domain
|
|
55
|
+
socket, Comet (long polling), keep-alive and multiple event loop support.
|
|
56
|
+
Note that the server uses signals for process management, so you should avoid
|
|
57
|
+
modifying signal handlers in your applications.
|
|
58
|
+
|
|
59
|
+
The workers are forks of the manager, made with :func:`os.fork`, so the
|
|
60
|
+
server runs where :func:`os.fork` exists, not on Windows, where
|
|
61
|
+
:mod:`Pyjo.Server.Prethread` has the workers in threads. Name resolution in
|
|
62
|
+
a thread, SOCKS5 and TLS support can be disabled with the ``PYJO_NO_NNR``,
|
|
63
|
+
``PYJO_NO_SOCKS`` and ``PYJO_NO_TLS`` environment variables.
|
|
64
|
+
|
|
65
|
+
Manager signals
|
|
66
|
+
---------------
|
|
67
|
+
|
|
68
|
+
The :mod:`Pyjo.Server.Prefork` manager process can be controlled at runtime
|
|
69
|
+
with the following signals.
|
|
70
|
+
|
|
71
|
+
INT, TERM
|
|
72
|
+
~~~~~~~~~
|
|
73
|
+
|
|
74
|
+
Shut down server gracefully. The manager and the workers close the listen
|
|
75
|
+
sockets, so that a new connection is refused, and the workers finish the
|
|
76
|
+
accepted ones, after :attr:`~Pyjo.Server.Daemon.Pyjo_Server_Daemon.shutdown_delay`,
|
|
77
|
+
while the workers serve as before, which the manager tells them with ``USR1``.
|
|
78
|
+
A second ``INT``, as of Ctrl-C pressed twice, shuts it down immediately, also
|
|
79
|
+
during the delay, a second ``TERM`` does nothing.
|
|
80
|
+
|
|
81
|
+
QUIT
|
|
82
|
+
~~~~
|
|
83
|
+
|
|
84
|
+
Shut down server immediately.
|
|
85
|
+
|
|
86
|
+
HUP
|
|
87
|
+
~~~
|
|
88
|
+
|
|
89
|
+
Restart the workers gracefully, or emit the event ``reload`` when it has
|
|
90
|
+
subscribers.
|
|
91
|
+
|
|
92
|
+
TTIN
|
|
93
|
+
~~~~
|
|
94
|
+
|
|
95
|
+
Increase worker pool by one.
|
|
96
|
+
|
|
97
|
+
TTOU
|
|
98
|
+
~~~~
|
|
99
|
+
|
|
100
|
+
Decrease worker pool by one.
|
|
101
|
+
|
|
102
|
+
Worker signals
|
|
103
|
+
--------------
|
|
104
|
+
|
|
105
|
+
:mod:`Pyjo.Server.Prefork` worker processes can be controlled at runtime
|
|
106
|
+
with the following signals.
|
|
107
|
+
|
|
108
|
+
TERM
|
|
109
|
+
~~~~
|
|
110
|
+
|
|
111
|
+
Stop worker gracefully, with its copies of the listen sockets closed.
|
|
112
|
+
|
|
113
|
+
USR1
|
|
114
|
+
~~~~
|
|
115
|
+
|
|
116
|
+
Stop worker gracefully after
|
|
117
|
+
:attr:`~Pyjo.Server.Daemon.Pyjo_Server_Daemon.shutdown_delay`, while it
|
|
118
|
+
serves as before.
|
|
119
|
+
|
|
120
|
+
QUIT
|
|
121
|
+
~~~~
|
|
122
|
+
|
|
123
|
+
Stop worker immediately.
|
|
124
|
+
|
|
125
|
+
The workers ignore ``INT``, which Ctrl-C sends to all the processes of the
|
|
126
|
+
server, and ``HUP``, as the manager stops them.
|
|
127
|
+
|
|
128
|
+
Events
|
|
129
|
+
------
|
|
130
|
+
|
|
131
|
+
:mod:`Pyjo.Server.Prefork` inherits all events from :mod:`Pyjo.Server.Daemon`
|
|
132
|
+
and can emit the following new ones.
|
|
133
|
+
|
|
134
|
+
finish
|
|
135
|
+
~~~~~~
|
|
136
|
+
::
|
|
137
|
+
|
|
138
|
+
prefork.on('finish', lambda prefork, graceful: ...)
|
|
139
|
+
|
|
140
|
+
Emitted when the server shuts down. ::
|
|
141
|
+
|
|
142
|
+
prefork.on('finish', lambda prefork, graceful: print('Graceful server shutdown' if graceful else 'Server shutdown'))
|
|
143
|
+
|
|
144
|
+
heartbeat
|
|
145
|
+
~~~~~~~~~
|
|
146
|
+
::
|
|
147
|
+
|
|
148
|
+
prefork.on('heartbeat', lambda prefork, pid: ...)
|
|
149
|
+
|
|
150
|
+
Emitted when a heartbeat message has been received from a worker. ::
|
|
151
|
+
|
|
152
|
+
prefork.on('heartbeat', lambda prefork, pid: print(f'Worker {pid} has a heartbeat'))
|
|
153
|
+
|
|
154
|
+
reap
|
|
155
|
+
~~~~
|
|
156
|
+
::
|
|
157
|
+
|
|
158
|
+
prefork.on('reap', lambda prefork, pid: ...)
|
|
159
|
+
|
|
160
|
+
Emitted when a child process exited. ::
|
|
161
|
+
|
|
162
|
+
prefork.on('reap', lambda prefork, pid: print(f'Worker {pid} stopped'))
|
|
163
|
+
|
|
164
|
+
reload
|
|
165
|
+
~~~~~~
|
|
166
|
+
::
|
|
167
|
+
|
|
168
|
+
prefork.on('reload', lambda prefork: ...)
|
|
169
|
+
|
|
170
|
+
Emitted when the manager gets the signal ``HUP``, in the place of the restart
|
|
171
|
+
of the workers. ::
|
|
172
|
+
|
|
173
|
+
prefork.on('reload', lambda prefork: print('Reloading the configuration'))
|
|
174
|
+
|
|
175
|
+
spawn
|
|
176
|
+
~~~~~
|
|
177
|
+
::
|
|
178
|
+
|
|
179
|
+
prefork.on('spawn', lambda prefork, pid: ...)
|
|
180
|
+
|
|
181
|
+
Emitted when a worker process is spawned. ::
|
|
182
|
+
|
|
183
|
+
prefork.on('spawn', lambda prefork, pid: print(f'Worker {pid} started'))
|
|
184
|
+
|
|
185
|
+
wait
|
|
186
|
+
~~~~
|
|
187
|
+
::
|
|
188
|
+
|
|
189
|
+
prefork.on('wait', lambda prefork: ...)
|
|
190
|
+
|
|
191
|
+
Emitted when the manager starts waiting for new heartbeat messages. ::
|
|
192
|
+
|
|
193
|
+
prefork.on('wait', lambda prefork: print(f'Waiting for heartbeat messages from {prefork.workers} workers'))
|
|
194
|
+
|
|
195
|
+
See also
|
|
196
|
+
--------
|
|
197
|
+
|
|
198
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
199
|
+
|
|
200
|
+
Ported from `Mojo::Server::Prefork <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/Prefork.pm>`_
|
|
201
|
+
of Mojolicious 9.49.
|
|
202
|
+
|
|
203
|
+
Classes
|
|
204
|
+
-------
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
import Pyjo.Exception
|
|
208
|
+
import Pyjo.File
|
|
209
|
+
import Pyjo.Server.Daemon
|
|
210
|
+
|
|
211
|
+
import os
|
|
212
|
+
import re
|
|
213
|
+
import select
|
|
214
|
+
import signal
|
|
215
|
+
import socket
|
|
216
|
+
import sys
|
|
217
|
+
import tempfile
|
|
218
|
+
import time
|
|
219
|
+
import traceback
|
|
220
|
+
import weakref
|
|
221
|
+
|
|
222
|
+
from typing import Any, NoReturn, TypedDict
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class _Worker(TypedDict, total=False):
|
|
226
|
+
force: bool
|
|
227
|
+
graceful: float | None
|
|
228
|
+
delayed: bool
|
|
229
|
+
healthy: bool
|
|
230
|
+
quit: int
|
|
231
|
+
time: float
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class Pyjo_Server_Prefork(Pyjo.Server.Daemon.object):
|
|
235
|
+
"""
|
|
236
|
+
:mod:`Pyjo.Server.Prefork` inherits all methods from
|
|
237
|
+
:mod:`Pyjo.Server.Daemon` and implements the following new ones.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
# Not in Mojolicious: the workers are forks of the manager, which Pyjo.Server.Prethread changes
|
|
241
|
+
_FORKS = True
|
|
242
|
+
|
|
243
|
+
# Not in Mojolicious: the signals of the manager, as a class attribute, which Pyjo.Server.Prethread changes; Windows
|
|
244
|
+
# has INT, TERM and BREAK of Ctrl-Break, which takes the place of QUIT
|
|
245
|
+
if sys.platform == 'win32':
|
|
246
|
+
_MANAGED: tuple[signal.Signals, ...] = (signal.SIGINT, signal.SIGTERM, signal.SIGBREAK)
|
|
247
|
+
else:
|
|
248
|
+
_MANAGED = (
|
|
249
|
+
signal.SIGCHLD,
|
|
250
|
+
signal.SIGHUP,
|
|
251
|
+
signal.SIGINT,
|
|
252
|
+
signal.SIGTERM,
|
|
253
|
+
signal.SIGQUIT,
|
|
254
|
+
signal.SIGTTIN,
|
|
255
|
+
signal.SIGTTOU,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def __init__(
|
|
259
|
+
self,
|
|
260
|
+
*,
|
|
261
|
+
accepts: int = 10000,
|
|
262
|
+
cleanup: bool = True,
|
|
263
|
+
graceful_timeout: float = 25,
|
|
264
|
+
heartbeat_interval: float = 5,
|
|
265
|
+
heartbeat_timeout: float = 50,
|
|
266
|
+
pid_file: str | os.PathLike[str] | None = None,
|
|
267
|
+
spare: int = 2,
|
|
268
|
+
workers: int = 4,
|
|
269
|
+
**kwargs: Any,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""::
|
|
272
|
+
|
|
273
|
+
prefork = Pyjo.Server.Prefork.new()
|
|
274
|
+
prefork = Pyjo.Server.Prefork.new(listen=['http://*:8080'], workers=10)
|
|
275
|
+
|
|
276
|
+
Construct a new :mod:`Pyjo.Server.Prefork` object.
|
|
277
|
+
"""
|
|
278
|
+
super().__init__(**kwargs)
|
|
279
|
+
|
|
280
|
+
self.accepts = accepts
|
|
281
|
+
"""::
|
|
282
|
+
|
|
283
|
+
accepts = prefork.accepts
|
|
284
|
+
prefork.accepts = 100
|
|
285
|
+
|
|
286
|
+
Maximum number of connections a worker is allowed to accept, before
|
|
287
|
+
stopping gracefully and then getting replaced with a newly started
|
|
288
|
+
worker, passed along to :attr:`Pyjo.IOLoop.Pyjo_IOLoop.max_accepts`,
|
|
289
|
+
defaults to ``10000``. Setting the value to ``0`` will allow workers to
|
|
290
|
+
accept new connections indefinitely. Note that up to half of this value
|
|
291
|
+
can be subtracted randomly to improve load balancing, and to make sure
|
|
292
|
+
that not all workers restart at the same time.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
self.cleanup = cleanup
|
|
296
|
+
"""::
|
|
297
|
+
|
|
298
|
+
boolean = prefork.cleanup
|
|
299
|
+
prefork.cleanup = False
|
|
300
|
+
|
|
301
|
+
Delete :attr:`pid_file` automatically once it is not needed anymore,
|
|
302
|
+
defaults to a true value.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
# Unlike Mojolicious: 25 seconds, where Mojo has 120, below the 30 seconds that Kubernetes gives a container to
|
|
306
|
+
# stop, so that the server forces its workers and stops by itself before it is killed
|
|
307
|
+
self.graceful_timeout = graceful_timeout
|
|
308
|
+
"""::
|
|
309
|
+
|
|
310
|
+
timeout = prefork.graceful_timeout
|
|
311
|
+
prefork.graceful_timeout = 15
|
|
312
|
+
|
|
313
|
+
Maximum amount of time in seconds stopping a worker gracefully may take
|
|
314
|
+
before being forced, defaults to ``25``, less than the 30 seconds that
|
|
315
|
+
Kubernetes gives a container to stop. Note that this value should
|
|
316
|
+
usually be a little larger than the maximum amount of time you expect
|
|
317
|
+
any one request to take.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
self.heartbeat_interval = heartbeat_interval
|
|
321
|
+
"""::
|
|
322
|
+
|
|
323
|
+
interval = prefork.heartbeat_interval
|
|
324
|
+
prefork.heartbeat_interval = 3
|
|
325
|
+
|
|
326
|
+
Heartbeat interval in seconds, defaults to ``5``.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
self.heartbeat_timeout = heartbeat_timeout
|
|
330
|
+
"""::
|
|
331
|
+
|
|
332
|
+
timeout = prefork.heartbeat_timeout
|
|
333
|
+
prefork.heartbeat_timeout = 2
|
|
334
|
+
|
|
335
|
+
Maximum amount of time in seconds before a worker without a heartbeat
|
|
336
|
+
will be stopped gracefully, defaults to ``50``. Note that this value
|
|
337
|
+
should usually be a little larger than the maximum amount of time you
|
|
338
|
+
expect any one operation to block the event loop.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
self.pid_file: str | os.PathLike[str] = (
|
|
342
|
+
pid_file if pid_file is not None else os.path.join(tempfile.gettempdir(), 'prefork.pid')
|
|
343
|
+
)
|
|
344
|
+
"""::
|
|
345
|
+
|
|
346
|
+
file = prefork.pid_file
|
|
347
|
+
prefork.pid_file = '/tmp/prefork.pid'
|
|
348
|
+
|
|
349
|
+
Full path of process id file, defaults to ``prefork.pid`` in a
|
|
350
|
+
temporary directory.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
self.spare = spare
|
|
354
|
+
"""::
|
|
355
|
+
|
|
356
|
+
spare = prefork.spare
|
|
357
|
+
prefork.spare = 4
|
|
358
|
+
|
|
359
|
+
Temporarily spawn up to this number of additional workers if there is a
|
|
360
|
+
need, defaults to ``2``. This allows for new workers to be started while
|
|
361
|
+
old ones are still shutting down gracefully, drastically reducing the
|
|
362
|
+
performance cost of worker restarts.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
self.workers = workers
|
|
366
|
+
"""::
|
|
367
|
+
|
|
368
|
+
workers = prefork.workers
|
|
369
|
+
prefork.workers = 10
|
|
370
|
+
|
|
371
|
+
Number of worker processes, defaults to ``4``. A good rule of thumb is
|
|
372
|
+
two worker processes per CPU core for applications that perform mostly
|
|
373
|
+
non-blocking operations, blocking operations often require more and
|
|
374
|
+
benefit from decreasing concurrency with
|
|
375
|
+
:attr:`Pyjo.Server.Daemon.Pyjo_Server_Daemon.max_clients` (often as
|
|
376
|
+
low as ``1``).
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
self._delayed: float | None = None
|
|
380
|
+
self._finished = False
|
|
381
|
+
self._graceful = False
|
|
382
|
+
self._pool: dict[int, _Worker] = {}
|
|
383
|
+
self._reader: int | None = None
|
|
384
|
+
self._running = False
|
|
385
|
+
self._signals: list[int] = []
|
|
386
|
+
self._wakee: socket.socket | None = None
|
|
387
|
+
self._writer: int | None = None
|
|
388
|
+
|
|
389
|
+
def check_pid(self) -> int | None:
|
|
390
|
+
"""::
|
|
391
|
+
|
|
392
|
+
pid = prefork.check_pid()
|
|
393
|
+
|
|
394
|
+
Get process id for running server from :attr:`pid_file` or delete it if
|
|
395
|
+
server is not running. ::
|
|
396
|
+
|
|
397
|
+
if not prefork.check_pid():
|
|
398
|
+
print('Server is not running')
|
|
399
|
+
"""
|
|
400
|
+
file = Pyjo.File.new(self.pid_file)
|
|
401
|
+
if not os.access(file, os.R_OK):
|
|
402
|
+
return None
|
|
403
|
+
try:
|
|
404
|
+
pid = int((file.slurp() or b'').strip())
|
|
405
|
+
except ValueError:
|
|
406
|
+
pid = 0
|
|
407
|
+
|
|
408
|
+
# Running
|
|
409
|
+
if pid and _alive(pid):
|
|
410
|
+
return pid
|
|
411
|
+
|
|
412
|
+
# Not running
|
|
413
|
+
file.remove()
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
def ensure_pid_file(self, pid: int) -> None:
|
|
417
|
+
"""::
|
|
418
|
+
|
|
419
|
+
prefork.ensure_pid_file(pid)
|
|
420
|
+
|
|
421
|
+
Ensure :attr:`pid_file` exists.
|
|
422
|
+
"""
|
|
423
|
+
# Check if PID file already exists
|
|
424
|
+
file = Pyjo.File.new(self.pid_file)
|
|
425
|
+
if os.path.exists(file):
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
# Create PID file
|
|
429
|
+
try:
|
|
430
|
+
file.spew(f'{pid}\n'.encode()).chmod(0o644)
|
|
431
|
+
except Exception as e:
|
|
432
|
+
err = f'Can\'t create process id file "{file}": {e}'
|
|
433
|
+
self.app.log.error(err)
|
|
434
|
+
raise Pyjo.Exception.new(err) from e
|
|
435
|
+
self.app.log.info(f'Creating process id file "{file}"')
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def healthy(self) -> int:
|
|
439
|
+
"""::
|
|
440
|
+
|
|
441
|
+
healthy = prefork.healthy
|
|
442
|
+
|
|
443
|
+
Number of currently active worker processes with a heartbeat.
|
|
444
|
+
"""
|
|
445
|
+
return sum(1 for w in self._pool.values() if w.get('healthy'))
|
|
446
|
+
|
|
447
|
+
def run(self) -> None:
|
|
448
|
+
"""::
|
|
449
|
+
|
|
450
|
+
prefork.run()
|
|
451
|
+
|
|
452
|
+
Run server and wait for the manager signals.
|
|
453
|
+
"""
|
|
454
|
+
# Unlike Mojolicious: no fork at all, where Mojo has no fork emulation of Perl on Windows
|
|
455
|
+
if self._FORKS and not hasattr(os, 'fork'):
|
|
456
|
+
print('Pre-forking needs os.fork.')
|
|
457
|
+
sys.exit(0)
|
|
458
|
+
|
|
459
|
+
# Pipe for worker communication
|
|
460
|
+
try:
|
|
461
|
+
reader, writer = self._reader, self._writer = self._pipe()
|
|
462
|
+
except OSError as e:
|
|
463
|
+
raise OSError(e.errno, f"Can't create pipe: {e.strerror}") from e
|
|
464
|
+
|
|
465
|
+
# Clean manager environment
|
|
466
|
+
# Not in Mojolicious: a handler only notes the signal, which the manager handles where it waits for
|
|
467
|
+
# heartbeats, as Perl defers the handlers of signals to a safe point, and a socket pair that Python writes to
|
|
468
|
+
# on a signal wakes the manager up, as Python repeats a system call that a signal interrupts until its timeout
|
|
469
|
+
def note(signum: int, frame: Any) -> None:
|
|
470
|
+
self._signals.append(signum)
|
|
471
|
+
|
|
472
|
+
# Not in Mojolicious: local %SIG, the handlers of the signals are set back at the end
|
|
473
|
+
handlers: dict[signal.Signals, Any] = {}
|
|
474
|
+
for name in self._MANAGED:
|
|
475
|
+
handlers[name] = signal.signal(name, note)
|
|
476
|
+
waker, self._wakee = socket.socketpair()
|
|
477
|
+
waker.setblocking(False)
|
|
478
|
+
self._wakee.setblocking(False)
|
|
479
|
+
wakeup = signal.set_wakeup_fd(waker.fileno())
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
# Preload application before starting workers
|
|
483
|
+
self.start().app.log.info(f'Manager {os.getpid()} started')
|
|
484
|
+
self.ioloop.max_accepts = self.accepts
|
|
485
|
+
self._running = True
|
|
486
|
+
while self._running:
|
|
487
|
+
self._manage()
|
|
488
|
+
self.app.log.info(f'Manager {os.getpid()} stopped')
|
|
489
|
+
|
|
490
|
+
# Not in Mojolicious: the end of the server in the manager, which has started it, once the workers have
|
|
491
|
+
# stopped, unless it has stopped immediately
|
|
492
|
+
if self._graceful:
|
|
493
|
+
self._server_stopped()
|
|
494
|
+
finally:
|
|
495
|
+
signal.set_wakeup_fd(wakeup)
|
|
496
|
+
|
|
497
|
+
# Not in Mojolicious: the signals are blocked while the handlers are set back, so that a signal that comes
|
|
498
|
+
# in between is not left to a handler that is gone, which Python reports as a race condition; Windows
|
|
499
|
+
# blocks no signal
|
|
500
|
+
if sys.platform == 'win32':
|
|
501
|
+
for name, handler in handlers.items():
|
|
502
|
+
signal.signal(name, handler)
|
|
503
|
+
else:
|
|
504
|
+
signal.pthread_sigmask(signal.SIG_BLOCK, handlers)
|
|
505
|
+
try:
|
|
506
|
+
for name, handler in handlers.items():
|
|
507
|
+
signal.signal(name, handler)
|
|
508
|
+
finally:
|
|
509
|
+
signal.pthread_sigmask(signal.SIG_UNBLOCK, handlers)
|
|
510
|
+
|
|
511
|
+
# Not in Mojolicious: the pipe is closed at the end, as Perl closes it with the object
|
|
512
|
+
self._close_pipe(reader, writer)
|
|
513
|
+
waker.close()
|
|
514
|
+
self._wakee.close()
|
|
515
|
+
self._reader = self._writer = self._wakee = None
|
|
516
|
+
|
|
517
|
+
# Not in Mojolicious: the pipe of the heartbeats in methods of their own, which Pyjo.Server.Prethread changes
|
|
518
|
+
def _pipe(self) -> tuple[int, int]:
|
|
519
|
+
return os.pipe()
|
|
520
|
+
|
|
521
|
+
def _read_pipe(self, reader: int) -> bytes:
|
|
522
|
+
return os.read(reader, 4194304)
|
|
523
|
+
|
|
524
|
+
def _close_pipe(self, reader: int, writer: int) -> None:
|
|
525
|
+
os.close(reader)
|
|
526
|
+
os.close(writer)
|
|
527
|
+
|
|
528
|
+
# DESTROY: the process id file is deleted with the object, after the listen sockets of the daemon are closed
|
|
529
|
+
def _cleanup(self) -> None:
|
|
530
|
+
super()._cleanup()
|
|
531
|
+
if self.cleanup:
|
|
532
|
+
Pyjo.File.new(self.pid_file).remove()
|
|
533
|
+
|
|
534
|
+
def _heartbeat(self, finished: bool) -> None:
|
|
535
|
+
try:
|
|
536
|
+
assert self._writer is not None
|
|
537
|
+
os.write(self._writer, f'{os.getpid()}:{int(finished)}\n'.encode())
|
|
538
|
+
except Exception:
|
|
539
|
+
_exit(0)
|
|
540
|
+
|
|
541
|
+
def _manage(self) -> None:
|
|
542
|
+
# Not in Mojolicious: the delay of a graceful shutdown, which the workers are told, also the ones that start in it
|
|
543
|
+
if (delayed := self._delayed) is not None and not self._finished:
|
|
544
|
+
if delayed <= time.monotonic():
|
|
545
|
+
self._term(True)
|
|
546
|
+
else:
|
|
547
|
+
for pid, w in self._pool.items():
|
|
548
|
+
if not w.get('delayed'):
|
|
549
|
+
w['delayed'] = True
|
|
550
|
+
self._delay_worker(pid)
|
|
551
|
+
|
|
552
|
+
# Spawn more workers if necessary and check PID file
|
|
553
|
+
if not self._finished:
|
|
554
|
+
graceful = sum(1 for w in self._pool.values() if w.get('graceful'))
|
|
555
|
+
spare = self.spare
|
|
556
|
+
spare = (spare if graceful > spare else graceful) if graceful else 0
|
|
557
|
+
need = (self.workers - len(self._pool)) + spare
|
|
558
|
+
while need > 0:
|
|
559
|
+
self._spawn()
|
|
560
|
+
need -= 1
|
|
561
|
+
self.ensure_pid_file(os.getpid())
|
|
562
|
+
|
|
563
|
+
# Shutdown
|
|
564
|
+
elif not len(self._pool):
|
|
565
|
+
self._running = False
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
# Wait for heartbeats
|
|
569
|
+
self._wait()
|
|
570
|
+
|
|
571
|
+
interval = self.heartbeat_interval
|
|
572
|
+
ht = self.heartbeat_timeout
|
|
573
|
+
gt = self.graceful_timeout
|
|
574
|
+
log = self.app.log
|
|
575
|
+
now = time.monotonic()
|
|
576
|
+
|
|
577
|
+
for pid in list(self._pool):
|
|
578
|
+
if (w := self._pool.get(pid)) is None:
|
|
579
|
+
continue
|
|
580
|
+
|
|
581
|
+
# No heartbeat (graceful stop)
|
|
582
|
+
if not w.get('graceful') and w.get('time', now) + interval + ht <= now:
|
|
583
|
+
log.error(f'Worker {pid} has no heartbeat ({_number(ht)} seconds), restarting (see FAQ for more)')
|
|
584
|
+
w['graceful'] = now
|
|
585
|
+
|
|
586
|
+
# Graceful stop with timeout
|
|
587
|
+
graceful = w['graceful'] = w.get('graceful') or (now if self._graceful else None)
|
|
588
|
+
if graceful:
|
|
589
|
+
quit = w.get('quit', 0)
|
|
590
|
+
w['quit'] = quit + 1
|
|
591
|
+
if not quit:
|
|
592
|
+
log.info(f'Stopping worker {pid} gracefully ({_number(gt)} seconds)')
|
|
593
|
+
if not self._kill_worker(pid, True):
|
|
594
|
+
self._stopped(pid)
|
|
595
|
+
if graceful and graceful + gt <= now:
|
|
596
|
+
w['force'] = True
|
|
597
|
+
|
|
598
|
+
# Normal stop
|
|
599
|
+
if w.get('force') or (self._finished and not graceful):
|
|
600
|
+
log.warn(f'Stopping worker {pid} immediately')
|
|
601
|
+
if not self._kill_worker(pid, False):
|
|
602
|
+
self._stopped(pid)
|
|
603
|
+
|
|
604
|
+
# Not in Mojolicious: a worker is stopped in a method of its own, which Pyjo.Server.Prethread does without signals;
|
|
605
|
+
# true if the worker has been told to stop
|
|
606
|
+
def _kill_worker(self, pid: int, graceful: bool) -> bool:
|
|
607
|
+
# Unlike Mojolicious: TERM stops a worker gracefully, where Mojo sends QUIT
|
|
608
|
+
return _kill(signal.SIGTERM if graceful else signal.SIGKILL, pid)
|
|
609
|
+
|
|
610
|
+
# Not in Mojolicious: a worker is told the delay of a graceful shutdown, in a method of its own, which
|
|
611
|
+
# Pyjo.Server.Prethread does without signals
|
|
612
|
+
def _delay_worker(self, pid: int) -> None:
|
|
613
|
+
if sys.platform != 'win32':
|
|
614
|
+
_kill(signal.SIGUSR1, pid)
|
|
615
|
+
|
|
616
|
+
# The signals that the handlers noted, as the handlers of Mojo do them
|
|
617
|
+
def _signal(self) -> None:
|
|
618
|
+
while len(self._signals):
|
|
619
|
+
signum = self._signals.pop(0)
|
|
620
|
+
if sys.platform != 'win32' and signum == signal.SIGCHLD:
|
|
621
|
+
while True:
|
|
622
|
+
try:
|
|
623
|
+
pid, _ = os.waitpid(-1, os.WNOHANG)
|
|
624
|
+
except ChildProcessError:
|
|
625
|
+
break
|
|
626
|
+
if pid <= 0:
|
|
627
|
+
break
|
|
628
|
+
self.emit('reap', pid)._stopped(pid)
|
|
629
|
+
# Unlike Mojolicious: INT and TERM shut the server down gracefully, as Docker and Kubernetes stop it with TERM,
|
|
630
|
+
# and QUIT immediately, where Mojo does it the other way round, as nginx and unicorn do; a second INT, as of
|
|
631
|
+
# Ctrl-C pressed twice, shuts it down immediately, also during the delay
|
|
632
|
+
elif signum in (signal.SIGINT, signal.SIGTERM):
|
|
633
|
+
if not self._finished and self._delayed is None:
|
|
634
|
+
self._shutdown()
|
|
635
|
+
elif signum == signal.SIGINT:
|
|
636
|
+
self._term()
|
|
637
|
+
elif signum == _QUIT:
|
|
638
|
+
self._term()
|
|
639
|
+
# Not in Mojolicious: HUP restarts the workers gracefully, as gunicorn does, unless something else reloads;
|
|
640
|
+
# Windows has no HUP, TTIN and TTOU
|
|
641
|
+
elif sys.platform == 'win32':
|
|
642
|
+
continue
|
|
643
|
+
elif signum == signal.SIGHUP:
|
|
644
|
+
self._reload()
|
|
645
|
+
elif signum == signal.SIGTTIN:
|
|
646
|
+
self.workers += 1
|
|
647
|
+
elif signum == signal.SIGTTOU and self.workers > 0:
|
|
648
|
+
self.workers -= 1
|
|
649
|
+
for w in self._pool.values():
|
|
650
|
+
if not w.get('graceful'):
|
|
651
|
+
w['graceful'] = time.monotonic()
|
|
652
|
+
break
|
|
653
|
+
|
|
654
|
+
def _reload(self) -> None:
|
|
655
|
+
if self.has_subscribers('reload'):
|
|
656
|
+
self.emit('reload')
|
|
657
|
+
return
|
|
658
|
+
self.app.log.info('Restarting workers gracefully')
|
|
659
|
+
now = time.monotonic()
|
|
660
|
+
for w in self._pool.values():
|
|
661
|
+
w['graceful'] = w.get('graceful') or now
|
|
662
|
+
|
|
663
|
+
# Not in Mojolicious: the graceful shutdown after its delay, while the workers serve as before
|
|
664
|
+
def _shutdown(self) -> None:
|
|
665
|
+
if (delay := self.shutdown_delay) <= 0:
|
|
666
|
+
return self._term(True)
|
|
667
|
+
self.app.log.info(f'Stopping gracefully in {_number(delay)} seconds')
|
|
668
|
+
self._delayed = time.monotonic() + delay
|
|
669
|
+
|
|
670
|
+
def _spawn(self) -> None:
|
|
671
|
+
# Manager
|
|
672
|
+
# Not in Mojolicious: the buffers of the standard streams are written before, so that the worker does not write
|
|
673
|
+
# them again
|
|
674
|
+
_flush()
|
|
675
|
+
# Unlike Mojolicious: TERM and QUIT are blocked while the manager forks, until the worker has its handlers, where
|
|
676
|
+
# the worker of Mojo takes them with the handlers of the manager and serves on, and Python clears a signal that
|
|
677
|
+
# comes during the fork
|
|
678
|
+
signal.pthread_sigmask(signal.SIG_BLOCK, _WORKER)
|
|
679
|
+
try:
|
|
680
|
+
pid = os.fork()
|
|
681
|
+
except OSError as e:
|
|
682
|
+
signal.pthread_sigmask(signal.SIG_UNBLOCK, _WORKER)
|
|
683
|
+
raise OSError(e.errno, f"Can't fork: {e.strerror}") from e
|
|
684
|
+
if pid:
|
|
685
|
+
signal.pthread_sigmask(signal.SIG_UNBLOCK, _WORKER)
|
|
686
|
+
self.emit('spawn', pid)._pool[pid] = {'time': time.monotonic()}
|
|
687
|
+
return
|
|
688
|
+
|
|
689
|
+
self._worker()
|
|
690
|
+
|
|
691
|
+
def _stopped(self, pid: int) -> None:
|
|
692
|
+
if (w := self._pool.pop(pid, None)) is None:
|
|
693
|
+
return
|
|
694
|
+
|
|
695
|
+
log = self.app.log
|
|
696
|
+
log.info(f'Worker {pid} stopped')
|
|
697
|
+
if not w.get('healthy'):
|
|
698
|
+
log.error(f'Worker {pid} stopped too early, shutting down')
|
|
699
|
+
self._term()
|
|
700
|
+
|
|
701
|
+
def _term(self, graceful: bool = False) -> None:
|
|
702
|
+
self.emit('finish', graceful)
|
|
703
|
+
self._finished, self._graceful = True, graceful
|
|
704
|
+
# Not in Mojolicious: the delay of a graceful shutdown is over, so that the manager waits as before
|
|
705
|
+
self._delayed = None
|
|
706
|
+
|
|
707
|
+
# Unlike Mojolicious: the copies of the listen sockets of the manager are closed, as it starts no more workers, so
|
|
708
|
+
# that a new connection is refused once the workers have closed theirs, where the manager of Mojo keeps them until
|
|
709
|
+
# it ends; the new manager of a hot deployment has copies of its own
|
|
710
|
+
self._close_listen()
|
|
711
|
+
|
|
712
|
+
# Not in Mojolicious: the workers that are stopping gracefully stop immediately too, after a graceful shutdown
|
|
713
|
+
if not graceful:
|
|
714
|
+
for w in self._pool.values():
|
|
715
|
+
w['force'] = True
|
|
716
|
+
|
|
717
|
+
def _wait(self) -> None:
|
|
718
|
+
# Poll for heartbeats
|
|
719
|
+
reader, wakee = self.emit('wait')._reader, self._wakee
|
|
720
|
+
assert reader is not None and wakee is not None
|
|
721
|
+
# Not in Mojolicious: the end of the delay of a graceful shutdown is not waited for longer
|
|
722
|
+
timeout = 1 if (delayed := self._delayed) is None else min(1, max(0, delayed - time.monotonic()))
|
|
723
|
+
readable = select.select([reader, wakee], [], [], timeout)[0]
|
|
724
|
+
if wakee in readable:
|
|
725
|
+
try:
|
|
726
|
+
wakee.recv(4096)
|
|
727
|
+
except OSError:
|
|
728
|
+
pass
|
|
729
|
+
self._signal()
|
|
730
|
+
if reader not in readable:
|
|
731
|
+
return
|
|
732
|
+
try:
|
|
733
|
+
chunk = self._read_pipe(reader)
|
|
734
|
+
except OSError:
|
|
735
|
+
return
|
|
736
|
+
if not chunk:
|
|
737
|
+
return
|
|
738
|
+
|
|
739
|
+
# Update heartbeats (and stop gracefully if necessary)
|
|
740
|
+
now = time.monotonic()
|
|
741
|
+
for m in re.finditer(rb'(\d+):(\d)\n', chunk):
|
|
742
|
+
if (w := self._pool.get(int(m[1]))) is None:
|
|
743
|
+
continue
|
|
744
|
+
w['healthy'], w['time'] = True, now
|
|
745
|
+
self.emit('heartbeat', int(m[1]))
|
|
746
|
+
if m[2] != b'0':
|
|
747
|
+
w['graceful'] = w.get('graceful') or now
|
|
748
|
+
w['quit'] = w.get('quit', 0) + 1
|
|
749
|
+
|
|
750
|
+
# The worker, a fork of the manager, which runs the event loop until it stops, and exits
|
|
751
|
+
def _worker(self) -> NoReturn:
|
|
752
|
+
code = 0
|
|
753
|
+
try:
|
|
754
|
+
# Heartbeat messages
|
|
755
|
+
self.cleanup = False
|
|
756
|
+
loop = self.ioloop
|
|
757
|
+
finished = False
|
|
758
|
+
|
|
759
|
+
def finish(loop: Any) -> None:
|
|
760
|
+
nonlocal finished
|
|
761
|
+
finished = True
|
|
762
|
+
|
|
763
|
+
loop.on('finish', finish)
|
|
764
|
+
ref = weakref.ref(self)
|
|
765
|
+
|
|
766
|
+
def heartbeat(loop: Any) -> None:
|
|
767
|
+
if (prefork := ref()) is not None:
|
|
768
|
+
prefork._heartbeat(finished)
|
|
769
|
+
|
|
770
|
+
loop.next_tick(heartbeat)
|
|
771
|
+
loop.recurring(self.heartbeat_interval, heartbeat)
|
|
772
|
+
|
|
773
|
+
# Clean worker environment
|
|
774
|
+
for name in (signal.SIGCHLD, signal.SIGTTIN, signal.SIGTTOU):
|
|
775
|
+
signal.signal(name, signal.SIG_DFL)
|
|
776
|
+
# Unlike Mojolicious: the workers ignore INT, which Ctrl-C sends to all the processes of the server, and HUP,
|
|
777
|
+
# as the manager stops them
|
|
778
|
+
for name in (signal.SIGHUP, signal.SIGINT):
|
|
779
|
+
signal.signal(name, signal.SIG_IGN)
|
|
780
|
+
# Not in Mojolicious: the handlers of TERM and QUIT only note the signal, which the event loop handles, as
|
|
781
|
+
# Perl defers the handlers of signals to a safe point, and a socket pair that Python writes to on a signal
|
|
782
|
+
# wakes the event loop up, as Python repeats a system call that a signal interrupts until its timeout
|
|
783
|
+
noted: list[int] = []
|
|
784
|
+
immediate = False
|
|
785
|
+
|
|
786
|
+
def note(signum: int, frame: Any) -> None:
|
|
787
|
+
noted.append(signum)
|
|
788
|
+
|
|
789
|
+
for name in _WORKER:
|
|
790
|
+
signal.signal(name, note)
|
|
791
|
+
waker, wakee = socket.socketpair()
|
|
792
|
+
waker.setblocking(False)
|
|
793
|
+
wakee.setblocking(False)
|
|
794
|
+
signal.set_wakeup_fd(waker.fileno())
|
|
795
|
+
|
|
796
|
+
def wake(reactor: Any, writable: bool) -> None:
|
|
797
|
+
try:
|
|
798
|
+
wakee.recv(4096)
|
|
799
|
+
except OSError:
|
|
800
|
+
pass
|
|
801
|
+
# Unlike Mojolicious: TERM stops the worker gracefully, and QUIT immediately, where Mojo has QUIT for a
|
|
802
|
+
# graceful stop
|
|
803
|
+
nonlocal immediate
|
|
804
|
+
while noted:
|
|
805
|
+
if (signum := noted.pop(0)) == signal.SIGQUIT:
|
|
806
|
+
immediate = True
|
|
807
|
+
loop.stop()
|
|
808
|
+
# Not in Mojolicious: USR1 stops the worker gracefully after the delay of the graceful shutdown
|
|
809
|
+
elif signum == signal.SIGUSR1:
|
|
810
|
+
if (prefork := ref()) is not None:
|
|
811
|
+
loop.stop_gracefully(prefork.shutdown_delay)
|
|
812
|
+
else:
|
|
813
|
+
loop.stop_gracefully()
|
|
814
|
+
|
|
815
|
+
loop.reactor.io(wakee, wake).watch(wakee, True, False)
|
|
816
|
+
|
|
817
|
+
# Not in Mojolicious: TERM and QUIT, which the manager blocks while it forks, come now that the worker has its
|
|
818
|
+
# handlers
|
|
819
|
+
signal.pthread_sigmask(signal.SIG_UNBLOCK, _WORKER)
|
|
820
|
+
|
|
821
|
+
# Unlike Mojolicious: the worker closes its copies of the listen sockets, as the daemon does
|
|
822
|
+
def max_requests(loop: Any) -> None:
|
|
823
|
+
if (prefork := ref()) is not None:
|
|
824
|
+
prefork._close_listen()
|
|
825
|
+
prefork.max_requests = 1
|
|
826
|
+
|
|
827
|
+
loop.on('finish', max_requests)
|
|
828
|
+
if (reader := self._reader) is not None:
|
|
829
|
+
self._reader = None
|
|
830
|
+
os.close(reader)
|
|
831
|
+
# Left out from Mojolicious: srand, as random seeds itself again in the child of a fork
|
|
832
|
+
|
|
833
|
+
self.app.log.info(f'Worker {os.getpid()} started')
|
|
834
|
+
loop.start()
|
|
835
|
+
|
|
836
|
+
# Not in Mojolicious: the end of the server in the worker, unless it has stopped immediately
|
|
837
|
+
if not immediate:
|
|
838
|
+
self._server_stopped()
|
|
839
|
+
|
|
840
|
+
# Not in Mojolicious: sys.exit() is the exit of the worker, as exit is in Perl
|
|
841
|
+
except SystemExit as e:
|
|
842
|
+
code = e.code if isinstance(e.code, int) else 0 if e.code is None else 1
|
|
843
|
+
except BaseException:
|
|
844
|
+
traceback.print_exc()
|
|
845
|
+
code = 255
|
|
846
|
+
_exit(code)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
# Not in Mojolicious: Windows has no QUIT, of Ctrl-\ in a terminal, and BREAK of Ctrl-Break takes its place in a console
|
|
850
|
+
_QUIT = signal.SIGBREAK if sys.platform == 'win32' else signal.SIGQUIT
|
|
851
|
+
|
|
852
|
+
# Not in Mojolicious: the signals that the workers handle, which the manager blocks while it forks
|
|
853
|
+
_WORKER = {signal.SIGTERM, _QUIT} if sys.platform == 'win32' else {signal.SIGTERM, _QUIT, signal.SIGUSR1}
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
# Not in Mojolicious: os._exit, as the worker leaves what the manager does at its end, with the buffers of the standard
|
|
857
|
+
# streams written before, which os._exit leaves
|
|
858
|
+
def _exit(code: int) -> NoReturn:
|
|
859
|
+
_flush()
|
|
860
|
+
os._exit(code)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _flush() -> None:
|
|
864
|
+
for stream in (sys.stdout, sys.stderr):
|
|
865
|
+
try:
|
|
866
|
+
stream.flush()
|
|
867
|
+
except Exception:
|
|
868
|
+
pass
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
# Perl's kill 0, true if the process runs. On Windows, os.kill with 0 is not a check: 0 is CTRL_C_EVENT there, which
|
|
872
|
+
# goes to every process of the console, and another signal is TerminateProcess, so the process is opened and asked for
|
|
873
|
+
# its exit code instead
|
|
874
|
+
if sys.platform == 'win32':
|
|
875
|
+
import ctypes
|
|
876
|
+
|
|
877
|
+
from ctypes import wintypes
|
|
878
|
+
|
|
879
|
+
_kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
|
880
|
+
_kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
|
|
881
|
+
_kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
882
|
+
_kernel32.GetExitCodeProcess.argtypes = (wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD))
|
|
883
|
+
_kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
|
|
884
|
+
|
|
885
|
+
# PROCESS_QUERY_LIMITED_INFORMATION, ERROR_ACCESS_DENIED of a process that runs as someone else, and STILL_ACTIVE
|
|
886
|
+
_QUERY, _DENIED, _ACTIVE = 0x1000, 5, 259
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _alive(pid: int) -> bool:
|
|
890
|
+
if sys.platform != 'win32':
|
|
891
|
+
return _kill(0, pid)
|
|
892
|
+
if not (handle := _kernel32.OpenProcess(_QUERY, False, pid)):
|
|
893
|
+
return ctypes.get_last_error() == _DENIED
|
|
894
|
+
try:
|
|
895
|
+
code = wintypes.DWORD()
|
|
896
|
+
return bool(_kernel32.GetExitCodeProcess(handle, ctypes.byref(code))) and code.value == _ACTIVE
|
|
897
|
+
finally:
|
|
898
|
+
_kernel32.CloseHandle(handle)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
# Perl's kill, true if the signal was sent
|
|
902
|
+
def _kill(sig: int, pid: int) -> bool:
|
|
903
|
+
try:
|
|
904
|
+
os.kill(pid, sig)
|
|
905
|
+
return True
|
|
906
|
+
except OSError:
|
|
907
|
+
return False
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
# A number of seconds as Perl prints it, without ".0"
|
|
911
|
+
def _number(value: float) -> str:
|
|
912
|
+
return str(int(value)) if value == int(value) else str(value)
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
new = Pyjo_Server_Prefork.new
|
|
916
|
+
object = Pyjo_Server_Prefork
|
|
917
|
+
Prefork = Pyjo_Server_Prefork
|