moat-kv 0.70.24__py3-none-any.whl → 0.71.6__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.
Files changed (127) hide show
  1. moat/kv/__init__.py +6 -7
  2. moat/kv/_cfg.yaml +5 -8
  3. moat/kv/actor/__init__.py +2 -1
  4. moat/kv/actor/deletor.py +4 -1
  5. moat/kv/auth/__init__.py +12 -13
  6. moat/kv/auth/_test.py +4 -1
  7. moat/kv/auth/password.py +11 -7
  8. moat/kv/backend/mqtt.py +4 -8
  9. moat/kv/client.py +20 -39
  10. moat/kv/code.py +3 -3
  11. moat/kv/command/data.py +4 -3
  12. moat/kv/command/dump/__init__.py +29 -29
  13. moat/kv/command/internal.py +2 -3
  14. moat/kv/command/job.py +1 -2
  15. moat/kv/command/type.py +3 -6
  16. moat/kv/data.py +9 -8
  17. moat/kv/errors.py +16 -8
  18. moat/kv/mock/__init__.py +2 -12
  19. moat/kv/model.py +28 -32
  20. moat/kv/obj/__init__.py +3 -3
  21. moat/kv/obj/command.py +3 -3
  22. moat/kv/runner.py +4 -5
  23. moat/kv/server.py +106 -126
  24. moat/kv/types.py +8 -6
  25. {moat_kv-0.70.24.dist-info → moat_kv-0.71.6.dist-info}/METADATA +7 -6
  26. moat_kv-0.71.6.dist-info/RECORD +47 -0
  27. {moat_kv-0.70.24.dist-info → moat_kv-0.71.6.dist-info}/WHEEL +1 -1
  28. moat_kv-0.71.6.dist-info/licenses/LICENSE +3 -0
  29. moat_kv-0.71.6.dist-info/licenses/LICENSE.APACHE2 +202 -0
  30. moat_kv-0.71.6.dist-info/licenses/LICENSE.MIT +20 -0
  31. moat_kv-0.71.6.dist-info/top_level.txt +1 -0
  32. build/lib/docs/source/conf.py +0 -201
  33. build/lib/examples/pathify.py +0 -45
  34. build/lib/moat/kv/__init__.py +0 -19
  35. build/lib/moat/kv/_cfg.yaml +0 -97
  36. build/lib/moat/kv/_main.py +0 -91
  37. build/lib/moat/kv/actor/__init__.py +0 -98
  38. build/lib/moat/kv/actor/deletor.py +0 -139
  39. build/lib/moat/kv/auth/__init__.py +0 -444
  40. build/lib/moat/kv/auth/_test.py +0 -166
  41. build/lib/moat/kv/auth/password.py +0 -234
  42. build/lib/moat/kv/auth/root.py +0 -58
  43. build/lib/moat/kv/backend/__init__.py +0 -67
  44. build/lib/moat/kv/backend/mqtt.py +0 -74
  45. build/lib/moat/kv/backend/serf.py +0 -45
  46. build/lib/moat/kv/client.py +0 -1025
  47. build/lib/moat/kv/code.py +0 -236
  48. build/lib/moat/kv/codec.py +0 -11
  49. build/lib/moat/kv/command/__init__.py +0 -1
  50. build/lib/moat/kv/command/acl.py +0 -180
  51. build/lib/moat/kv/command/auth.py +0 -261
  52. build/lib/moat/kv/command/code.py +0 -293
  53. build/lib/moat/kv/command/codec.py +0 -186
  54. build/lib/moat/kv/command/data.py +0 -265
  55. build/lib/moat/kv/command/dump/__init__.py +0 -143
  56. build/lib/moat/kv/command/error.py +0 -149
  57. build/lib/moat/kv/command/internal.py +0 -248
  58. build/lib/moat/kv/command/job.py +0 -433
  59. build/lib/moat/kv/command/log.py +0 -53
  60. build/lib/moat/kv/command/server.py +0 -114
  61. build/lib/moat/kv/command/type.py +0 -201
  62. build/lib/moat/kv/config.py +0 -46
  63. build/lib/moat/kv/data.py +0 -216
  64. build/lib/moat/kv/errors.py +0 -561
  65. build/lib/moat/kv/exceptions.py +0 -126
  66. build/lib/moat/kv/mock/__init__.py +0 -101
  67. build/lib/moat/kv/mock/mqtt.py +0 -159
  68. build/lib/moat/kv/mock/serf.py +0 -250
  69. build/lib/moat/kv/mock/tracer.py +0 -63
  70. build/lib/moat/kv/model.py +0 -1069
  71. build/lib/moat/kv/obj/__init__.py +0 -646
  72. build/lib/moat/kv/obj/command.py +0 -241
  73. build/lib/moat/kv/runner.py +0 -1347
  74. build/lib/moat/kv/server.py +0 -2809
  75. build/lib/moat/kv/types.py +0 -513
  76. debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +0 -201
  77. debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +0 -45
  78. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +0 -19
  79. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +0 -97
  80. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +0 -91
  81. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +0 -98
  82. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +0 -139
  83. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +0 -444
  84. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +0 -166
  85. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +0 -234
  86. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +0 -58
  87. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +0 -67
  88. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +0 -74
  89. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/serf.py +0 -45
  90. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +0 -1025
  91. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +0 -236
  92. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +0 -11
  93. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +0 -1
  94. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +0 -180
  95. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +0 -261
  96. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +0 -293
  97. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +0 -186
  98. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +0 -265
  99. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +0 -143
  100. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +0 -149
  101. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +0 -248
  102. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +0 -433
  103. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +0 -53
  104. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +0 -114
  105. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +0 -201
  106. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +0 -46
  107. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +0 -216
  108. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +0 -561
  109. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +0 -126
  110. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +0 -101
  111. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +0 -159
  112. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/serf.py +0 -250
  113. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +0 -63
  114. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +0 -1069
  115. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +0 -646
  116. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +0 -241
  117. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +0 -1347
  118. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +0 -2809
  119. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +0 -513
  120. docs/source/conf.py +0 -201
  121. examples/pathify.py +0 -45
  122. moat/kv/backend/serf.py +0 -45
  123. moat/kv/codec.py +0 -11
  124. moat/kv/mock/serf.py +0 -250
  125. moat_kv-0.70.24.dist-info/RECORD +0 -137
  126. moat_kv-0.70.24.dist-info/top_level.txt +0 -9
  127. {moat_kv-0.70.24.dist-info → moat_kv-0.71.6.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,1347 +0,0 @@
1
- """
2
- This module's job is to run code, resp. to keep it running.
3
-
4
-
5
- """
6
- from __future__ import annotations
7
-
8
- import time
9
- from collections.abc import Mapping
10
- from contextlib import AsyncExitStack, asynccontextmanager
11
- from weakref import ref
12
-
13
- import anyio
14
- import psutil
15
- from asyncactor import AuthPingEvent, NodeList, PingEvent, TagEvent, UntagEvent
16
- from moat.util import (
17
- NotGiven,
18
- P,
19
- Path,
20
- attrdict,
21
- combine_dict,
22
- create_queue,
23
- digits,
24
- logger_for,
25
- spawn,
26
- )
27
-
28
- from .actor import (
29
- ActorState,
30
- BrokenState,
31
- ClientActor,
32
- CompleteState,
33
- DetachedState,
34
- PartialState,
35
- )
36
- from .exceptions import ServerError
37
- from .obj import AttrClientEntry, ClientRoot, MirrorRoot
38
-
39
- try:
40
- ClosedResourceError = anyio.exceptions.ClosedResourceError
41
- except AttributeError:
42
- ClosedResourceError = anyio.ClosedResourceError
43
-
44
- import logging
45
-
46
- logger = logging.getLogger(__name__)
47
-
48
- QLEN = 10
49
-
50
-
51
- class NotSelected(RuntimeError):
52
- """
53
- This node has not been selected for a very long time. Something is amiss.
54
- """
55
-
56
- pass
57
-
58
-
59
- class ErrorRecorded(RuntimeError):
60
- pass
61
-
62
-
63
- class RunnerMsg(ActorState):
64
- """Superclass for runner-generated messages.
65
-
66
- Not directly instantiated.
67
-
68
- This message and its descendants take one opaque parameter: ``msg``.
69
- """
70
-
71
- pass
72
-
73
-
74
- class ChangeMsg(RunnerMsg):
75
- """A message telling your code that some entry has been updated.
76
-
77
- Subclass this and use it as `CallAdmin.watch`'s ``cls`` parameter for easier
78
- disambiguation.
79
-
80
- The runner sets ``path`` and ``value`` attributes.
81
- """
82
-
83
- pass
84
-
85
-
86
- class MQTTmsg(RunnerMsg):
87
- """A message transporting some MQTT data.
88
-
89
- `value` is the MsgPack-decoded content. If that doesn't exist the
90
- message is not decodeable.
91
-
92
- The runner also sets the ``path`` attribute.
93
- """
94
-
95
- pass
96
-
97
-
98
- class ReadyMsg(RunnerMsg):
99
- """
100
- This message is queued when the last watcher has read all data.
101
- """
102
-
103
- pass
104
-
105
-
106
- class TimerMsg(RunnerMsg):
107
- """
108
- A message telling your code that a timer triggers.
109
-
110
- Subclass this and use it as `CallAdmin.timer`'s ``cls`` parameter for easier
111
- disambiguation.
112
- """
113
-
114
- pass
115
-
116
-
117
- _CLASSES = attrdict()
118
- for _c in (
119
- DetachedState,
120
- PartialState,
121
- CompleteState,
122
- ActorState,
123
- BrokenState,
124
- TimerMsg,
125
- ReadyMsg,
126
- ChangeMsg,
127
- MQTTmsg,
128
- RunnerMsg,
129
- ErrorRecorded,
130
- ):
131
- _CLASSES[_c.__name__] = _c
132
-
133
- _CLASSES["NotGiven"] = NotGiven # ellipsis
134
-
135
-
136
- class CallAdmin:
137
- """
138
- This class collects some standard tasks which async MoaT-KV-embedded
139
- code might want to do.
140
- """
141
-
142
- _taskgroup = None
143
- _stack = None
144
- _restart = False
145
- _n_watch: int = None
146
- _n_watch_seen: int = None
147
-
148
- def __init__(self, runner, state, data):
149
- self._runner = runner
150
- self._state = state
151
- self._data = data
152
- self._client = runner.root.client
153
- self._err = runner.root.err
154
- self._q = runner._q
155
- self._path = runner._path
156
- self._subpath = runner.subpath
157
- self._logger = runner._logger
158
-
159
- async def _run(self, code, data):
160
- while True:
161
- self._n_watch = 0
162
- self._n_watch_seen = 0
163
-
164
- res = await self._run2(code, data)
165
- if self._restart:
166
- self._restart = False
167
- else:
168
- return res
169
-
170
- async def _run2(self, code, data):
171
- """Called by the runner to actually execute the code."""
172
- self._logger.debug("Start %s with %s", self._runner._path, self._runner.code)
173
- async with (
174
- anyio.create_task_group() as tg,
175
- AsyncExitStack() as stack,
176
- ):
177
- self._stack = stack
178
- self._taskgroup = tg
179
- self._runner.scope = sc = tg.cancel_scope
180
-
181
- data["_self"] = self
182
-
183
- oka = getattr(self._runner, "ok_after", 0)
184
- if oka > 0:
185
-
186
- async def is_ok(oka):
187
- await anyio.sleep(oka)
188
- await self.setup_done()
189
-
190
- tg.start_soon(is_ok, oka)
191
- tg.start_soon(self._changed_code, code)
192
-
193
- await self._runner.send_event(ReadyMsg(0))
194
- res = code(**data)
195
- if code.is_async is not None:
196
- res = await res
197
-
198
- sc.cancel()
199
- return res
200
-
201
- async def _pinger(self):
202
- t = self._runner.ok_after or 10
203
- await anyio.sleep(t / 2)
204
- while True:
205
- await self._runner.state.ping()
206
- await anyio.sleep(t * 5)
207
-
208
- async def _changed_code(self, code):
209
- """
210
- Kill the job if the underlying code has changed
211
- """
212
- await code.reload_event.wait()
213
- self._restart = True
214
- self.cancel()
215
-
216
- def cancel(self):
217
- """
218
- Cancel the running task
219
- """
220
- self._taskgroup.cancel_scope.cancel()
221
-
222
- async def spawn(self, proc, *a, **kw):
223
- """
224
- Start a background subtask.
225
-
226
- The task is auto-cancelled when your code ends.
227
-
228
- Returns: an `anyio.abc.CancelScope` which you can use to cancel the
229
- subtask.
230
- """
231
- return await spawn(self._taskgroup, proc, *a, **kw)
232
-
233
- async def setup_done(self, **kw):
234
- """
235
- Call this when your code has successfully started up.
236
- """
237
- self._state.backoff = 0
238
- await self._state.save()
239
- await self._err.record_working("run", self._runner._path, **kw)
240
-
241
- async def error(self, path=None, **kw):
242
- """
243
- Record that an error has occurred. This function records specific
244
- error data, then raises `ErrorRecorded` which the code is not
245
- supposed to catch.
246
-
247
- See `moat.kv.errors.ErrorRoot.record_error` for keyword details. The
248
- ``path`` argument is auto-filled to point to the current task.
249
- """
250
- if path is None:
251
- path = self._path
252
- r = await self._err.record_error("run", path, **kw)
253
- await self._err.root.wait_chain(r.chain)
254
- raise ErrorRecorded
255
-
256
- async def open_context(self, ctx):
257
- return await self._stack.enter_async_context(ctx)
258
-
259
- async def watch(self, path, cls=ChangeMsg, **kw):
260
- """
261
- Create a watcher. This path is monitored as per `moat.kv.client.Client.watch`;
262
- messages are encapsulated in `ChangeMsg` objects.
263
- A `ReadyMsg` will be sent when all watchers have transmitted their
264
- initial state.
265
-
266
- By default a watcher will only monitor a single entry. Set
267
- ``max_depth`` if you also want child entries.
268
-
269
- By default a watcher will not report existing entries. Set
270
- ``fetch=True`` if you want them.
271
- """
272
-
273
- class Watcher:
274
- """Helper class for watching an entry"""
275
-
276
- # pylint: disable=no-self-argument
277
-
278
- def __init__(slf, admin, runner, client, cls, path, kw):
279
- kw.setdefault("fetch", True)
280
-
281
- slf.admin = admin
282
- slf.runner = runner
283
- slf.client = client
284
- slf.path = path
285
- slf.kw = kw
286
- slf.cls = cls
287
- slf.scope = None
288
-
289
- async def run(slf):
290
- @asynccontextmanager
291
- async def _watch(path, kw):
292
- if path.mark == "r":
293
- async with slf.client.msg_monitor(path, **kw) as watcher:
294
- yield watcher
295
- elif not path.mark:
296
- async with slf.client.watch(path, **kw) as watcher:
297
- yield watcher
298
- else:
299
- raise RuntimeError(f"What should I do with a path marked {path.mark!r}?")
300
-
301
- with anyio.CancelScope() as sc:
302
- slf.scope = sc
303
- async with _watch(path, kw) as watcher:
304
- async for msg in watcher:
305
- if "path" in msg:
306
- chg = cls(msg)
307
- try:
308
- chg.value = ( # pylint:disable=attribute-defined-outside-init
309
- msg.value
310
- )
311
- except AttributeError:
312
- pass
313
- chg.path = ( # pylint:disable=attribute-defined-outside-init
314
- msg.path
315
- )
316
- await slf.runner.send_event(chg)
317
-
318
- elif msg.get("state", "") == "uptodate":
319
- slf.admin._n_watch_seen += 1
320
- if slf.admin._n_watch_seen == slf.admin._n_watch:
321
- await slf.runner.send_event(ReadyMsg(slf.admin._n_watch_seen))
322
-
323
- def cancel(slf):
324
- if slf.scope is None:
325
- return False
326
- sc, slf.scope = slf.scope, None
327
- sc.cancel()
328
-
329
- if isinstance(path, (tuple, list)):
330
- path = Path.build(path)
331
- elif not isinstance(path, Path):
332
- raise RuntimeError(f"You didn't pass in a path: {path!r}")
333
-
334
- kw.setdefault("max_depth", 0)
335
- if kw.setdefault("fetch", True):
336
- self._n_watch += 1
337
-
338
- w = Watcher(self, self._runner, self._client, cls, path, kw)
339
- self._taskgroup.start_soon(w.run)
340
- return w
341
-
342
- async def send(self, path, value=NotGiven, raw=None):
343
- """
344
- Publish an MQTT message.
345
-
346
- Set either ``value`` or ``raw``.
347
- """
348
- if isinstance(path, (tuple, list)):
349
- path = Path.build(path)
350
- elif not isinstance(path, Path):
351
- raise RuntimeError(f"You didn't pass in a path: {path!r}")
352
-
353
- await self._client.msg_send(topic=path, data=value, raw=raw)
354
-
355
- async def set(self, path, value, chain=NotGiven):
356
- """
357
- Set a MoaT-KV value.
358
- """
359
- if path.mark == "r":
360
- return await self.send(path, value)
361
- elif path.mark:
362
- raise RuntimeError(f"What should I do with a path marked {path.mark!r}")
363
-
364
- if isinstance(path, (tuple, list)):
365
- path = Path.build(path)
366
- elif not isinstance(path, Path):
367
- raise RuntimeError(f"You didn't pass in a path: {path!r}")
368
-
369
- return await self._client.set(path, value=value, chain=chain)
370
-
371
- async def get(self, path, value):
372
- """
373
- Get a MoaT-KV value.
374
- """
375
- if isinstance(path, (tuple, list)):
376
- path = Path.build(path)
377
- elif not isinstance(path, Path):
378
- raise RuntimeError(f"You didn't pass in a path: {path!r}")
379
-
380
- res = await self._client.get(path, value=value) # TODO chain
381
-
382
- if "value" in res:
383
- return res.value
384
- else:
385
- return KeyError(path)
386
-
387
- async def monitor(self, path, cls=MQTTmsg, **kw):
388
- """
389
- Create an MQTT monitor.
390
- Messages are encapsulated in `MQTTmsg` objects.
391
-
392
- By default a monitor will only monitor a single entry. You may use
393
- MQTT wildcards.
394
-
395
- The message is decoded and stored in the ``value`` attribute unless
396
- it's either undecodeable or ``raw`` is set, in which case it's
397
- stored in ``.msg``. The topic the message was sent to is in
398
- ``topic``.
399
- """
400
-
401
- class Monitor:
402
- """Helper class for reading an MQTT topic"""
403
-
404
- def __init__(self, admin, runner, client, cls, path, kw):
405
- self.admin = admin
406
- self.runner = runner
407
- self.client = client
408
- self.path = path
409
- self.kw = kw
410
- self.cls = cls
411
- self.scope = None
412
-
413
- async def run(self):
414
- with anyio.CancelScope() as sc:
415
- self.scope = sc
416
- async with self.client.msg_monitor(path, **kw) as watcher:
417
- async for msg in watcher:
418
- if "topic" in msg:
419
- # pylint:disable=attribute-defined-outside-init
420
- chg = cls(msg.get("raw", None))
421
- try:
422
- chg.value = msg.data
423
- except AttributeError:
424
- pass
425
- chg.path = Path.build(msg.topic)
426
- await self.runner.send_event(chg)
427
-
428
- async def cancel(self):
429
- if self.scope is None:
430
- return False
431
- sc, self.scope = self.scope, None
432
- sc.cancel()
433
-
434
- if isinstance(path, (tuple, list)):
435
- path = Path.build(path)
436
- elif not isinstance(path, Path):
437
- raise RuntimeError(f"You didn't pass in a path: {path!r}")
438
-
439
- w = Monitor(self, self._runner, self._client, cls, path, kw)
440
- self._taskgroup.start_soon(w.run)
441
- return w
442
-
443
- async def timer(self, delay, cls=TimerMsg):
444
- class Timer:
445
- def __init__(self, runner, cls, tg):
446
- self.runner = runner
447
- self.cls = cls
448
- self.scope = None
449
- self._taskgroup = tg
450
- self.delay = None
451
-
452
- async def _run(self):
453
- with anyio.CancelScope() as sc:
454
- self.scope = sc
455
- await anyio.sleep(self.delay)
456
- self.scope = None
457
- await self.runner.send_event(self.cls(self))
458
-
459
- def cancel(self):
460
- if self.scope is None:
461
- return False
462
- sc, self.scope = self.scope, None
463
- sc.cancel()
464
- return True
465
-
466
- async def run(self, delay):
467
- self.cancel()
468
- self.delay = delay
469
- if self.delay > 0:
470
- self._taskgroup.start_soon(t._run)
471
-
472
- t = Timer(self._runner, cls, self._taskgroup)
473
- await t.run(delay)
474
- return t
475
-
476
-
477
- class RunnerEntry(AttrClientEntry):
478
- """
479
- An entry representing some hopefully-running code.
480
-
481
- The code will run some time after ``target`` has passed.
482
- On success, it will run again ``repeat`` seconds later (if >0).
483
- On error, it will run ``delay`` seconds later (if >0), multiplied by 2**backoff.
484
-
485
- Arguments:
486
- code (list): pointer to the code that's to be started.
487
- data (dict): additional data for the code.
488
- delay (float): time before restarting the job on error.
489
- Default 100.
490
- repeat (float): time before restarting on success.
491
- Default: zero: no restart.
492
- target (float): time the job should be started at.
493
- Default: zero: don't start.
494
- ok_after (float): the job is marked OK if it has run this long.
495
- Default: zero: the code will do that itself.
496
- backoff (float): Exponential back-off factor on errors.
497
- Default: 1.1.
498
-
499
- The code runs with these additional keywords::
500
-
501
- _self: the `CallEnv` object, which the task can use to actually do things.
502
- _client: the MoaT-KV client connection.
503
- _info: a queue which the task can use to receive events. A message of
504
- ``None`` signals that the queue was overflowing and no further
505
- messages will be delivered. Your task should use that as its
506
- mainloop.
507
- _P: build a path from a string
508
- _Path: build a path from its arguments
509
-
510
- Some possible messages are defined in :mod:`moat.kv.actor`.
511
- """
512
-
513
- ATTRS = "code data delay ok_after repeat backoff target".split()
514
-
515
- delay = 100 # timedelta, before restarting
516
- repeat = 0
517
- target = 0
518
- backoff = 1.1
519
- ok_after = 0
520
-
521
- code = () # what to execute
522
- data = None
523
- scope = None # scope to kill off
524
- _comment = None # used for error entries, i.e. mainly Cancel
525
- _q = None # send events to the running task. Async tasks only.
526
- _running = False # .run is active. Careful with applying updates.
527
- _task = None
528
- retry = None
529
-
530
- def __init__(self, *a, **k):
531
- self.data = {} # local data
532
-
533
- super().__init__(*a, **k)
534
-
535
- self._logger = logger_for(self._path)
536
-
537
- def __repr__(self):
538
- return "<%s %r:%r>" % (self.__class__.__name__, self.subpath, self.code)
539
-
540
- @property
541
- def state(self):
542
- return self.root.state.follow(self.subpath, create=None)
543
-
544
- async def run(self):
545
- if self.code is None:
546
- return # nothing to do here
547
-
548
- state = self.state
549
- try:
550
- self._running = True
551
- try:
552
- self._logger.debug("Start")
553
- if state.node is not None:
554
- raise RuntimeError(f"already running on {state.node}")
555
- code = self.root.code.follow(self.code, create=False)
556
- data = combine_dict(
557
- self.data or {},
558
- {} if code.value is NotGiven else code.value.get("default", {}),
559
- deep=True,
560
- )
561
-
562
- if code.is_async:
563
- data["_info"] = self._q = create_queue(QLEN)
564
- data["_client"] = self.root.client
565
- data["_cfg"] = self.root.client._cfg
566
- data["_cls"] = _CLASSES
567
- data["_P"] = P
568
- data["_Path"] = Path
569
- data["_log"] = self._logger
570
- data["_digits"] = digits
571
-
572
- state.started = time.time()
573
- state.node = state.root.name
574
-
575
- await state.save(wait=True)
576
- if state.node != state.root.name:
577
- raise RuntimeError("Rudely taken away from us.", state.node, state.root.name)
578
-
579
- data["_self"] = calls = CallAdmin(self, state, data)
580
- res = await calls._run(code, data)
581
-
582
- except BaseException as exc:
583
- self._logger.info("Error: %r", exc)
584
- raise
585
- else:
586
- self._logger.debug("End")
587
- finally:
588
- self.scope = None
589
- self._q = None
590
- t = time.time()
591
-
592
- except ErrorRecorded:
593
- # record_error() has already been called
594
- self._comment = None
595
- state.backoff = min(state.backoff + 1, 20)
596
-
597
- except BaseException as exc:
598
- c, self._comment = self._comment, None
599
- with anyio.move_on_after(5, shield=True):
600
- r = await self.root.err.record_error(
601
- "run",
602
- self._path,
603
- exc=exc,
604
- data=self.data,
605
- comment=c,
606
- )
607
- if r is not None:
608
- await self.root.err.wait_chain(r.chain)
609
- if isinstance(exc, Exception):
610
- state.backoff = min(state.backoff + 1, 20)
611
- else:
612
- raise
613
-
614
- else:
615
- state.result = res
616
- state.backoff = 0
617
- await self.root.err.record_working("run", self._path)
618
-
619
- finally:
620
- with anyio.fail_after(2, shield=True):
621
- if state.node == state.root.name:
622
- state.node = None
623
- self._running = False
624
- state.stopped = t
625
-
626
- if state.backoff > 0:
627
- self.retry = t + (self.backoff**state.backoff) * self.delay
628
- else:
629
- self.retry = None
630
-
631
- try:
632
- await state.save()
633
- except ClosedResourceError:
634
- pass
635
- except ServerError:
636
- logger.exception("Could not save")
637
-
638
- await self.root.trigger_rescan()
639
-
640
- async def send_event(self, evt):
641
- """Send an event to the running process."""
642
- if self._q is None:
643
- if self._running:
644
- self._logger.info("Discarding %r", evt)
645
- elif self._q.qsize() < QLEN - 1:
646
- self._logger.debug("Event: %r", evt)
647
- await self._q.put(evt)
648
- elif self._q.qsize() == QLEN - 1:
649
- self._logger.warning("Queue full")
650
- await self._q.put(None)
651
- self._q = None
652
- self._logger.info("Discarding %r", evt)
653
-
654
- async def seems_down(self):
655
- state = self.state
656
- state.node = None
657
- if not self._running:
658
- await state.save(wait=True)
659
-
660
- async def set_value(self, value):
661
- """Process incoming value changes"""
662
- c = self.code
663
- await super().set_value(value)
664
-
665
- # Check whether running code needs to be killed off
666
- if self.scope is not None:
667
- if value is NotGiven or c is not self.code:
668
- # The code changed.
669
- self._comment = "Cancel: Code changed"
670
- self.scope.cancel()
671
- elif not getattr(self, "target", None):
672
- self._comment = "Cancel: target zeroed"
673
- self.scope.cancel()
674
-
675
- await self.root.trigger_rescan()
676
-
677
- async def seen_value(self):
678
- await super().seen_value()
679
- await self.root.trigger_rescan()
680
-
681
- async def run_at(self, t: float):
682
- """Next run at this time."""
683
- self.target = t
684
- if not self._running:
685
- await self.save()
686
-
687
- def should_start(self):
688
- """Tell whether this job might want to be started.
689
-
690
- Returns:
691
- ``False``: No, it's running (or has run and doesn't restart).
692
- ``0``: No, it should not start
693
- ``>0``: timestamp at which it should start, or should have started
694
-
695
- """
696
-
697
- state = self.state
698
- if self.code is None:
699
- return False, "no code"
700
- if state.node is not None:
701
- # not our responsibility!
702
- return None, "node set"
703
- if state.started and not state.stopped:
704
- raise RuntimeError("Running! should not be called")
705
-
706
- if self.target is None:
707
- return False, "no target"
708
- elif self.target > state.started:
709
- return self.target, "target > started"
710
- elif state.backoff:
711
- return state.stopped + self.delay * (self.backoff**state.backoff), "backoff"
712
- elif self.repeat:
713
- return state.stopped + self.repeat, "repeat"
714
- elif state.started and state.started > state.stopped:
715
- return False, "is started"
716
- else:
717
- return 0, "no target"
718
-
719
- def __hash__(self):
720
- return hash(self.subpath)
721
-
722
- def __eq__(self, other):
723
- other = getattr(other, "subpath", other)
724
- return self.subpath == other
725
-
726
- def __lt__(self, other):
727
- other = getattr(other, "subpath", other)
728
- return self.subpath < other
729
-
730
- @property
731
- def age(self):
732
- return time.time() - self.state.started
733
-
734
-
735
- class RunnerNode:
736
- """
737
- Represents all nodes in this runner group.
738
-
739
- This is used for load balancing and such. TODO.
740
- """
741
-
742
- seen = 0
743
- load = 999
744
-
745
- def __new__(cls, root, name):
746
- try:
747
- self = root._nodes[name]
748
- except KeyError:
749
- self = object.__new__(cls)
750
- self.root = root
751
- self.name = name
752
- root._nodes[name] = self
753
- return self
754
-
755
- def __init__(self, *a, **k):
756
- pass
757
-
758
-
759
- class StateEntry(AttrClientEntry):
760
- """
761
- This is the actual state associated with a RunnerEntry.
762
- It must only be managed by the node that actually runs the code.
763
-
764
- Arguments:
765
- started (float): timestamp when the job was last started
766
- stopped (float): timestamp when the job last terminated
767
- pinged (float): timestamp when the state was last verified by the runner
768
- result (Any): the code's return value
769
- node (str): the node running this code
770
- backoff (float): on error, the multiplier to apply to the restart timeout
771
- computed (float): computed start time
772
- reason (str): reason why (not) starting
773
- """
774
-
775
- ATTRS = "started stopped pinged computed reason result node backoff".split()
776
-
777
- started = 0 # timestamp
778
- stopped = 0 # timestamp
779
- pinged = 0 # timestamp
780
- node = None # on which the code is currently running
781
- result = NotGiven
782
- backoff = 0
783
- computed = 0 # timestamp
784
- reason = ""
785
-
786
- @property
787
- def runner(self):
788
- return self.root.runner.follow(self.subpath, create=False)
789
-
790
- async def startup(self):
791
- try:
792
- self.runner
793
- except KeyError:
794
- # The code entry doesn't exist any more.
795
- await self.delete()
796
- return
797
-
798
- if self.node is None:
799
- return
800
- if self.node != self.root.name:
801
- self.root.runner.get_node(self.node)
802
- return
803
-
804
- self.stopped = time.time()
805
- self.node = None
806
- self.backoff = min(20, self.backoff + 1)
807
- await self.root.runner.err.record_error(
808
- "run",
809
- self.runner._path,
810
- message="Runner restarted",
811
- )
812
- await self.save()
813
-
814
- async def ping(self):
815
- if self.node != self.root.name:
816
- raise RuntimeError("Not our state!")
817
- self.pinged = time.time()
818
- await self.save()
819
-
820
- async def stale(self):
821
- self.stopped = time.time()
822
- node, self.node = self.node, None
823
- self.backoff = min(20, self.backoff + 2)
824
- await self.root.runner.err.record_error(
825
- "run",
826
- self.runner._path,
827
- message="Runner killed: {node} {state}",
828
- data={"node": node, "state": "offline" if self.stopped else "stale"},
829
- )
830
- await self.save()
831
-
832
- async def set_value(self, value):
833
- n = self.node
834
-
835
- await super().set_value(value)
836
- if not self.root.runner_ready:
837
- return
838
- try:
839
- run = self.runner
840
- except KeyError:
841
- return
842
-
843
- # side effect: add to the global node list
844
- if n is not None:
845
- run.root.get_node(n)
846
-
847
- # Check whether running code needs to be killed off
848
- if run.scope is None:
849
- return
850
- if self.node is not None and self.node == n:
851
- # Nothing changed.
852
- return
853
- elif self.node is None or n == self.root.runner.name:
854
- # Owch. Our job got taken away from us.
855
- run._comment = f"Cancel: Node set to {self.node!r}"
856
- run.scope.cancel()
857
- elif n is not None:
858
- logger.warning(
859
- "Runner %s at %r: running but node is %s",
860
- self.root.name,
861
- self.subpath,
862
- n,
863
- )
864
-
865
- await run.root.trigger_rescan()
866
-
867
-
868
- class StateRoot(MirrorRoot):
869
- """Base class for handling the state of entries.
870
-
871
- This is separate from the RunnerRoot hierarchy because the latter may
872
- be changed by anybody while this subtree may only be affected by the
873
- actual runner. Otherwise we get interesting race conditions.
874
- """
875
-
876
- _runner = None
877
-
878
- @classmethod
879
- def child_type(cls, name):
880
- return StateEntry
881
-
882
- @property
883
- def name(self):
884
- return self.client.client_name
885
-
886
- @property
887
- def runner(self):
888
- return self._runner()
889
-
890
- @property
891
- def runner_ready(self):
892
- r = self._runner
893
- if r is None:
894
- return False
895
- r = r()
896
- if r is None:
897
- return False
898
- return r.ready
899
-
900
- def set_runner(self, runner):
901
- self._runner = ref(runner)
902
-
903
- async def runner_running(self):
904
- for n in self.all_children:
905
- await n.startup()
906
-
907
- async def kill_stale_nodes(self, names):
908
- """States with node names in the "names" set are stale. Kill them."""
909
- for s in self.all_children:
910
- if s.node in names:
911
- await s.stale()
912
-
913
- _last_t = 0
914
-
915
- async def ping(self):
916
- t = time.time()
917
- if t - self._last_t >= abs(self._cfg["ping"]):
918
- self._last_t = t
919
- if self._cfg["ping"] > 0:
920
- val = self.value_or({}, Mapping)
921
- val["alive"] = t
922
- await self.update(val)
923
- else:
924
- await self.client.msg_send(
925
- "run",
926
- {"group": self.runner.group, "time": t, "node": self.name},
927
- )
928
-
929
-
930
- class _BaseRunnerRoot(ClientRoot):
931
- """common code for AnyRunnerRoot and SingleRunnerRoot"""
932
-
933
- _trigger: anyio.abc.Event = None
934
- _run_now_task: anyio.abc.CancelScope = None
935
- _tagged: bool = True
936
-
937
- err = None
938
- code = None
939
- ready = False
940
- _run_next = 0
941
- node_history = None
942
- _start_delay = None
943
- state = None
944
- _act = None
945
-
946
- CFG = "runner"
947
- SUB = None
948
-
949
- def __init__(self, *a, _subpath, err=None, code=None, nodes=0, **kw):
950
- super().__init__(*a, **kw)
951
- self.err = err
952
- self.code = code
953
- self._nodes = {}
954
- self.n_nodes = nodes
955
- self._trigger = anyio.Event()
956
- self._x_subpath = _subpath
957
-
958
- @classmethod
959
- def child_type(cls, name):
960
- return RunnerEntry
961
-
962
- @classmethod
963
- async def as_handler(cls, client, subpath, cfg=None, **kw): # pylint: disable=arguments-differ
964
- assert cls.SUB is not None
965
- if cfg is None:
966
- cfg_ = client._cfg["runner"]
967
- else:
968
- cfg_ = combine_dict(cfg, client._cfg["runner"])
969
- return await super().as_handler(client, subpath=subpath, _subpath=subpath, cfg=cfg_, **kw)
970
-
971
- async def run_starting(self):
972
- from .code import CodeRoot
973
- from .errors import ErrorRoot
974
-
975
- if self.err is None:
976
- self.err = await ErrorRoot.as_handler(self.client)
977
- if self.code is None:
978
- self.code = await CodeRoot.as_handler(self.client)
979
-
980
- await self._state_runner()
981
- self.state.set_runner(self)
982
-
983
- self.node_history = NodeList(0)
984
- self._start_delay = self._cfg["start_delay"]
985
-
986
- await super().run_starting()
987
-
988
- async def _state_runner(self):
989
- self.state = await StateRoot.as_handler(
990
- self.client,
991
- cfg=self._cfg,
992
- subpath=self._x_subpath,
993
- key="state",
994
- )
995
-
996
- @property
997
- def name(self):
998
- """my node name"""
999
- return self.client.client_name
1000
-
1001
- def get_node(self, name): # pylint: disable=unused-argument
1002
- """
1003
- If the runner keeps track of "foreign" nodes, allocate them
1004
- """
1005
- return None
1006
-
1007
- async def running(self):
1008
- self._tg.start_soon(self._run_actor)
1009
-
1010
- # the next block needs to be atomic
1011
- self.ready = True
1012
-
1013
- await self.state.runner_running()
1014
- await super().running()
1015
-
1016
- async def _run_actor(self):
1017
- """
1018
- This method is started as a long-lived task of the root as soon as
1019
- the subtree's data are loaded.
1020
-
1021
- Its job is to control which tasks are started.
1022
- """
1023
- raise RuntimeError("You want to override me.")
1024
-
1025
- async def trigger_rescan(self):
1026
- """Tell the _run_actor task to rescan our job list prematurely"""
1027
- if self._trigger is not None:
1028
- self._trigger.set()
1029
-
1030
- async def _run_now(self, evt=None):
1031
- t_next = self._run_next
1032
- with anyio.CancelScope() as sc:
1033
- self._run_now_task = sc
1034
- if evt is not None:
1035
- evt.set()
1036
- t = time.time()
1037
- while True:
1038
- if t_next > t:
1039
- with anyio.move_on_after(t_next - t):
1040
- await self._trigger.wait()
1041
- self._trigger = anyio.Event()
1042
-
1043
- t = time.time()
1044
- t_next = t + 999
1045
- for j in self.all_children:
1046
- d, r = j.should_start()
1047
- if d is None:
1048
- continue
1049
- if not d or d > t:
1050
- j.state.computed = d
1051
- j.state.reason = r
1052
- if self._tagged:
1053
- try:
1054
- await j.state.save()
1055
- except anyio.ClosedResourceError: # owch
1056
- logger.error("Could not update state %r %r", j, j.state)
1057
- return
1058
- if d and t_next > d:
1059
- t_next = d
1060
- continue
1061
- self._tg.start_soon(j.run)
1062
- await anyio.sleep(self._start_delay)
1063
-
1064
- async def notify_actor_state(self, msg=None):
1065
- """
1066
- Notify all running jobs about a possible change in active status.
1067
- """
1068
- ac = len(self.node_history)
1069
- if ac == 0 or msg is None:
1070
- ac = BrokenState
1071
- elif self.name in self.node_history and ac == 1:
1072
- ac = DetachedState
1073
- elif self._act is not None and ac >= self.n_nodes: # TODO configureable
1074
- ac = CompleteState
1075
- else:
1076
- ac = PartialState
1077
-
1078
- logger.debug("State %r %r", ac, msg)
1079
-
1080
- ac = ac(msg)
1081
- for n in self.all_children:
1082
- await n.send_event(ac)
1083
-
1084
-
1085
- class AnyRunnerRoot(_BaseRunnerRoot):
1086
- """
1087
- This class represents the root of a code runner. Its job is to start
1088
- (and periodically restart, if required) the entry points stored under it.
1089
-
1090
- :class:`AnyRunnerRoot` tries to ensure that the code in question runs
1091
- on one single cluster member. In case of a network split, the code will
1092
- run once in each split areas until the split is healed.
1093
- """
1094
-
1095
- SUB = "group"
1096
-
1097
- _stale_times = None
1098
- tg = None
1099
- seen_load = None
1100
- _tagged: bool = False
1101
-
1102
- def __init__(self, *a, **kw):
1103
- super().__init__(*a, **kw)
1104
- self.group = (
1105
- P(self.client.config.server["root"]) + P(self._cfg["name"]) | "any" | self._path[-1]
1106
- )
1107
-
1108
- def get_node(self, name):
1109
- return RunnerNode(self, name)
1110
-
1111
- @property
1112
- def max_age(self):
1113
- """Timeout after which we really should have gotten another go"""
1114
- # allow one go-around without being in the list; happens when the
1115
- # bus is slow
1116
- return self._act.cycle_time_max * (self._act.history_size + 2.5)
1117
-
1118
- async def _run_actor(self):
1119
- """
1120
- Monitor the Actor state, run a :meth:`_run_now` subtask whenever we're 'it'.
1121
- """
1122
- async with ClientActor(
1123
- self.client,
1124
- self.name,
1125
- topic=self.group,
1126
- cfg=self._cfg["actor"],
1127
- ) as act:
1128
- self._act = act
1129
-
1130
- age_q = create_queue(10)
1131
- await self.spawn(self._age_killer, age_q)
1132
-
1133
- psutil.cpu_percent(interval=None)
1134
- await act.set_value(0)
1135
- self.seen_load = None
1136
-
1137
- async for msg in act:
1138
- logger.debug("Actor %r", msg)
1139
- if isinstance(msg, PingEvent):
1140
- await act.set_value(100 - psutil.cpu_percent(interval=None))
1141
-
1142
- node = self.get_node(msg.node)
1143
- node.load = msg.value
1144
- node.seen = time.time()
1145
- if self.seen_load is not None:
1146
- self.seen_load += msg.value
1147
- self.node_history += node
1148
-
1149
- elif isinstance(msg, TagEvent):
1150
- self._tagged = True
1151
- load = 100 - psutil.cpu_percent(interval=None)
1152
- await act.set_value(load)
1153
- if self.seen_load is not None:
1154
- pass # TODO
1155
-
1156
- self.get_node(self.name).seen = time.time()
1157
- self.node_history += self.name
1158
- evt = anyio.Event()
1159
- await self.spawn(self._run_now, evt)
1160
- await age_q.put(None)
1161
- await evt.wait()
1162
-
1163
- await self.state.ping()
1164
-
1165
- elif isinstance(msg, UntagEvent):
1166
- self._tagged = False
1167
- await act.set_value(100 - psutil.cpu_percent(interval=None))
1168
- self.seen_load = 0
1169
-
1170
- self._run_now_task.cancel()
1171
- # TODO if this is a DetagEvent, kill everything?
1172
-
1173
- await self.notify_actor_state(msg)
1174
-
1175
- pass # end of actor task
1176
-
1177
- async def find_stale_nodes(self, cur):
1178
- """
1179
- Find stale nodes (i.e. last seen < cur) and clean them.
1180
- """
1181
- if self._stale_times is None:
1182
- self._stale_times = []
1183
- self._stale_times.append(cur)
1184
- if self._stale_times[0] > cur - self.max_age:
1185
- return
1186
- if len(self._stale_times) <= 2 * self._cfg["actor"]["n_hosts"] + 1:
1187
- return
1188
- cut = self._stale_times.pop(0)
1189
-
1190
- dropped = []
1191
- for node in self._nodes.values():
1192
- if node.seen < cut:
1193
- dropped.append(node)
1194
- names = set()
1195
- for node in dropped:
1196
- names.add(node.name)
1197
- del self._nodes[node.name]
1198
- if names:
1199
- await self.state.kill_stale_nodes(names)
1200
-
1201
- async def _age_killer(self, age_q):
1202
- """
1203
- Subtask which cleans up stale tasks
1204
-
1205
- TODO check where to use time.monotonic
1206
- """
1207
- t0 = None
1208
- t1 = time.time()
1209
- while True:
1210
- with anyio.move_on_after(self.max_age):
1211
- await age_q.get()
1212
- t1 = time.time()
1213
- t2 = time.time()
1214
- if t1 + self.max_age < t2:
1215
- raise NotSelected(self.max_age, t2, t1)
1216
- t0 = t1
1217
- t1 = t2
1218
- if t0 is not None:
1219
- await self.find_stale_nodes(t0)
1220
-
1221
-
1222
- class SingleRunnerRoot(_BaseRunnerRoot):
1223
- """
1224
- This class represents the root of a code runner. Its job is to start
1225
- (and periodically restart, if required) the entry points stored under it.
1226
-
1227
- While :class:`AnyRunnerRoot` tries to ensure that the code in question runs
1228
- on any cluster member, this class runs tasks on a single node.
1229
- The code is able to check whether any and/or all of the cluster's main
1230
- nodes are reachable; this way, the code can default to local operation
1231
- if connectivity is lost.
1232
-
1233
- Local data (dict):
1234
-
1235
- Arguments:
1236
- cores (tuple): list of nodes whose reachability may determine
1237
- whether the code uses local/emergency/??? mode.
1238
-
1239
- Config file:
1240
-
1241
- Arguments:
1242
- path (tuple): the location this entry is stored at. Defaults to
1243
- ``('.moat', 'kv', 'process')``.
1244
- name (str): this runner's name. Defaults to the client's name plus
1245
- the name stored in the root node, if any.
1246
- actor (dict): the configuration for the underlying actor. See
1247
- ``asyncactor`` for details.
1248
- """
1249
-
1250
- SUB = "single"
1251
-
1252
- err = None
1253
- code = None
1254
- state = None
1255
- tg = None
1256
-
1257
- def __init__(self, *a, **kw):
1258
- super().__init__(*a, **kw)
1259
- self.group = (
1260
- P(self.client.config.server["root"]) + P(self._cfg["name"])
1261
- | "single"
1262
- | self._path[-2]
1263
- | self._path[-1]
1264
- )
1265
-
1266
- async def set_value(self, value):
1267
- await super().set_value(value)
1268
- try:
1269
- cores = value["cores"]
1270
- except (TypeError, KeyError):
1271
- if self._act is not None:
1272
- await self._act.disable(0)
1273
- else:
1274
- if self.name in cores:
1275
- await self._act.enable(len(cores))
1276
- else:
1277
- await self._act.disable(len(cores))
1278
-
1279
- @property
1280
- def max_age(self):
1281
- """Timeout after which we really should have gotten another ping"""
1282
- return self._act.cycle_time_max * 1.5
1283
-
1284
- async def _run_actor(self):
1285
- async with anyio.create_task_group() as tg:
1286
- age_q = create_queue(1)
1287
-
1288
- async with ClientActor(self.client, self.name, topic=self.group, cfg=self._cfg) as act:
1289
- self._act = act
1290
- tg.start_soon(self._age_notifier, age_q)
1291
- await self.spawn(self._run_now)
1292
- await act.set_value(0)
1293
-
1294
- async for msg in act:
1295
- if isinstance(msg, AuthPingEvent):
1296
- # Some node, not necessarily us, is "it".
1297
- # We're the SingleNode runner: we manage our jobs
1298
- # when triggered by this, no matter whether we're
1299
- # "it" or not.
1300
- self.node_history += msg.node
1301
- await age_q.put(None)
1302
- await self.notify_actor_state(msg)
1303
-
1304
- await self.state.ping()
1305
-
1306
- pass # end of actor task
1307
-
1308
- async def _age_notifier(self, age_q):
1309
- """
1310
- This background job triggers :meth:`notify_actor_state` when too much
1311
- time has passed without an AuthPing.
1312
- """
1313
- while True:
1314
- try:
1315
- with anyio.fail_after(self.max_age):
1316
- await age_q.get()
1317
- except TimeoutError:
1318
- await self.notify_actor_state()
1319
-
1320
-
1321
- class AllRunnerRoot(SingleRunnerRoot):
1322
- """
1323
- This class represents the root of a code runner. Its job is to start
1324
- (and periodically restart, if required) the entry points stored under it.
1325
-
1326
- This class behaves like `SingleRunner`, except that it runs tasks on all nodes.
1327
- """
1328
-
1329
- SUB = "all"
1330
-
1331
- err = None
1332
- _act = None
1333
- code = None
1334
-
1335
- def __init__(self, *a, **kw):
1336
- super().__init__(*a, **kw)
1337
- self.group = (
1338
- P(self.client.config.server["root"]) + P(self._cfg["name"]) | "all" | self._path[-1]
1339
- )
1340
-
1341
- async def _state_runner(self):
1342
- self.state = await StateRoot.as_handler(
1343
- self.client,
1344
- cfg=self._cfg,
1345
- subpath=self._x_subpath + (self.client.client_name,),
1346
- key="state",
1347
- )