ominfra 0.0.0.dev128__py3-none-any.whl → 0.0.0.dev130__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -22,10 +22,10 @@ from .spawning import ProcessSpawnError
22
22
  from .spawning import ProcessSpawning
23
23
  from .states import ProcessState
24
24
  from .states import SupervisorState
25
- from .types import InputDispatcher
26
25
  from .types import Process
27
26
  from .types import ProcessGroup
28
- from .types import ServerContext
27
+ from .types import ProcessInputDispatcher
28
+ from .types import SupervisorStateManager
29
29
  from .utils.os import decode_wait_status
30
30
  from .utils.ostypes import Pid
31
31
  from .utils.ostypes import Rc
@@ -47,7 +47,7 @@ class ProcessImpl(Process):
47
47
  config: ProcessConfig,
48
48
  group: ProcessGroup,
49
49
  *,
50
- context: ServerContext,
50
+ supervisor_states: SupervisorStateManager,
51
51
  event_callbacks: EventCallbacks,
52
52
  process_spawning_factory: ProcessSpawningFactory,
53
53
  ) -> None:
@@ -56,7 +56,7 @@ class ProcessImpl(Process):
56
56
  self._config = config
57
57
  self._group = group
58
58
 
59
- self._context = context
59
+ self._supervisor_states = supervisor_states
60
60
  self._event_callbacks = event_callbacks
61
61
 
62
62
  self._spawning = process_spawning_factory(self)
@@ -87,7 +87,7 @@ class ProcessImpl(Process):
87
87
  #
88
88
 
89
89
  def __repr__(self) -> str:
90
- return f'<Subprocess at {id(self)} with name {self._config.name} in state {self.get_state().name}>'
90
+ return f'<Subprocess at {id(self)} with name {self._config.name} in state {self._state.name}>'
91
91
 
92
92
  #
93
93
 
@@ -109,10 +109,6 @@ class ProcessImpl(Process):
109
109
 
110
110
  #
111
111
 
112
- @property
113
- def context(self) -> ServerContext:
114
- return self._context
115
-
116
112
  @property
117
113
  def state(self) -> ProcessState:
118
114
  return self._state
@@ -175,7 +171,7 @@ class ProcessImpl(Process):
175
171
  if stdin_fd is None:
176
172
  raise OSError(errno.EPIPE, 'Process has no stdin channel')
177
173
 
178
- dispatcher = check_isinstance(self._dispatchers[stdin_fd], InputDispatcher)
174
+ dispatcher = check_isinstance(self._dispatchers[stdin_fd], ProcessInputDispatcher)
179
175
  if dispatcher.closed:
180
176
  raise OSError(errno.EPIPE, "Process' stdin channel is closed")
181
177
 
@@ -382,6 +378,7 @@ class ProcessImpl(Process):
382
378
  self._last_stop = now
383
379
 
384
380
  if now > self._last_start:
381
+ log.info(f'{now - self._last_start=}') # noqa
385
382
  too_quickly = now - self._last_start < self._config.startsecs
386
383
  else:
387
384
  too_quickly = False
@@ -445,18 +442,13 @@ class ProcessImpl(Process):
445
442
  self._pipes = ProcessPipes()
446
443
  self._dispatchers = Dispatchers([])
447
444
 
448
- def get_state(self) -> ProcessState:
449
- return self._state
450
-
451
445
  def transition(self) -> None:
452
446
  now = time.time()
453
447
  state = self._state
454
448
 
455
449
  self._check_and_adjust_for_system_clock_rollback(now)
456
450
 
457
- logger = log
458
-
459
- if self.context.state > SupervisorState.RESTARTING:
451
+ if self._supervisor_states.state > SupervisorState.RESTARTING:
460
452
  # dont start any processes if supervisor is shutting down
461
453
  if state == ProcessState.EXITED:
462
454
  if self._config.autorestart:
@@ -487,14 +479,14 @@ class ProcessImpl(Process):
487
479
  self.check_in_state(ProcessState.STARTING)
488
480
  self.change_state(ProcessState.RUNNING)
489
481
  msg = ('entered RUNNING state, process has stayed up for > than %s seconds (startsecs)' % self._config.startsecs) # noqa
490
- logger.info('success: %s %s', self.name, msg)
482
+ log.info('success: %s %s', self.name, msg)
491
483
 
492
484
  if state == ProcessState.BACKOFF:
493
485
  if self._backoff > self._config.startretries:
494
486
  # BACKOFF -> FATAL if the proc has exceeded its number of retries
495
487
  self.give_up()
496
488
  msg = ('entered FATAL state, too many start retries too quickly')
497
- logger.info('gave up: %s %s', self.name, msg)
489
+ log.info('gave up: %s %s', self.name, msg)
498
490
 
499
491
  elif state == ProcessState.STOPPING:
500
492
  time_left = self._delay - now
@@ -0,0 +1,66 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import signal
3
+
4
+ from omlish.lite.logs import log
5
+
6
+ from .groups import ProcessGroupManager
7
+ from .states import SupervisorState
8
+ from .types import ProcessOutputDispatcher
9
+ from .types import SupervisorStateManager
10
+ from .utils.signals import SignalReceiver
11
+ from .utils.signals import sig_name
12
+
13
+
14
+ class SignalHandler:
15
+ def __init__(
16
+ self,
17
+ *,
18
+ states: SupervisorStateManager,
19
+ signal_receiver: SignalReceiver,
20
+ process_groups: ProcessGroupManager,
21
+ ) -> None:
22
+ super().__init__()
23
+
24
+ self._states = states
25
+ self._signal_receiver = signal_receiver
26
+ self._process_groups = process_groups
27
+
28
+ def set_signals(self) -> None:
29
+ self._signal_receiver.install(
30
+ signal.SIGTERM,
31
+ signal.SIGINT,
32
+ signal.SIGQUIT,
33
+ signal.SIGHUP,
34
+ signal.SIGCHLD,
35
+ signal.SIGUSR2,
36
+ )
37
+
38
+ def handle_signals(self) -> None:
39
+ sig = self._signal_receiver.get_signal()
40
+ if not sig:
41
+ return
42
+
43
+ if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
44
+ log.warning('received %s indicating exit request', sig_name(sig))
45
+ self._states.set_state(SupervisorState.SHUTDOWN)
46
+
47
+ elif sig == signal.SIGHUP:
48
+ if self._states.state == SupervisorState.SHUTDOWN:
49
+ log.warning('ignored %s indicating restart request (shutdown in progress)', sig_name(sig)) # noqa
50
+ else:
51
+ log.warning('received %s indicating restart request', sig_name(sig)) # noqa
52
+ self._states.set_state(SupervisorState.RESTARTING)
53
+
54
+ elif sig == signal.SIGCHLD:
55
+ log.debug('received %s indicating a child quit', sig_name(sig))
56
+
57
+ elif sig == signal.SIGUSR2:
58
+ log.info('received %s indicating log reopen request', sig_name(sig))
59
+
60
+ for p in self._process_groups.all_processes():
61
+ for d in p.get_dispatchers():
62
+ if isinstance(d, ProcessOutputDispatcher):
63
+ d.reopen_logs()
64
+
65
+ else:
66
+ log.debug('received %s indicating nothing', sig_name(sig))
@@ -7,6 +7,7 @@ import typing as ta
7
7
 
8
8
  from omlish.lite.check import check_isinstance
9
9
  from omlish.lite.check import check_not_none
10
+ from omlish.lite.fdio.handlers import FdIoHandler
10
11
  from omlish.lite.typing import Func3
11
12
 
12
13
  from .configs import ProcessConfig
@@ -29,11 +30,10 @@ from .process import PidHistory
29
30
  from .spawning import ProcessSpawnError
30
31
  from .spawning import ProcessSpawning
31
32
  from .spawning import SpawnedProcess
32
- from .types import Dispatcher
33
- from .types import InputDispatcher
34
- from .types import OutputDispatcher
35
33
  from .types import Process
36
34
  from .types import ProcessGroup
35
+ from .types import ProcessInputDispatcher
36
+ from .types import ProcessOutputDispatcher
37
37
  from .utils.diag import compact_traceback
38
38
  from .utils.fds import close_fd
39
39
  from .utils.fs import get_path
@@ -44,11 +44,11 @@ from .utils.ostypes import Rc
44
44
  from .utils.strings import as_bytes
45
45
 
46
46
 
47
- class OutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], Fd, OutputDispatcher]):
47
+ class ProcessOutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], Fd, ProcessOutputDispatcher]):
48
48
  pass
49
49
 
50
50
 
51
- class InputDispatcherFactory(Func3[Process, str, Fd, InputDispatcher]):
51
+ class ProcessInputDispatcherFactory(Func3[Process, str, Fd, ProcessInputDispatcher]):
52
52
  pass
53
53
 
54
54
 
@@ -66,8 +66,8 @@ class ProcessSpawningImpl(ProcessSpawning):
66
66
  server_config: ServerConfig,
67
67
  pid_history: PidHistory,
68
68
 
69
- output_dispatcher_factory: OutputDispatcherFactory,
70
- input_dispatcher_factory: InputDispatcherFactory,
69
+ output_dispatcher_factory: ProcessOutputDispatcherFactory,
70
+ input_dispatcher_factory: ProcessInputDispatcherFactory,
71
71
 
72
72
  inherited_fds: ta.Optional[InheritedFds] = None,
73
73
  ) -> None:
@@ -200,28 +200,28 @@ class ProcessSpawningImpl(ProcessSpawning):
200
200
  return exe, args
201
201
 
202
202
  def _make_dispatchers(self, pipes: ProcessPipes) -> Dispatchers:
203
- dispatchers: ta.List[Dispatcher] = []
203
+ dispatchers: ta.List[FdIoHandler] = []
204
204
 
205
205
  if pipes.stdout is not None:
206
206
  dispatchers.append(check_isinstance(self._output_dispatcher_factory(
207
207
  self.process,
208
208
  ProcessCommunicationStdoutEvent,
209
209
  pipes.stdout,
210
- ), OutputDispatcher))
210
+ ), ProcessOutputDispatcher))
211
211
 
212
212
  if pipes.stderr is not None:
213
213
  dispatchers.append(check_isinstance(self._output_dispatcher_factory(
214
214
  self.process,
215
215
  ProcessCommunicationStderrEvent,
216
216
  pipes.stderr,
217
- ), OutputDispatcher))
217
+ ), ProcessOutputDispatcher))
218
218
 
219
219
  if pipes.stdin is not None:
220
220
  dispatchers.append(check_isinstance(self._input_dispatcher_factory(
221
221
  self.process,
222
222
  'stdin',
223
223
  pipes.stdin,
224
- ), InputDispatcher))
224
+ ), ProcessInputDispatcher))
225
225
 
226
226
  return Dispatchers(dispatchers)
227
227
 
@@ -1,40 +1,38 @@
1
1
  # ruff: noqa: UP006 UP007
2
- import signal
2
+ import errno
3
+ import os
3
4
  import time
4
5
  import typing as ta
5
6
 
6
7
  from omlish.lite.check import check_isinstance
7
- from omlish.lite.check import check_not_none
8
+ from omlish.lite.fdio.pollers import FdIoPoller
8
9
  from omlish.lite.logs import log
9
10
  from omlish.lite.typing import Func1
10
11
 
11
12
  from .configs import ProcessGroupConfig
12
- from .context import ServerContextImpl
13
- from .dispatchers import Dispatchers
13
+ from .configs import ServerConfig
14
14
  from .events import TICK_EVENTS
15
15
  from .events import EventCallbacks
16
16
  from .events import SupervisorRunningEvent
17
17
  from .events import SupervisorStoppingEvent
18
18
  from .groups import ProcessGroup
19
19
  from .groups import ProcessGroupManager
20
- from .poller import Poller
20
+ from .io import IoManager
21
21
  from .process import PidHistory
22
22
  from .setup import SupervisorSetup
23
+ from .signals import SignalHandler
23
24
  from .states import SupervisorState
24
- from .types import OutputDispatcher
25
+ from .types import ExitNow
25
26
  from .types import Process
27
+ from .types import SupervisorStateManager
26
28
  from .utils.os import decode_wait_status
27
- from .utils.signals import SignalReceiver
28
- from .utils.signals import sig_name
29
+ from .utils.ostypes import Pid
30
+ from .utils.ostypes import Rc
29
31
 
30
32
 
31
33
  ##
32
34
 
33
35
 
34
- class ExitNow(Exception): # noqa
35
- pass
36
-
37
-
38
36
  def timeslice(period: int, when: float) -> int:
39
37
  return int(when - (when % period))
40
38
 
@@ -42,59 +40,18 @@ def timeslice(period: int, when: float) -> int:
42
40
  ##
43
41
 
44
42
 
45
- class SignalHandler:
46
- def __init__(
47
- self,
48
- *,
49
- context: ServerContextImpl,
50
- signal_receiver: SignalReceiver,
51
- process_groups: ProcessGroupManager,
52
- ) -> None:
43
+ class SupervisorStateManagerImpl(SupervisorStateManager):
44
+ def __init__(self) -> None:
53
45
  super().__init__()
54
46
 
55
- self._context = context
56
- self._signal_receiver = signal_receiver
57
- self._process_groups = process_groups
58
-
59
- def set_signals(self) -> None:
60
- self._signal_receiver.install(
61
- signal.SIGTERM,
62
- signal.SIGINT,
63
- signal.SIGQUIT,
64
- signal.SIGHUP,
65
- signal.SIGCHLD,
66
- signal.SIGUSR2,
67
- )
68
-
69
- def handle_signals(self) -> None:
70
- sig = self._signal_receiver.get_signal()
71
- if not sig:
72
- return
73
-
74
- if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
75
- log.warning('received %s indicating exit request', sig_name(sig))
76
- self._context.set_state(SupervisorState.SHUTDOWN)
77
-
78
- elif sig == signal.SIGHUP:
79
- if self._context.state == SupervisorState.SHUTDOWN:
80
- log.warning('ignored %s indicating restart request (shutdown in progress)', sig_name(sig)) # noqa
81
- else:
82
- log.warning('received %s indicating restart request', sig_name(sig)) # noqa
83
- self._context.set_state(SupervisorState.RESTARTING)
47
+ self._state: SupervisorState = SupervisorState.RUNNING
84
48
 
85
- elif sig == signal.SIGCHLD:
86
- log.debug('received %s indicating a child quit', sig_name(sig))
87
-
88
- elif sig == signal.SIGUSR2:
89
- log.info('received %s indicating log reopen request', sig_name(sig))
90
-
91
- for p in self._process_groups.all_processes():
92
- for d in p.get_dispatchers():
93
- if isinstance(d, OutputDispatcher):
94
- d.reopen_logs()
49
+ @property
50
+ def state(self) -> SupervisorState:
51
+ return self._state
95
52
 
96
- else:
97
- log.debug('received %s indicating nothing', sig_name(sig))
53
+ def set_state(self, state: SupervisorState) -> None:
54
+ self._state = state
98
55
 
99
56
 
100
57
  ##
@@ -108,18 +65,20 @@ class Supervisor:
108
65
  def __init__(
109
66
  self,
110
67
  *,
111
- context: ServerContextImpl,
112
- poller: Poller,
68
+ config: ServerConfig,
69
+ poller: FdIoPoller,
113
70
  process_groups: ProcessGroupManager,
114
71
  signal_handler: SignalHandler,
115
72
  event_callbacks: EventCallbacks,
116
73
  process_group_factory: ProcessGroupFactory,
117
74
  pid_history: PidHistory,
118
75
  setup: SupervisorSetup,
76
+ states: SupervisorStateManager,
77
+ io: IoManager,
119
78
  ) -> None:
120
79
  super().__init__()
121
80
 
122
- self._context = context
81
+ self._config = config
123
82
  self._poller = poller
124
83
  self._process_groups = process_groups
125
84
  self._signal_handler = signal_handler
@@ -127,6 +86,8 @@ class Supervisor:
127
86
  self._process_group_factory = process_group_factory
128
87
  self._pid_history = pid_history
129
88
  self._setup = setup
89
+ self._states = states
90
+ self._io = io
130
91
 
131
92
  self._ticks: ta.Dict[int, float] = {}
132
93
  self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
@@ -136,11 +97,8 @@ class Supervisor:
136
97
  #
137
98
 
138
99
  @property
139
- def context(self) -> ServerContextImpl:
140
- return self._context
141
-
142
- def get_state(self) -> SupervisorState:
143
- return self._context.state
100
+ def state(self) -> SupervisorState:
101
+ return self._states.state
144
102
 
145
103
  #
146
104
 
@@ -181,7 +139,7 @@ class Supervisor:
181
139
  log.info('waiting for %s to die', namestr)
182
140
  self._last_shutdown_report = now
183
141
  for proc in unstopped:
184
- log.debug('%s state: %s', proc.config.name, proc.get_state().name)
142
+ log.debug('%s state: %s', proc.config.name, proc.state.name)
185
143
 
186
144
  return unstopped
187
145
 
@@ -205,7 +163,7 @@ class Supervisor:
205
163
  self._event_callbacks.clear()
206
164
 
207
165
  try:
208
- for config in self._context.config.groups or []:
166
+ for config in self._config.groups or []:
209
167
  self.add_process_group(config)
210
168
 
211
169
  self._signal_handler.set_signals()
@@ -224,12 +182,14 @@ class Supervisor:
224
182
  #
225
183
 
226
184
  def _run_once(self) -> None:
185
+ now = time.time()
227
186
  self._poll()
187
+ log.info(f'Poll took {time.time() - now}') # noqa
228
188
  self._reap()
229
189
  self._signal_handler.handle_signals()
230
190
  self._tick()
231
191
 
232
- if self._context.state < SupervisorState.RUNNING:
192
+ if self._states.state < SupervisorState.RUNNING:
233
193
  self._ordered_stop_groups_phase_2()
234
194
 
235
195
  def _ordered_stop_groups_phase_1(self) -> None:
@@ -248,20 +208,11 @@ class Supervisor:
248
208
  # down, so push it back on to the end of the stop group queue
249
209
  self._stop_groups.append(group)
250
210
 
251
- def get_dispatchers(self) -> Dispatchers:
252
- return Dispatchers(
253
- d
254
- for p in self._process_groups.all_processes()
255
- for d in p.get_dispatchers()
256
- )
257
-
258
211
  def _poll(self) -> None:
259
- dispatchers = self.get_dispatchers()
260
-
261
212
  sorted_groups = list(self._process_groups)
262
213
  sorted_groups.sort()
263
214
 
264
- if self._context.state < SupervisorState.RUNNING:
215
+ if self._states.state < SupervisorState.RUNNING:
265
216
  if not self._stopping:
266
217
  # first time, set the stopping flag, do a notification and set stop_groups
267
218
  self._stopping = True
@@ -274,54 +225,7 @@ class Supervisor:
274
225
  # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
275
226
  raise ExitNow
276
227
 
277
- for fd, dispatcher in dispatchers.items():
278
- if dispatcher.readable():
279
- self._poller.register_readable(fd)
280
- if dispatcher.writable():
281
- self._poller.register_writable(fd)
282
-
283
- timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
284
- r, w = self._poller.poll(timeout)
285
-
286
- for fd in r:
287
- if fd in dispatchers:
288
- try:
289
- dispatcher = dispatchers[fd]
290
- log.debug('read event caused by %r', dispatcher)
291
- dispatcher.handle_read_event()
292
- if not dispatcher.readable():
293
- self._poller.unregister_readable(fd)
294
- except ExitNow:
295
- raise
296
- except Exception: # noqa
297
- dispatchers[fd].handle_error()
298
- else:
299
- # if the fd is not in combined map, we should unregister it. otherwise, it will be polled every
300
- # time, which may cause 100% cpu usage
301
- log.debug('unexpected read event from fd %r', fd)
302
- try:
303
- self._poller.unregister_readable(fd)
304
- except Exception: # noqa
305
- pass
306
-
307
- for fd in w:
308
- if fd in dispatchers:
309
- try:
310
- dispatcher = dispatchers[fd]
311
- log.debug('write event caused by %r', dispatcher)
312
- dispatcher.handle_write_event()
313
- if not dispatcher.writable():
314
- self._poller.unregister_writable(fd)
315
- except ExitNow:
316
- raise
317
- except Exception: # noqa
318
- dispatchers[fd].handle_error()
319
- else:
320
- log.debug('unexpected write event from fd %r', fd)
321
- try:
322
- self._poller.unregister_writable(fd)
323
- except Exception: # noqa
324
- pass
228
+ self._io.poll()
325
229
 
326
230
  for group in sorted_groups:
327
231
  for process in group:
@@ -331,17 +235,18 @@ class Supervisor:
331
235
  if depth >= 100:
332
236
  return
333
237
 
334
- pid, sts = self._context.waitpid()
335
- if not pid:
238
+ wp = waitpid()
239
+ log.info(f'Waited pid: {wp}') # noqa
240
+ if wp is None or not wp.pid:
336
241
  return
337
242
 
338
- process = self._pid_history.get(pid, None)
243
+ process = self._pid_history.get(wp.pid, None)
339
244
  if process is None:
340
- _, msg = decode_wait_status(check_not_none(sts))
341
- log.info('reaped unknown pid %s (%s)', pid, msg)
245
+ _, msg = decode_wait_status(wp.sts)
246
+ log.info('reaped unknown pid %s (%s)', wp.pid, msg)
342
247
  else:
343
- process.finish(check_not_none(sts))
344
- del self._pid_history[pid]
248
+ process.finish(wp.sts)
249
+ del self._pid_history[wp.pid]
345
250
 
346
251
  if not once:
347
252
  # keep reaping until no more kids to reap, but don't recurse infinitely
@@ -366,3 +271,31 @@ class Supervisor:
366
271
  if this_tick != last_tick:
367
272
  self._ticks[period] = this_tick
368
273
  self._event_callbacks.notify(event(this_tick, self))
274
+
275
+
276
+ ##
277
+
278
+
279
+ class WaitedPid(ta.NamedTuple):
280
+ pid: Pid
281
+ sts: Rc
282
+
283
+
284
+ def waitpid() -> ta.Optional[WaitedPid]:
285
+ # Need pthread_sigmask here to avoid concurrent sigchld, but Python doesn't offer in Python < 3.4. There is
286
+ # still a race condition here; we can get a sigchld while we're sitting in the waitpid call. However, AFAICT, if
287
+ # waitpid is interrupted by SIGCHLD, as long as we call waitpid again (which happens every so often during the
288
+ # normal course in the mainloop), we'll eventually reap the child that we tried to reap during the interrupted
289
+ # call. At least on Linux, this appears to be true, or at least stopping 50 processes at once never left zombies
290
+ # lying around.
291
+ try:
292
+ pid, sts = os.waitpid(-1, os.WNOHANG)
293
+ except OSError as exc:
294
+ code = exc.args[0]
295
+ if code not in (errno.ECHILD, errno.EINTR):
296
+ log.critical('waitpid error %r; a process may not be cleaned up properly', code)
297
+ if code == errno.EINTR:
298
+ log.debug('EINTR during reap')
299
+ return None
300
+ else:
301
+ return WaitedPid(pid, sts) # type: ignore