ominfra 0.0.0.dev122__py3-none-any.whl → 0.0.0.dev124__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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