actio 0.0.2__tar.gz
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.
- actio-0.0.2/LICENSE +21 -0
- actio-0.0.2/PKG-INFO +67 -0
- actio-0.0.2/README.md +40 -0
- actio-0.0.2/pyproject.toml +52 -0
- actio-0.0.2/src/actio/__init__.py +40 -0
- actio-0.0.2/src/actio/actor.py +522 -0
- actio-0.0.2/src/actio/messages.py +23 -0
- actio-0.0.2/src/actio/ref.py +38 -0
- actio-0.0.2/src/actio/registry.py +130 -0
actio-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Semenets V. Pavel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
actio-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: actio
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Pure Python actor system for building concurrent and distributed applications with async/await support for Python 3.11+
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Semenets V. Pavel
|
|
8
|
+
Author-email: p.semenets@gmail.com
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Communications
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: System :: Hardware
|
|
22
|
+
Project-URL: Documentation, https://github.com/xDarkmanx/actio#readme
|
|
23
|
+
Project-URL: Homepage, https://github.com/xDarkmanx/actio
|
|
24
|
+
Project-URL: Repository, https://github.com/xDarkmanx/actio
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
### Actio Project
|
|
28
|
+
|
|
29
|
+
Pure Python actor system for building concurrent and distributed applications with async/await support.
|
|
30
|
+
|
|
31
|
+
### Features
|
|
32
|
+
|
|
33
|
+
**Async/await native** - Built on Python 3.11+ asyncio
|
|
34
|
+
|
|
35
|
+
**Hierarchical actors** - Parent-child relationships with supervision
|
|
36
|
+
|
|
37
|
+
**Message routing** - Path-based message delivery between actors
|
|
38
|
+
|
|
39
|
+
**Lifecycle management** - Started/receive/stopped hooks with error handling
|
|
40
|
+
|
|
41
|
+
**Type safe** - Full type annotations with Pylance/MyPy support
|
|
42
|
+
|
|
43
|
+
### Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
from actio import Actor, ActorSystem, actio
|
|
48
|
+
|
|
49
|
+
@actio()
|
|
50
|
+
class MyActor(Actor):
|
|
51
|
+
async def receive(self, sender, message):
|
|
52
|
+
print(f"Received: {message}")
|
|
53
|
+
|
|
54
|
+
async def main():
|
|
55
|
+
system = ActorSystem()
|
|
56
|
+
actor_ref = system.create(MyActor())
|
|
57
|
+
|
|
58
|
+
system.tell(actor_ref, "Hello World!")
|
|
59
|
+
await asyncio.sleep(0.1)
|
|
60
|
+
system.shutdown()
|
|
61
|
+
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### License
|
|
66
|
+
MIT License - see LICENSE file for details.
|
|
67
|
+
|
actio-0.0.2/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
### Actio Project
|
|
2
|
+
|
|
3
|
+
Pure Python actor system for building concurrent and distributed applications with async/await support.
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
**Async/await native** - Built on Python 3.11+ asyncio
|
|
8
|
+
|
|
9
|
+
**Hierarchical actors** - Parent-child relationships with supervision
|
|
10
|
+
|
|
11
|
+
**Message routing** - Path-based message delivery between actors
|
|
12
|
+
|
|
13
|
+
**Lifecycle management** - Started/receive/stopped hooks with error handling
|
|
14
|
+
|
|
15
|
+
**Type safe** - Full type annotations with Pylance/MyPy support
|
|
16
|
+
|
|
17
|
+
### Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import asyncio
|
|
21
|
+
from actio import Actor, ActorSystem, actio
|
|
22
|
+
|
|
23
|
+
@actio()
|
|
24
|
+
class MyActor(Actor):
|
|
25
|
+
async def receive(self, sender, message):
|
|
26
|
+
print(f"Received: {message}")
|
|
27
|
+
|
|
28
|
+
async def main():
|
|
29
|
+
system = ActorSystem()
|
|
30
|
+
actor_ref = system.create(MyActor())
|
|
31
|
+
|
|
32
|
+
system.tell(actor_ref, "Hello World!")
|
|
33
|
+
await asyncio.sleep(0.1)
|
|
34
|
+
system.shutdown()
|
|
35
|
+
|
|
36
|
+
asyncio.run(main())
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### License
|
|
40
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "actio"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "Pure Python actor system for building concurrent and distributed applications with async/await support for Python 3.11+"
|
|
5
|
+
authors = ["Semenets V. Pavel <p.semenets@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
packages = [{ include = "actio", from = "src" }]
|
|
10
|
+
homepage = "https://github.com/xDarkmanx/actio"
|
|
11
|
+
repository = "https://github.com/xDarkmanx/actio"
|
|
12
|
+
documentation = "https://github.com/xDarkmanx/actio#readme"
|
|
13
|
+
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Topic :: System :: Hardware",
|
|
25
|
+
"Topic :: Communications",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.poetry.dependencies]
|
|
29
|
+
python = ">=3.11"
|
|
30
|
+
|
|
31
|
+
[tool.poetry.group.lint.dependencies]
|
|
32
|
+
flake8 = "^7.3.0"
|
|
33
|
+
|
|
34
|
+
[tool.poetry.group.security.dependencies]
|
|
35
|
+
bandit = "^1.8.6"
|
|
36
|
+
|
|
37
|
+
[tool.poetry.group.test.dependencies]
|
|
38
|
+
pytest = ">=8.4.2"
|
|
39
|
+
pytest-asyncio = ">=1.2.0"
|
|
40
|
+
pytest-cov = ">=4.1.0"
|
|
41
|
+
pytest-mock = ">=3.11.0"
|
|
42
|
+
anyio = ">=3.7.0"
|
|
43
|
+
|
|
44
|
+
[tool.flake8]
|
|
45
|
+
max-line-length = 127
|
|
46
|
+
max-complexity = 25
|
|
47
|
+
ignore = "E203,W503"
|
|
48
|
+
exclude = [".git", "__pycache__", ".venv", "build", "dist"]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
52
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# actio/__init__.py
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from .ref import ActorRef
|
|
5
|
+
from .ref import ActorDefinition
|
|
6
|
+
|
|
7
|
+
from .messages import PoisonPill
|
|
8
|
+
from .messages import DeadLetter
|
|
9
|
+
from .messages import Terminated
|
|
10
|
+
|
|
11
|
+
from .actor import Actor
|
|
12
|
+
from .actor import ActorSystem
|
|
13
|
+
|
|
14
|
+
from .registry import ActorRegistry
|
|
15
|
+
from .registry import registry
|
|
16
|
+
from .registry import actio
|
|
17
|
+
|
|
18
|
+
__version__ = "0.0.2"
|
|
19
|
+
__author__ = "Semenets V. Pavel"
|
|
20
|
+
__license__ = "MIT"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# from ref.py
|
|
24
|
+
'ActorRef',
|
|
25
|
+
'ActorDefinition',
|
|
26
|
+
|
|
27
|
+
# from messages.py
|
|
28
|
+
'PoisonPill',
|
|
29
|
+
'DeadLetter',
|
|
30
|
+
'Terminated',
|
|
31
|
+
|
|
32
|
+
# from actor.py
|
|
33
|
+
'Actor',
|
|
34
|
+
'ActorSystem',
|
|
35
|
+
|
|
36
|
+
# from registry.py
|
|
37
|
+
'ActorRegistry',
|
|
38
|
+
'registry',
|
|
39
|
+
'actio'
|
|
40
|
+
]
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# actio/actor.py
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from typing import Union
|
|
5
|
+
from typing import Set
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from typing import List
|
|
8
|
+
from typing import Any
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
from . import PoisonPill
|
|
17
|
+
from . import DeadLetter
|
|
18
|
+
from . import Terminated
|
|
19
|
+
|
|
20
|
+
from . import ActorRef
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger('actio.actor')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Actor:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._context: Optional['_ActorContext'] = None
|
|
28
|
+
|
|
29
|
+
async def receive(self, sender: ActorRef, message: Any) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Обрабатывает входящие сообщения.
|
|
32
|
+
|
|
33
|
+
Этот метод должен быть переопределён в подклассах.
|
|
34
|
+
"""
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
# --- Вспомогательные методы для работы с детьми и системой ---
|
|
38
|
+
|
|
39
|
+
def create(self, actor: 'Actor', name: Optional[str] = None) -> ActorRef:
|
|
40
|
+
"""Создаёт дочерний актор."""
|
|
41
|
+
child_actor_ref = self.system._create(actor=actor, parent=self.actor_ref, name=name)
|
|
42
|
+
self.watch(child_actor_ref)
|
|
43
|
+
return child_actor_ref
|
|
44
|
+
|
|
45
|
+
def tell(self, actor: Union['Actor', ActorRef], message: Any) -> None:
|
|
46
|
+
"""Отправляет сообщение другому актору."""
|
|
47
|
+
self.system._tell(actor=actor, message=message, sender=self.actor_ref)
|
|
48
|
+
|
|
49
|
+
def schedule_tell(
|
|
50
|
+
self,
|
|
51
|
+
actor: Union['Actor', ActorRef],
|
|
52
|
+
message: Any,
|
|
53
|
+
*,
|
|
54
|
+
delay: Union[None, int, float] = None,
|
|
55
|
+
period: Union[None, int, float] = None,
|
|
56
|
+
) -> asyncio.Task:
|
|
57
|
+
"""Отправляет сообщение с задержкой или периодически."""
|
|
58
|
+
return self.system._schedule_tell(
|
|
59
|
+
actor=actor,
|
|
60
|
+
message=message,
|
|
61
|
+
sender=self.actor_ref,
|
|
62
|
+
delay=delay,
|
|
63
|
+
period=period
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def watch(self, actor: ActorRef) -> None:
|
|
67
|
+
"""Наблюдает за другим актором."""
|
|
68
|
+
self.system._watch(actor=self.actor_ref, other=actor)
|
|
69
|
+
|
|
70
|
+
def unwatch(self, actor: ActorRef) -> None:
|
|
71
|
+
"""Прекращает наблюдение за другим актором."""
|
|
72
|
+
self.system._unwatch(actor=self.actor_ref, other=actor)
|
|
73
|
+
|
|
74
|
+
def stop(self) -> None:
|
|
75
|
+
"""Останавливает этот актор."""
|
|
76
|
+
self.system.stop(actor=self.actor_ref)
|
|
77
|
+
|
|
78
|
+
# --- Хуки жизненного цикла ---
|
|
79
|
+
async def started(self) -> None:
|
|
80
|
+
"""Вызывается при запуске актора."""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
async def restarted(self, sender: ActorRef, message: Any, error: Exception) -> None:
|
|
84
|
+
"""Вызывается при ошибке в `receive`."""
|
|
85
|
+
log.exception(
|
|
86
|
+
'%s failed to receive message %s from %s',
|
|
87
|
+
self.actor_ref, message, sender, exc_info=error
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def stopped(self) -> None:
|
|
91
|
+
"""Вызывается при остановке актора."""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
# --- Свойства для доступа к контексту ---
|
|
95
|
+
@property
|
|
96
|
+
def actor_ref(self) -> ActorRef:
|
|
97
|
+
assert self._context is not None, "Actor context not initialized"
|
|
98
|
+
return self._context.actor_ref
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def system(self) -> 'ActorSystem':
|
|
102
|
+
assert self._context is not None, "Actor context not initialized"
|
|
103
|
+
return self._context.system
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def parent(self) -> Optional[ActorRef]:
|
|
107
|
+
assert self._context is not None, "Actor context not initialized"
|
|
108
|
+
return self._context.parent
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def name(self) -> str:
|
|
112
|
+
return self.actor_ref.name
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def path(self) -> str:
|
|
116
|
+
return self.actor_ref.path
|
|
117
|
+
|
|
118
|
+
def __str__(self):
|
|
119
|
+
return self.path
|
|
120
|
+
|
|
121
|
+
def __repr__(self):
|
|
122
|
+
return self.path
|
|
123
|
+
|
|
124
|
+
# --- Вспомогательные методы для маршрутизации ---
|
|
125
|
+
|
|
126
|
+
async def _route_message_logic(self, sender: ActorRef, message: Dict[str, Any]) -> bool:
|
|
127
|
+
"""
|
|
128
|
+
Встроенная логика маршрутизации для сообщений с действием 'route_message'.
|
|
129
|
+
|
|
130
|
+
:param sender: Отправитель сообщения.
|
|
131
|
+
:param message: Сообщение.
|
|
132
|
+
:return: True, если сообщение было обработано как маршрутное, иначе False.
|
|
133
|
+
"""
|
|
134
|
+
action = message.get('action')
|
|
135
|
+
if action != 'route_message':
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
destination: str = message.get('destination', '')
|
|
139
|
+
data = message.get('data')
|
|
140
|
+
|
|
141
|
+
# 1. Если destination пустой, это сообщение для текущего актора
|
|
142
|
+
if not destination:
|
|
143
|
+
# Передаём данные для обработки
|
|
144
|
+
# Создаём новое сообщение, чтобы не мутировать оригинальное
|
|
145
|
+
final_message = data if isinstance(data, dict) else {'data': data}
|
|
146
|
+
# Добавляем информацию о маршруте в финальное сообщение, если нужно
|
|
147
|
+
final_message['source'] = message.get('source')
|
|
148
|
+
await self.receive(sender, final_message)
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
# 2. Разбираем путь
|
|
152
|
+
path_parts = [p for p in destination.split('/') if p]
|
|
153
|
+
if not path_parts:
|
|
154
|
+
# destination был "/", что тоже означает "этот актор"
|
|
155
|
+
final_message = data if isinstance(data, dict) else {'data': data}
|
|
156
|
+
await self.receive(sender, final_message)
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
first_part = path_parts[0]
|
|
160
|
+
remaining_path = "/".join(path_parts[1:]) if len(path_parts) > 1 else None
|
|
161
|
+
|
|
162
|
+
# 3. Проверяем, является ли первая часть именем нашего прямого потомка
|
|
163
|
+
# Предполагаем, что у актора есть словарь потомков self.actors (как в примерах)
|
|
164
|
+
# Если такого атрибута нет, ищем в _context.children
|
|
165
|
+
target_child_ref: Optional[ActorRef] = None
|
|
166
|
+
child_actors_dict = getattr(self, 'actors', None)
|
|
167
|
+
if child_actors_dict and isinstance(child_actors_dict, dict):
|
|
168
|
+
target_child_ref = child_actors_dict.get(first_part)
|
|
169
|
+
else:
|
|
170
|
+
# Поиск по имени в списке детей из контекста
|
|
171
|
+
assert self._context is not None, "Actor context not initialized"
|
|
172
|
+
for child_ref in self._context.children:
|
|
173
|
+
if child_ref.name == first_part:
|
|
174
|
+
target_child_ref = child_ref
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
if target_child_ref:
|
|
178
|
+
# Нашли потомка, пересылаем ему.
|
|
179
|
+
# Формируем новое сообщение с обновлённым destination
|
|
180
|
+
forwarded_message = message.copy()
|
|
181
|
+
forwarded_message['destination'] = remaining_path
|
|
182
|
+
# Обновляем source: добавляем себя
|
|
183
|
+
current_source = forwarded_message.get('source', '')
|
|
184
|
+
# forwarded_message['source'] = f"{self.actor_ref.name}/{current_source}".strip('/')
|
|
185
|
+
|
|
186
|
+
log.debug(
|
|
187
|
+
f"[{self.actor_ref.path}] Routing to child '{first_part}' "
|
|
188
|
+
f"with new destination '{remaining_path}' and updated source "
|
|
189
|
+
f"'{forwarded_message['source']}'.")
|
|
190
|
+
self.tell(target_child_ref, forwarded_message)
|
|
191
|
+
else:
|
|
192
|
+
# Не нашли потомка, пересылаем родителю.
|
|
193
|
+
if self.parent:
|
|
194
|
+
# Формируем новое сообщение с обновлённым source
|
|
195
|
+
forwarded_message = message.copy()
|
|
196
|
+
# Обновляем source: добавляем себя
|
|
197
|
+
current_source = forwarded_message.get('source', '')
|
|
198
|
+
forwarded_message['source'] = f"{self.actor_ref.name}/{current_source}".strip('/')
|
|
199
|
+
|
|
200
|
+
log.debug(
|
|
201
|
+
f"[{self.actor_ref.path}] Forwarding to parent with "
|
|
202
|
+
f"updated source '{forwarded_message['source']}'.")
|
|
203
|
+
self.tell(self.parent, forwarded_message)
|
|
204
|
+
else:
|
|
205
|
+
# Мы корневой актор и не можем маршрутизировать дальше
|
|
206
|
+
log.warning(
|
|
207
|
+
f"[{self.actor_ref.path}] Cannot route message, "
|
|
208
|
+
f"no parent and '{first_part}' not found among children. Message: {message}"
|
|
209
|
+
)
|
|
210
|
+
# Можно отправить DeadLetter отправителю или в специальный актор
|
|
211
|
+
# self.tell(sender, DeadLetter(...))
|
|
212
|
+
|
|
213
|
+
return True # Сообщение было обработано как маршрутное
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ActorSystem:
|
|
217
|
+
"""Система акторов."""
|
|
218
|
+
|
|
219
|
+
def __init__(self):
|
|
220
|
+
self._actors: Dict[ActorRef, '_ActorContext'] = {}
|
|
221
|
+
self._is_stopped = asyncio.Event()
|
|
222
|
+
self.children: List[ActorRef] = [] # Корневые акторы
|
|
223
|
+
|
|
224
|
+
def create(self, actor: Actor, name: Optional[str] = None) -> ActorRef:
|
|
225
|
+
"""Создаёт корневой актор."""
|
|
226
|
+
return self._create(actor=actor, parent=None, name=name)
|
|
227
|
+
|
|
228
|
+
def tell(self, actor: Union[Actor, ActorRef], message: Any) -> None:
|
|
229
|
+
"""Отправляет сообщение актору."""
|
|
230
|
+
self._tell(actor=actor, message=message, sender=None)
|
|
231
|
+
|
|
232
|
+
def schedule_tell(
|
|
233
|
+
self,
|
|
234
|
+
actor: Union[Actor, ActorRef],
|
|
235
|
+
message: Any,
|
|
236
|
+
*,
|
|
237
|
+
delay: Union[None, int, float] = None,
|
|
238
|
+
period: Union[None, int, float] = None
|
|
239
|
+
) -> asyncio.Task:
|
|
240
|
+
"""Отправляет сообщение с задержкой или периодически."""
|
|
241
|
+
return self._schedule_tell(actor=actor, message=message, sender=None, delay=delay, period=period)
|
|
242
|
+
|
|
243
|
+
def stop(self, actor: Union[Actor, ActorRef]) -> None:
|
|
244
|
+
"""Останавливает актор."""
|
|
245
|
+
self._tell(actor=actor, message=PoisonPill(), sender=None)
|
|
246
|
+
|
|
247
|
+
def shutdown(self, timeout: Union[None, int, float] = None) -> None:
|
|
248
|
+
"""Завершает работу всей системы акторов."""
|
|
249
|
+
asyncio.create_task(self._shutdown(timeout=timeout))
|
|
250
|
+
|
|
251
|
+
def stopped(self):
|
|
252
|
+
"""Ожидает завершения работы системы."""
|
|
253
|
+
return self._is_stopped.wait()
|
|
254
|
+
|
|
255
|
+
# --- Внутренние методы ---
|
|
256
|
+
def _create(self, actor: Actor, *, parent: Union[None, Actor, ActorRef], name: Optional[str] = None) -> ActorRef:
|
|
257
|
+
"""Внутренний метод создания актора."""
|
|
258
|
+
if not isinstance(actor, Actor):
|
|
259
|
+
raise ValueError(f'Not an actor: {actor}')
|
|
260
|
+
|
|
261
|
+
parent_ctx: Optional[_ActorContext] = None
|
|
262
|
+
if parent:
|
|
263
|
+
parent = self._validate_actor_ref(parent)
|
|
264
|
+
parent_ctx = self._actors[parent]
|
|
265
|
+
child_idx = len(parent_ctx.children) + 1
|
|
266
|
+
else:
|
|
267
|
+
child_idx = len(self.children) + 1
|
|
268
|
+
|
|
269
|
+
if not name:
|
|
270
|
+
name = f'{type(actor).__name__}-{child_idx}'
|
|
271
|
+
|
|
272
|
+
if parent:
|
|
273
|
+
path = f'{parent.path}/{name}'
|
|
274
|
+
else:
|
|
275
|
+
path = name
|
|
276
|
+
|
|
277
|
+
actor_id = str(uuid4().hex)
|
|
278
|
+
actor_ref = ActorRef(actor_id=actor_id, path=path, name=name)
|
|
279
|
+
actor_ctx = _ActorContext(self, actor_ref, parent)
|
|
280
|
+
|
|
281
|
+
actor_ctx.lifecycle = asyncio.get_event_loop().create_task(
|
|
282
|
+
self._actor_lifecycle_loop(actor, actor_ref, actor_ctx)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
actor._context = actor_ctx
|
|
286
|
+
self._actors[actor_ref] = actor_ctx
|
|
287
|
+
|
|
288
|
+
if parent and parent_ctx:
|
|
289
|
+
parent_ctx.children.append(actor_ref)
|
|
290
|
+
|
|
291
|
+
else:
|
|
292
|
+
self.children.append(actor_ref)
|
|
293
|
+
|
|
294
|
+
return actor_ref
|
|
295
|
+
|
|
296
|
+
def _tell(self, actor: Union[Actor, ActorRef], message: Any, *, sender: Union[None, Actor, ActorRef]) -> None:
|
|
297
|
+
"""Внутренний метод отправки сообщения."""
|
|
298
|
+
actor = self._validate_actor_ref(actor)
|
|
299
|
+
|
|
300
|
+
if sender:
|
|
301
|
+
sender = self._validate_actor_ref(sender)
|
|
302
|
+
|
|
303
|
+
if sender not in self._actors:
|
|
304
|
+
raise ValueError(f'Sender does not exist: {sender}')
|
|
305
|
+
|
|
306
|
+
if actor in self._actors:
|
|
307
|
+
actor_ctx = self._actors[actor]
|
|
308
|
+
actor_ctx.letterbox.put_nowait((sender, message))
|
|
309
|
+
|
|
310
|
+
elif sender:
|
|
311
|
+
deadletter = DeadLetter(actor=actor, message=message)
|
|
312
|
+
self._tell(sender, deadletter, sender=None)
|
|
313
|
+
|
|
314
|
+
else:
|
|
315
|
+
log.warning(f'Failed to deliver message {message} to {actor}')
|
|
316
|
+
|
|
317
|
+
def _schedule_tell(
|
|
318
|
+
self,
|
|
319
|
+
actor: Union[Actor, ActorRef],
|
|
320
|
+
message: Any,
|
|
321
|
+
*,
|
|
322
|
+
sender: Union[None, Actor, ActorRef],
|
|
323
|
+
delay: Union[None, int, float] = None,
|
|
324
|
+
period: Union[None, int, float] = None
|
|
325
|
+
) -> asyncio.Task:
|
|
326
|
+
"""Внутренний метод планирования отправки сообщения."""
|
|
327
|
+
if not delay:
|
|
328
|
+
if not period:
|
|
329
|
+
raise ValueError('Cannot schedule message without delay and period')
|
|
330
|
+
self._tell(actor=actor, message=message, sender=sender)
|
|
331
|
+
delay = period
|
|
332
|
+
|
|
333
|
+
ts = asyncio.get_event_loop().time() + delay
|
|
334
|
+
return asyncio.create_task(self._schedule_tell_loop(actor=actor, message=message, sender=sender, ts=ts, period=period))
|
|
335
|
+
|
|
336
|
+
async def _schedule_tell_loop(
|
|
337
|
+
self,
|
|
338
|
+
actor: Union[Actor, ActorRef],
|
|
339
|
+
message: Any,
|
|
340
|
+
*,
|
|
341
|
+
sender: Union[None, Actor, ActorRef],
|
|
342
|
+
ts: Union[None, int, float] = None,
|
|
343
|
+
period: Union[None, int, float] = None
|
|
344
|
+
) -> None:
|
|
345
|
+
"""Цикл для периодической отправки сообщения."""
|
|
346
|
+
actor = self._validate_actor_ref(actor)
|
|
347
|
+
delay = (ts - asyncio.get_event_loop().time()) if ts else 0
|
|
348
|
+
while True:
|
|
349
|
+
if delay > 0:
|
|
350
|
+
await asyncio.sleep(delay)
|
|
351
|
+
|
|
352
|
+
self._tell(actor=actor, message=message, sender=sender)
|
|
353
|
+
if not period:
|
|
354
|
+
break
|
|
355
|
+
|
|
356
|
+
delay = period
|
|
357
|
+
|
|
358
|
+
def _watch(self, actor: Union[Actor, ActorRef], other: Union[Actor, ActorRef]) -> None:
|
|
359
|
+
"""Внутренний метод для наблюдения за актором."""
|
|
360
|
+
actor = self._validate_actor_ref(actor)
|
|
361
|
+
|
|
362
|
+
if actor not in self._actors:
|
|
363
|
+
raise ValueError(f'Actor does not exist: {actor}')
|
|
364
|
+
|
|
365
|
+
other = self._validate_actor_ref(other)
|
|
366
|
+
if other not in self._actors:
|
|
367
|
+
raise ValueError(f'Actor does not exist: {other}')
|
|
368
|
+
|
|
369
|
+
if actor == other:
|
|
370
|
+
raise ValueError(f'Actor cannot watch themselves: {actor}')
|
|
371
|
+
|
|
372
|
+
actor_ctx = self._actors[actor]
|
|
373
|
+
other_ctx = self._actors[other]
|
|
374
|
+
actor_ctx.watching.add(other)
|
|
375
|
+
other_ctx.watched_by.add(actor)
|
|
376
|
+
|
|
377
|
+
def _unwatch(self, actor: Union[Actor, ActorRef], other: Union[Actor, ActorRef]) -> None:
|
|
378
|
+
"""Внутренний метод для прекращения наблюдения за актором."""
|
|
379
|
+
actor = self._validate_actor_ref(actor)
|
|
380
|
+
if actor not in self._actors:
|
|
381
|
+
raise ValueError(f'Actor does not exist: {actor}')
|
|
382
|
+
|
|
383
|
+
other = self._validate_actor_ref(other)
|
|
384
|
+
if actor == other:
|
|
385
|
+
raise ValueError(f'Actor cannot unwatch themselves: {actor}')
|
|
386
|
+
|
|
387
|
+
actor_ctx = self._actors[actor]
|
|
388
|
+
if other in actor_ctx.watching:
|
|
389
|
+
actor_ctx.watching.remove(other)
|
|
390
|
+
|
|
391
|
+
if other in self._actors:
|
|
392
|
+
other_ctx = self._actors[other]
|
|
393
|
+
if actor in other_ctx.watched_by:
|
|
394
|
+
other_ctx.watched_by.remove(actor)
|
|
395
|
+
|
|
396
|
+
async def _shutdown(self, timeout: Union[None, int, float] = None) -> None:
|
|
397
|
+
"""Внутренний метод завершения работы системы."""
|
|
398
|
+
if self._actors:
|
|
399
|
+
for actor_ref in self.children: # children propagate stop messages
|
|
400
|
+
self.stop(actor_ref)
|
|
401
|
+
|
|
402
|
+
lifecycle_tasks = [
|
|
403
|
+
task for task
|
|
404
|
+
in (actor_ctx.lifecycle for actor_ctx in self._actors.values())
|
|
405
|
+
if task is not None
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
done, pending = await asyncio.wait(lifecycle_tasks, timeout=timeout)
|
|
409
|
+
for lifecycle_task in pending:
|
|
410
|
+
lifecycle_task.cancel()
|
|
411
|
+
|
|
412
|
+
self._is_stopped.set()
|
|
413
|
+
|
|
414
|
+
async def _actor_lifecycle_loop(self, actor: Actor, actor_ref: ActorRef, actor_ctx: '_ActorContext') -> None:
|
|
415
|
+
"""Основной цикл жизни актора."""
|
|
416
|
+
try:
|
|
417
|
+
await actor.started()
|
|
418
|
+
actor_ctx.receiving_messages = True
|
|
419
|
+
|
|
420
|
+
except Exception as e:
|
|
421
|
+
log.exception('Exception raised while awaiting start of %s', actor_ref, exc_info=e)
|
|
422
|
+
|
|
423
|
+
while actor_ctx.receiving_messages:
|
|
424
|
+
sender, message = await actor_ctx.letterbox.get()
|
|
425
|
+
if isinstance(message, PoisonPill):
|
|
426
|
+
break
|
|
427
|
+
|
|
428
|
+
# --- Логика маршрутизации ---
|
|
429
|
+
# Проверяем, является ли сообщение маршрутизируемым.
|
|
430
|
+
# Это делается до вызова пользовательского `receive`.
|
|
431
|
+
message_handled_by_routing = False
|
|
432
|
+
if isinstance(message, dict) and message.get('action') == 'route_message':
|
|
433
|
+
try:
|
|
434
|
+
# Даем актору возможность обработать маршрутизацию самостоятельно
|
|
435
|
+
# (например, если у него своя логика для 'route_message')
|
|
436
|
+
# Если метод _route_message_logic возвращает True, значит он обработал.
|
|
437
|
+
# Мы предполагаем, что базовый Actor имеет этот метод.
|
|
438
|
+
|
|
439
|
+
if hasattr(actor, '_route_message_logic'):
|
|
440
|
+
# Вызываем напрямую, так как это вспомогательный метод
|
|
441
|
+
message_handled_by_routing = await actor._route_message_logic(sender, message)
|
|
442
|
+
except Exception as e:
|
|
443
|
+
log.error(f"Error in _route_message_logic for {actor_ref.path}: {e}")
|
|
444
|
+
|
|
445
|
+
# Если маршрутизация не обработала сообщение, передаём его в receive актора
|
|
446
|
+
if not message_handled_by_routing:
|
|
447
|
+
try:
|
|
448
|
+
await actor.receive(sender, message)
|
|
449
|
+
except Exception as e:
|
|
450
|
+
try:
|
|
451
|
+
await actor.restarted(sender, message, e)
|
|
452
|
+
except Exception as e:
|
|
453
|
+
log.exception(f'Exception raised while awaiting restart of {actor_ref}', exc_info=e)
|
|
454
|
+
# --- Конец логики маршрутизации ---
|
|
455
|
+
|
|
456
|
+
actor_ctx.receiving_messages = False
|
|
457
|
+
|
|
458
|
+
children_stopping = []
|
|
459
|
+
for child in actor_ctx.children:
|
|
460
|
+
child_ctx = self._actors[child]
|
|
461
|
+
children_stopping.append(child_ctx.is_stopped.wait())
|
|
462
|
+
self.stop(child)
|
|
463
|
+
|
|
464
|
+
if children_stopping:
|
|
465
|
+
await asyncio.wait(children_stopping)
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
await actor.stopped()
|
|
469
|
+
|
|
470
|
+
except Exception as e:
|
|
471
|
+
log.exception(f'Exception raised while awaiting stop of {actor_ref}', exc_info=e)
|
|
472
|
+
|
|
473
|
+
for other in actor_ctx.watched_by:
|
|
474
|
+
self._tell(other, Terminated(actor_ref), sender=None)
|
|
475
|
+
other_ctx = self._actors[other]
|
|
476
|
+
other_ctx.watching.remove(actor_ref)
|
|
477
|
+
|
|
478
|
+
for other in actor_ctx.watching:
|
|
479
|
+
other_ctx = self._actors[other]
|
|
480
|
+
other_ctx.watched_by.remove(actor_ref)
|
|
481
|
+
|
|
482
|
+
if actor_ctx.parent:
|
|
483
|
+
parent_ctx = self._actors[actor_ctx.parent]
|
|
484
|
+
parent_ctx.children.remove(actor_ref)
|
|
485
|
+
else:
|
|
486
|
+
self.children.remove(actor_ref)
|
|
487
|
+
|
|
488
|
+
while not actor_ctx.letterbox.empty():
|
|
489
|
+
sender, message = actor_ctx.letterbox.get_nowait()
|
|
490
|
+
if sender and sender != actor_ref:
|
|
491
|
+
deadletter = DeadLetter(actor_ref, message)
|
|
492
|
+
self._tell(sender, deadletter, sender=None)
|
|
493
|
+
|
|
494
|
+
actor_ctx.is_stopped.set()
|
|
495
|
+
del self._actors[actor_ref]
|
|
496
|
+
|
|
497
|
+
@staticmethod
|
|
498
|
+
def _validate_actor_ref(actor: Union[Actor, ActorRef]) -> ActorRef:
|
|
499
|
+
"""Проверяет и возвращает ActorRef."""
|
|
500
|
+
if isinstance(actor, Actor):
|
|
501
|
+
actor = actor.actor_ref
|
|
502
|
+
|
|
503
|
+
if not isinstance(actor, ActorRef):
|
|
504
|
+
raise ValueError(f'Not an actor: {actor}')
|
|
505
|
+
|
|
506
|
+
return actor
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class _ActorContext:
|
|
510
|
+
"""Контекст выполнения актора."""
|
|
511
|
+
|
|
512
|
+
def __init__(self, system: ActorSystem, actor_ref: ActorRef, parent: Optional[ActorRef]):
|
|
513
|
+
self.system: ActorSystem = system
|
|
514
|
+
self.actor_ref: ActorRef = actor_ref
|
|
515
|
+
self.parent: Optional[ActorRef] = parent
|
|
516
|
+
self.letterbox: asyncio.Queue = asyncio.Queue()
|
|
517
|
+
self.lifecycle: Optional[asyncio.Task] = None
|
|
518
|
+
self.watching: Set[ActorRef] = set()
|
|
519
|
+
self.watched_by: Set[ActorRef] = set()
|
|
520
|
+
self.children: List[ActorRef] = []
|
|
521
|
+
self.is_stopped: asyncio.Event = asyncio.Event()
|
|
522
|
+
self.receiving_messages: bool = False
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# actio/messages.py
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from . import ActorRef
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class PoisonPill:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class DeadLetter:
|
|
17
|
+
actor: ActorRef
|
|
18
|
+
message: Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Terminated:
|
|
23
|
+
actor: ActorRef
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# actio/ref.py
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import Any
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from dataclasses import field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(repr=False, eq=False, frozen=True)
|
|
13
|
+
class ActorRef:
|
|
14
|
+
actor_id: str
|
|
15
|
+
path: str
|
|
16
|
+
name: str
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return self.path
|
|
20
|
+
|
|
21
|
+
def __hash__(self):
|
|
22
|
+
return hash(self.actor_id)
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
return self.path
|
|
26
|
+
|
|
27
|
+
def __eq__(self, other):
|
|
28
|
+
return isinstance(other, ActorRef) and self.actor_id == other.actor_id
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ActorDefinition:
|
|
33
|
+
name: str
|
|
34
|
+
cls: type
|
|
35
|
+
parent: Optional[str]
|
|
36
|
+
replicas: int = 1
|
|
37
|
+
minimal: int = 1
|
|
38
|
+
config: Dict[str, Any] = field(default_factory=dict)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# actio/registry.py
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from typing import Dict
|
|
8
|
+
from typing import Any
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from . import ActorRef
|
|
13
|
+
from . import ActorDefinition
|
|
14
|
+
|
|
15
|
+
from . import ActorSystem
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger('actio.registry')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ActorRegistry:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self._definitions: Dict[str, ActorDefinition] = {}
|
|
23
|
+
|
|
24
|
+
def actio(
|
|
25
|
+
self,
|
|
26
|
+
name: Optional[str] = None,
|
|
27
|
+
parent: Optional[str] = None,
|
|
28
|
+
replicas: int = 1,
|
|
29
|
+
minimal: int = 1,
|
|
30
|
+
config: Optional[Dict[str, Any]] = None
|
|
31
|
+
):
|
|
32
|
+
|
|
33
|
+
def decorator(cls):
|
|
34
|
+
actor_name = name or cls.__name__
|
|
35
|
+
self._definitions[actor_name] = ActorDefinition(
|
|
36
|
+
name=actor_name,
|
|
37
|
+
cls=cls,
|
|
38
|
+
parent=parent,
|
|
39
|
+
replicas=replicas,
|
|
40
|
+
minimal=minimal,
|
|
41
|
+
config=config or {}
|
|
42
|
+
)
|
|
43
|
+
return cls
|
|
44
|
+
return decorator
|
|
45
|
+
|
|
46
|
+
async def build_actor_tree(
|
|
47
|
+
self,
|
|
48
|
+
system: ActorSystem,
|
|
49
|
+
root_name: str = 'MainTasks',
|
|
50
|
+
timeout: float = 5.0
|
|
51
|
+
) -> Dict[str, ActorRef]:
|
|
52
|
+
refs = {}
|
|
53
|
+
actor_instances = {}
|
|
54
|
+
|
|
55
|
+
# Создаем корневые акторы через систему
|
|
56
|
+
for defn in self._definitions.values():
|
|
57
|
+
if defn.parent is None:
|
|
58
|
+
actor_instance = defn.cls()
|
|
59
|
+
refs[defn.name] = system.create(actor_instance, name=defn.name)
|
|
60
|
+
actor_instances[defn.name] = actor_instance
|
|
61
|
+
|
|
62
|
+
# Создаем дочерние акторы через родительские ЭКЗЕМПЛЯРЫ акторов
|
|
63
|
+
created = set(refs.keys())
|
|
64
|
+
while len(created) < len(self._definitions):
|
|
65
|
+
for defn in self._definitions.values():
|
|
66
|
+
if defn.name not in created and defn.parent in created:
|
|
67
|
+
parent_instance = actor_instances[defn.parent] # Экземпляр актора-родителя
|
|
68
|
+
actor_instance = defn.cls()
|
|
69
|
+
|
|
70
|
+
# создаем через parent_instance.create(), а не parent_ref.create()
|
|
71
|
+
child_ref = parent_instance.create(actor_instance, name=defn.name)
|
|
72
|
+
refs[defn.name] = child_ref
|
|
73
|
+
actor_instances[defn.name] = actor_instance
|
|
74
|
+
|
|
75
|
+
# Сохраняем ссылку в родителе
|
|
76
|
+
if hasattr(parent_instance, 'actors') and isinstance(parent_instance.actors, dict):
|
|
77
|
+
parent_instance.actors[defn.name] = child_ref
|
|
78
|
+
|
|
79
|
+
created.add(defn.name)
|
|
80
|
+
|
|
81
|
+
start_time = asyncio.get_event_loop().time()
|
|
82
|
+
while asyncio.get_event_loop().time() - start_time < timeout:
|
|
83
|
+
all_started = True
|
|
84
|
+
for actor_instance in actor_instances.values():
|
|
85
|
+
if (
|
|
86
|
+
hasattr(actor_instance, '_context')
|
|
87
|
+
and actor_instance._context
|
|
88
|
+
and not actor_instance._context.receiving_messages
|
|
89
|
+
):
|
|
90
|
+
all_started = False
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
if all_started:
|
|
94
|
+
break
|
|
95
|
+
await asyncio.sleep(0.1)
|
|
96
|
+
|
|
97
|
+
return refs
|
|
98
|
+
|
|
99
|
+
def get_actor_graph(self) -> Dict[Optional[str], List[str]]:
|
|
100
|
+
graph = {}
|
|
101
|
+
for defn in self._definitions.values():
|
|
102
|
+
if defn.parent not in graph:
|
|
103
|
+
graph[defn.parent] = []
|
|
104
|
+
graph[defn.parent].append(defn.name)
|
|
105
|
+
|
|
106
|
+
return graph
|
|
107
|
+
|
|
108
|
+
def print_actor_tree(self):
|
|
109
|
+
"""Печатает дерево акторов в консоль"""
|
|
110
|
+
graph = self.get_actor_graph()
|
|
111
|
+
|
|
112
|
+
def print_node(parent: Optional[str], level: int = 0):
|
|
113
|
+
indent = " " * level
|
|
114
|
+
if parent in graph:
|
|
115
|
+
for child in graph[parent]:
|
|
116
|
+
defn = self._definitions[child]
|
|
117
|
+
log.warning(f"{indent}├── {child} (replicas={defn.replicas}, minimal={defn.minimal})")
|
|
118
|
+
print_node(child, level + 1)
|
|
119
|
+
elif parent is None:
|
|
120
|
+
for root in graph[None]:
|
|
121
|
+
defn = self._definitions[root]
|
|
122
|
+
log.warning(f"┌── {root} (replicas={defn.replicas}, minimal={defn.minimal})")
|
|
123
|
+
print_node(root, 1)
|
|
124
|
+
|
|
125
|
+
log.warning("Actor System Tree:")
|
|
126
|
+
print_node(None)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
registry = ActorRegistry()
|
|
130
|
+
actio = registry.actio
|