ominfra 0.0.0.dev121__py3-none-any.whl → 0.0.0.dev123__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.
@@ -0,0 +1,52 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import signal
3
+ import typing as ta
4
+
5
+
6
+ ##
7
+
8
+
9
+ _SIG_NAMES: ta.Optional[ta.Mapping[int, str]] = None
10
+
11
+
12
+ def sig_name(sig: int) -> str:
13
+ global _SIG_NAMES
14
+ if _SIG_NAMES is None:
15
+ _SIG_NAMES = _init_sig_names()
16
+ return _SIG_NAMES.get(sig) or 'signal %d' % sig
17
+
18
+
19
+ def _init_sig_names() -> ta.Dict[int, str]:
20
+ d = {}
21
+ for k, v in signal.__dict__.items(): # noqa
22
+ k_startswith = getattr(k, 'startswith', None)
23
+ if k_startswith is None:
24
+ continue
25
+ if k_startswith('SIG') and not k_startswith('SIG_'):
26
+ d[v] = k
27
+ return d
28
+
29
+
30
+ ##
31
+
32
+
33
+ class SignalReceiver:
34
+ def __init__(self) -> None:
35
+ super().__init__()
36
+
37
+ self._signals_recvd: ta.List[int] = []
38
+
39
+ def receive(self, sig: int, frame: ta.Any) -> None:
40
+ if sig not in self._signals_recvd:
41
+ self._signals_recvd.append(sig)
42
+
43
+ def install(self, *sigs: int) -> None:
44
+ for sig in sigs:
45
+ signal.signal(sig, self.receive)
46
+
47
+ def get_signal(self) -> ta.Optional[int]:
48
+ if self._signals_recvd:
49
+ sig = self._signals_recvd.pop(0)
50
+ else:
51
+ sig = None
52
+ return sig
@@ -1,29 +1,10 @@
1
- # ruff: noqa: UP006
2
- import typing as ta
3
-
4
- from omlish.lite.check import check_not_none
5
-
6
-
7
- ProcessState = int # ta.TypeAlias
8
- SupervisorState = int # ta.TypeAlias
9
-
10
-
11
- ##
12
-
13
-
14
- def _names_by_code(states: ta.Any) -> ta.Dict[int, str]:
15
- d = {}
16
- for name in states.__dict__:
17
- if not name.startswith('__'):
18
- code = getattr(states, name)
19
- d[code] = name
20
- return d
1
+ import enum
21
2
 
22
3
 
23
4
  ##
24
5
 
25
6
 
26
- class ProcessStates:
7
+ class ProcessState(enum.IntEnum):
27
8
  STOPPED = 0
28
9
  STARTING = 10
29
10
  RUNNING = 20
@@ -33,46 +14,44 @@ class ProcessStates:
33
14
  FATAL = 200
34
15
  UNKNOWN = 1000
35
16
 
17
+ @property
18
+ def stopped(self) -> bool:
19
+ return self in STOPPED_STATES
20
+
21
+ @property
22
+ def running(self) -> bool:
23
+ return self in RUNNING_STATES
24
+
25
+ @property
26
+ def signalable(self) -> bool:
27
+ return self in SIGNALABLE_STATES
28
+
36
29
 
37
30
  STOPPED_STATES = (
38
- ProcessStates.STOPPED,
39
- ProcessStates.EXITED,
40
- ProcessStates.FATAL,
41
- ProcessStates.UNKNOWN,
31
+ ProcessState.STOPPED,
32
+ ProcessState.EXITED,
33
+ ProcessState.FATAL,
34
+ ProcessState.UNKNOWN,
42
35
  )
43
36
 
44
37
  RUNNING_STATES = (
45
- ProcessStates.RUNNING,
46
- ProcessStates.BACKOFF,
47
- ProcessStates.STARTING,
38
+ ProcessState.RUNNING,
39
+ ProcessState.BACKOFF,
40
+ ProcessState.STARTING,
48
41
  )
49
42
 
50
- SIGNALLABLE_STATES = (
51
- ProcessStates.RUNNING,
52
- ProcessStates.STARTING,
53
- ProcessStates.STOPPING,
43
+ SIGNALABLE_STATES = (
44
+ ProcessState.RUNNING,
45
+ ProcessState.STARTING,
46
+ ProcessState.STOPPING,
54
47
  )
55
48
 
56
49
 
57
- _process_states_by_code = _names_by_code(ProcessStates)
58
-
59
-
60
- def get_process_state_description(code: ProcessState) -> str:
61
- return check_not_none(_process_states_by_code.get(code))
62
-
63
-
64
50
  ##
65
51
 
66
52
 
67
- class SupervisorStates:
53
+ class SupervisorState(enum.IntEnum):
68
54
  FATAL = 2
69
55
  RUNNING = 1
70
56
  RESTARTING = 0
71
57
  SHUTDOWN = -1
72
-
73
-
74
- _supervisor_states_by_code = _names_by_code(SupervisorStates)
75
-
76
-
77
- def get_supervisor_state_description(code: SupervisorState) -> str:
78
- return check_not_none(_supervisor_states_by_code.get(code))
@@ -8,28 +8,83 @@ from omlish.lite.cached import cached_nullary
8
8
  from omlish.lite.check import check_not_none
9
9
  from omlish.lite.logs import log
10
10
 
11
- from .compat import ExitNow
12
- from .compat import as_string
13
- from .compat import decode_wait_status
14
- from .compat import signame
15
11
  from .configs import ProcessGroupConfig
16
12
  from .context import ServerContext
17
13
  from .dispatchers import Dispatcher
18
- from .events import EVENT_CALLBACKS
19
14
  from .events import TICK_EVENTS
20
- from .events import ProcessGroupAddedEvent
21
- from .events import ProcessGroupRemovedEvent
15
+ from .events import EventCallbacks
22
16
  from .events import SupervisorRunningEvent
23
17
  from .events import SupervisorStoppingEvent
24
- from .process import ProcessGroup
18
+ from .groups import ProcessGroup
19
+ from .groups import ProcessGroups
20
+ from .poller import Poller
25
21
  from .process import Subprocess
22
+ from .signals import SignalReceiver
23
+ from .signals import sig_name
26
24
  from .states import SupervisorState
27
- from .states import SupervisorStates
28
- from .states import get_process_state_description
25
+ from .utils import ExitNow
26
+ from .utils import as_string
27
+ from .utils import decode_wait_status
28
+ from .utils import timeslice
29
29
 
30
30
 
31
- def timeslice(period: int, when: float) -> int:
32
- return int(when - (when % period))
31
+ ##
32
+
33
+
34
+ class SignalHandler:
35
+ def __init__(
36
+ self,
37
+ *,
38
+ context: ServerContext,
39
+ signal_receiver: SignalReceiver,
40
+ process_groups: ProcessGroups,
41
+ ) -> None:
42
+ super().__init__()
43
+
44
+ self._context = context
45
+ self._signal_receiver = signal_receiver
46
+ self._process_groups = process_groups
47
+
48
+ def set_signals(self) -> None:
49
+ self._signal_receiver.install(
50
+ signal.SIGTERM,
51
+ signal.SIGINT,
52
+ signal.SIGQUIT,
53
+ signal.SIGHUP,
54
+ signal.SIGCHLD,
55
+ signal.SIGUSR2,
56
+ )
57
+
58
+ def handle_signals(self) -> None:
59
+ sig = self._signal_receiver.get_signal()
60
+ if not sig:
61
+ return
62
+
63
+ if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
64
+ log.warning('received %s indicating exit request', sig_name(sig))
65
+ self._context.set_state(SupervisorState.SHUTDOWN)
66
+
67
+ elif sig == signal.SIGHUP:
68
+ if self._context.state == SupervisorState.SHUTDOWN:
69
+ log.warning('ignored %s indicating restart request (shutdown in progress)', sig_name(sig)) # noqa
70
+ else:
71
+ log.warning('received %s indicating restart request', sig_name(sig)) # noqa
72
+ self._context.set_state(SupervisorState.RESTARTING)
73
+
74
+ elif sig == signal.SIGCHLD:
75
+ log.debug('received %s indicating a child quit', sig_name(sig))
76
+
77
+ elif sig == signal.SIGUSR2:
78
+ log.info('received %s indicating log reopen request', sig_name(sig))
79
+
80
+ for group in self._process_groups:
81
+ group.reopen_logs()
82
+
83
+ else:
84
+ log.debug('received %s indicating nothing', sig_name(sig))
85
+
86
+
87
+ ##
33
88
 
34
89
 
35
90
  @dc.dataclass(frozen=True)
@@ -41,25 +96,26 @@ class ProcessGroupFactory:
41
96
 
42
97
 
43
98
  class Supervisor:
44
-
45
99
  def __init__(
46
100
  self,
47
- context: ServerContext,
48
101
  *,
49
- process_group_factory: ta.Optional[ProcessGroupFactory] = None,
102
+ context: ServerContext,
103
+ poller: Poller,
104
+ process_groups: ProcessGroups,
105
+ signal_handler: SignalHandler,
106
+ event_callbacks: EventCallbacks,
107
+ process_group_factory: ProcessGroupFactory,
50
108
  ) -> None:
51
109
  super().__init__()
52
110
 
53
111
  self._context = context
54
-
55
- if process_group_factory is None:
56
- def make_process_group(config: ProcessGroupConfig) -> ProcessGroup:
57
- return ProcessGroup(config, self._context)
58
- process_group_factory = ProcessGroupFactory(make_process_group)
112
+ self._poller = poller
113
+ self._process_groups = process_groups
114
+ self._signal_handler = signal_handler
115
+ self._event_callbacks = event_callbacks
59
116
  self._process_group_factory = process_group_factory
60
117
 
61
118
  self._ticks: ta.Dict[int, float] = {}
62
- self._process_groups: ta.Dict[str, ProcessGroup] = {} # map of process group name to process group object
63
119
  self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
64
120
  self._stopping = False # set after we detect that we are handling a stop request
65
121
  self._last_shutdown_report = 0. # throttle for delayed process error reports at stop
@@ -82,7 +138,7 @@ class Supervisor:
82
138
 
83
139
  def diff_to_active(self) -> DiffToActive:
84
140
  new = self._context.config.groups or []
85
- cur = [group.config for group in self._process_groups.values()]
141
+ cur = [group.config for group in self._process_groups]
86
142
 
87
143
  curdict = dict(zip([cfg.name for cfg in cur], cur))
88
144
  newdict = dict(zip([cfg.name for cfg in new], new))
@@ -95,38 +151,35 @@ class Supervisor:
95
151
  return Supervisor.DiffToActive(added, changed, removed)
96
152
 
97
153
  def add_process_group(self, config: ProcessGroupConfig) -> bool:
98
- name = config.name
99
- if name in self._process_groups:
154
+ if self._process_groups.get(config.name) is not None:
100
155
  return False
101
156
 
102
- group = self._process_groups[name] = self._process_group_factory(config)
157
+ group = self._process_group_factory(config)
103
158
  group.after_setuid()
104
159
 
105
- EVENT_CALLBACKS.notify(ProcessGroupAddedEvent(name))
160
+ self._process_groups.add(group)
161
+
106
162
  return True
107
163
 
108
164
  def remove_process_group(self, name: str) -> bool:
109
165
  if self._process_groups[name].get_unstopped_processes():
110
166
  return False
111
167
 
112
- self._process_groups[name].before_remove()
113
-
114
- del self._process_groups[name]
168
+ self._process_groups.remove(name)
115
169
 
116
- EVENT_CALLBACKS.notify(ProcessGroupRemovedEvent(name))
117
170
  return True
118
171
 
119
172
  def get_process_map(self) -> ta.Dict[int, Dispatcher]:
120
173
  process_map = {}
121
- for group in self._process_groups.values():
174
+ for group in self._process_groups:
122
175
  process_map.update(group.get_dispatchers())
123
176
  return process_map
124
177
 
125
178
  def shutdown_report(self) -> ta.List[Subprocess]:
126
179
  unstopped: ta.List[Subprocess] = []
127
180
 
128
- for group in self._process_groups.values():
129
- unstopped.extend(group.get_unstopped_processes())
181
+ for group in self._process_groups:
182
+ unstopped.extend(group.get_unstopped_processes()) # type: ignore
130
183
 
131
184
  if unstopped:
132
185
  # throttle 'waiting for x to die' reports
@@ -137,8 +190,7 @@ class Supervisor:
137
190
  log.info('waiting for %s to die', namestr)
138
191
  self._last_shutdown_report = now
139
192
  for proc in unstopped:
140
- state = get_process_state_description(proc.get_state())
141
- log.debug('%s state: %s', proc.config.name, state)
193
+ log.debug('%s state: %s', proc.config.name, proc.get_state().name)
142
194
 
143
195
  return unstopped
144
196
 
@@ -169,16 +221,16 @@ class Supervisor:
169
221
  *,
170
222
  callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
171
223
  ) -> None:
172
- self._process_groups = {} # clear
224
+ self._process_groups.clear()
173
225
  self._stop_groups = None # clear
174
226
 
175
- EVENT_CALLBACKS.clear()
227
+ self._event_callbacks.clear()
176
228
 
177
229
  try:
178
230
  for config in self._context.config.groups or []:
179
231
  self.add_process_group(config)
180
232
 
181
- self._context.set_signals()
233
+ self._signal_handler.set_signals()
182
234
 
183
235
  if not self._context.config.nodaemon and self._context.first:
184
236
  self._context.daemonize()
@@ -186,7 +238,7 @@ class Supervisor:
186
238
  # writing pid file needs to come *after* daemonizing or pid will be wrong
187
239
  self._context.write_pidfile()
188
240
 
189
- EVENT_CALLBACKS.notify(SupervisorRunningEvent())
241
+ self._event_callbacks.notify(SupervisorRunningEvent())
190
242
 
191
243
  while True:
192
244
  if callback is not None and not callback(self):
@@ -202,10 +254,10 @@ class Supervisor:
202
254
  def _run_once(self) -> None:
203
255
  self._poll()
204
256
  self._reap()
205
- self._handle_signal()
257
+ self._signal_handler.handle_signals()
206
258
  self._tick()
207
259
 
208
- if self._context.state < SupervisorStates.RUNNING:
260
+ if self._context.state < SupervisorState.RUNNING:
209
261
  self._ordered_stop_groups_phase_2()
210
262
 
211
263
  def _ordered_stop_groups_phase_1(self) -> None:
@@ -228,15 +280,15 @@ class Supervisor:
228
280
  combined_map = {}
229
281
  combined_map.update(self.get_process_map())
230
282
 
231
- pgroups = list(self._process_groups.values())
283
+ pgroups = list(self._process_groups)
232
284
  pgroups.sort()
233
285
 
234
- if self._context.state < SupervisorStates.RUNNING:
286
+ if self._context.state < SupervisorState.RUNNING:
235
287
  if not self._stopping:
236
288
  # first time, set the stopping flag, do a notification and set stop_groups
237
289
  self._stopping = True
238
290
  self._stop_groups = pgroups[:]
239
- EVENT_CALLBACKS.notify(SupervisorStoppingEvent())
291
+ self._event_callbacks.notify(SupervisorStoppingEvent())
240
292
 
241
293
  self._ordered_stop_groups_phase_1()
242
294
 
@@ -246,12 +298,12 @@ class Supervisor:
246
298
 
247
299
  for fd, dispatcher in combined_map.items():
248
300
  if dispatcher.readable():
249
- self._context.poller.register_readable(fd)
301
+ self._poller.register_readable(fd)
250
302
  if dispatcher.writable():
251
- self._context.poller.register_writable(fd)
303
+ self._poller.register_writable(fd)
252
304
 
253
305
  timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
254
- r, w = self._context.poller.poll(timeout)
306
+ r, w = self._poller.poll(timeout)
255
307
 
256
308
  for fd in r:
257
309
  if fd in combined_map:
@@ -260,7 +312,7 @@ class Supervisor:
260
312
  log.debug('read event caused by %r', dispatcher)
261
313
  dispatcher.handle_read_event()
262
314
  if not dispatcher.readable():
263
- self._context.poller.unregister_readable(fd)
315
+ self._poller.unregister_readable(fd)
264
316
  except ExitNow:
265
317
  raise
266
318
  except Exception: # noqa
@@ -270,7 +322,7 @@ class Supervisor:
270
322
  # time, which may cause 100% cpu usage
271
323
  log.debug('unexpected read event from fd %r', fd)
272
324
  try:
273
- self._context.poller.unregister_readable(fd)
325
+ self._poller.unregister_readable(fd)
274
326
  except Exception: # noqa
275
327
  pass
276
328
 
@@ -281,7 +333,7 @@ class Supervisor:
281
333
  log.debug('write event caused by %r', dispatcher)
282
334
  dispatcher.handle_write_event()
283
335
  if not dispatcher.writable():
284
- self._context.poller.unregister_writable(fd)
336
+ self._poller.unregister_writable(fd)
285
337
  except ExitNow:
286
338
  raise
287
339
  except Exception: # noqa
@@ -289,7 +341,7 @@ class Supervisor:
289
341
  else:
290
342
  log.debug('unexpected write event from fd %r', fd)
291
343
  try:
292
- self._context.poller.unregister_writable(fd)
344
+ self._poller.unregister_writable(fd)
293
345
  except Exception: # noqa
294
346
  pass
295
347
 
@@ -316,34 +368,6 @@ class Supervisor:
316
368
  # keep reaping until no more kids to reap, but don't recurse infinitely
317
369
  self._reap(once=False, depth=depth + 1)
318
370
 
319
- def _handle_signal(self) -> None:
320
- sig = self._context.get_signal()
321
- if not sig:
322
- return
323
-
324
- if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
325
- log.warning('received %s indicating exit request', signame(sig))
326
- self._context.set_state(SupervisorStates.SHUTDOWN)
327
-
328
- elif sig == signal.SIGHUP:
329
- if self._context.state == SupervisorStates.SHUTDOWN:
330
- log.warning('ignored %s indicating restart request (shutdown in progress)', signame(sig)) # noqa
331
- else:
332
- log.warning('received %s indicating restart request', signame(sig)) # noqa
333
- self._context.set_state(SupervisorStates.RESTARTING)
334
-
335
- elif sig == signal.SIGCHLD:
336
- log.debug('received %s indicating a child quit', signame(sig))
337
-
338
- elif sig == signal.SIGUSR2:
339
- log.info('received %s indicating log reopen request', signame(sig))
340
- # self._context.reopen_logs()
341
- for group in self._process_groups.values():
342
- group.reopen_logs()
343
-
344
- else:
345
- log.debug('received %s indicating nothing', signame(sig))
346
-
347
371
  def _tick(self, now: ta.Optional[float] = None) -> None:
348
372
  """Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
349
373
 
@@ -352,7 +376,7 @@ class Supervisor:
352
376
  now = time.time()
353
377
 
354
378
  for event in TICK_EVENTS:
355
- period = event.period # type: ignore
379
+ period = event.period
356
380
 
357
381
  last_tick = self._ticks.get(period)
358
382
  if last_tick is None:
@@ -362,4 +386,4 @@ class Supervisor:
362
386
  this_tick = timeslice(period, now)
363
387
  if this_tick != last_tick:
364
388
  self._ticks[period] = this_tick
365
- EVENT_CALLBACKS.notify(event(this_tick, self))
389
+ self._event_callbacks.notify(event(this_tick, self))
@@ -1,9 +1,12 @@
1
- # ruff: noqa: UP006
1
+ # ruff: noqa: UP006 UP007
2
2
  import abc
3
+ import functools
3
4
  import typing as ta
4
5
 
5
6
  from .configs import ProcessConfig
7
+ from .configs import ProcessGroupConfig
6
8
  from .configs import ServerConfig
9
+ from .states import ProcessState
7
10
  from .states import SupervisorState
8
11
 
9
12
 
@@ -27,12 +30,8 @@ class AbstractServerContext(abc.ABC):
27
30
  def pid_history(self) -> ta.Dict[int, 'AbstractSubprocess']:
28
31
  raise NotImplementedError
29
32
 
30
- @property
31
- @abc.abstractmethod
32
- def inherited_fds(self) -> ta.FrozenSet[int]:
33
- raise NotImplementedError
34
-
35
33
 
34
+ @functools.total_ordering
36
35
  class AbstractSubprocess(abc.ABC):
37
36
  @property
38
37
  @abc.abstractmethod
@@ -44,6 +43,12 @@ class AbstractSubprocess(abc.ABC):
44
43
  def config(self) -> ProcessConfig:
45
44
  raise NotImplementedError
46
45
 
46
+ def __lt__(self, other):
47
+ return self.config.priority < other.config.priority
48
+
49
+ def __eq__(self, other):
50
+ return self.config.priority == other.config.priority
51
+
47
52
  @property
48
53
  @abc.abstractmethod
49
54
  def context(self) -> AbstractServerContext:
@@ -52,3 +57,49 @@ class AbstractSubprocess(abc.ABC):
52
57
  @abc.abstractmethod
53
58
  def finish(self, sts: int) -> None:
54
59
  raise NotImplementedError
60
+
61
+ @abc.abstractmethod
62
+ def remove_logs(self) -> None:
63
+ raise NotImplementedError
64
+
65
+ @abc.abstractmethod
66
+ def reopen_logs(self) -> None:
67
+ raise NotImplementedError
68
+
69
+ @abc.abstractmethod
70
+ def stop(self) -> ta.Optional[str]:
71
+ raise NotImplementedError
72
+
73
+ @abc.abstractmethod
74
+ def give_up(self) -> None:
75
+ raise NotImplementedError
76
+
77
+ @abc.abstractmethod
78
+ def transition(self) -> None:
79
+ raise NotImplementedError
80
+
81
+ @abc.abstractmethod
82
+ def get_state(self) -> ProcessState:
83
+ raise NotImplementedError
84
+
85
+ @abc.abstractmethod
86
+ def create_auto_child_logs(self) -> None:
87
+ raise NotImplementedError
88
+
89
+ @abc.abstractmethod
90
+ def get_dispatchers(self) -> ta.Mapping[int, ta.Any]: # dict[int, Dispatcher]
91
+ raise NotImplementedError
92
+
93
+
94
+ @functools.total_ordering
95
+ class AbstractProcessGroup(abc.ABC):
96
+ @property
97
+ @abc.abstractmethod
98
+ def config(self) -> ProcessGroupConfig:
99
+ raise NotImplementedError
100
+
101
+ def __lt__(self, other):
102
+ return self.config.priority < other.config.priority
103
+
104
+ def __eq__(self, other):
105
+ return self.config.priority == other.config.priority