ezmsg 3.7.2__tar.gz → 3.8.0__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.
- {ezmsg-3.7.2 → ezmsg-3.8.0}/.github/workflows/ci.yml +1 -1
- {ezmsg-3.7.2 → ezmsg-3.8.0}/PKG-INFO +1 -1
- ezmsg-3.8.0/examples/simple_async_publisher.py +33 -0
- ezmsg-3.8.0/examples/simple_async_subscriber.py +34 -0
- ezmsg-3.8.0/examples/simple_publisher.py +35 -0
- ezmsg-3.8.0/examples/simple_subscriber.py +30 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/pyproject.toml +1 -1
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/__init__.py +6 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/backend.py +86 -3
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/backendprocess.py +194 -14
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/graphcontext.py +9 -1
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/graphserver.py +15 -5
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/netprotocol.py +4 -1
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/subclient.py +10 -5
- ezmsg-3.8.0/src/ezmsg/core/sync.py +444 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/unit.py +21 -7
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/debuglog.py +1 -1
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messagereplay.py +1 -1
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messages/key.py +2 -2
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messages/modify.py +1 -1
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/perf/impl.py +3 -3
- ezmsg-3.8.0/tests/clean_shutdown_examples_runner.py +171 -0
- ezmsg-3.8.0/tests/perf_sync_overhead.py +119 -0
- ezmsg-3.8.0/tests/shutdown_runner.py +102 -0
- ezmsg-3.8.0/tests/test_clean_shutdown.py +225 -0
- ezmsg-3.8.0/tests/test_messagereplay.py +92 -0
- ezmsg-3.8.0/tests/test_subclient.py +148 -0
- ezmsg-3.8.0/tests/test_sync_api.py +171 -0
- ezmsg-3.7.2/examples/lowlevel_api.py +0 -119
- {ezmsg-3.7.2 → ezmsg-3.8.0}/.flake8 +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/.git-blame-ignore-revs +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/.github/workflows/docs.yml +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/.github/workflows/publish.yml +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/.gitignore +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/.pre-commit-config.yaml +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/LICENSE +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/README.md +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/Makefile +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/make.bat +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/_static/_images/cloning.png +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/_static/_images/ezmsg_logo.png +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/_static/_images/tutorial_graph.png +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/_static/_images/tutorial_graph_compact.png +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/_static/_images/tutorial_graphviz.png +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/_static/_images/tutorial_pipeline.png +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/_static/_images/tutorial_system.png +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/_static/custom.css +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/conf.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/index.rst +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/reference/API/axisarray-util-units.rst +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/reference/API/axisarray.rst +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/reference/API/components.rst +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/reference/API/content-api.rst +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/reference/API/entrypoint.rst +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/reference/API/functiondecorators.rst +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/reference/API/util-units.rst +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/docs/source/reference/content-reference.rst +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/.gitignore +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/ezmsg_attach.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/ezmsg_configs.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/ezmsg_count.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/ezmsg_generator.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/ezmsg_intro.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/ezmsg_leaky_subscriber.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/ezmsg_log_to_file.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/ezmsg_normalterm.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/ezmsg_stop.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/examples/ezmsg_toy.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/__main__.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/addressable.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/backpressure.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/channelmanager.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/collection.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/command.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/component.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/dag.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/graph_util.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/message.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/messagecache.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/messagechannel.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/messagemarshal.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/pubclient.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/settings.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/shm.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/state.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/stream.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/test.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/core/util.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/__init__.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/gen_to_unit.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/generator.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messagecodec.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messagegate.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messagelogger.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messagequeue.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messages/__init__.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messages/axisarray.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messages/chunker.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/messages/util.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/perf/__init__.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/perf/analysis.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/perf/command.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/perf/envinfo.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/perf/run.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/perf/util.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/profiler.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/rate.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/src/ezmsg/util/terminate.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/__init__.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/ez_test_utils.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/messages/__init__.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/messages/test_axisarray.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/messages/test_chunker.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/messages/test_key.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/messages/test_modify.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_addressable.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_attach.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_channel.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_channelmanager.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_connections.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_dag.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_generator.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_graph.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_graph_visualization.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_leaky_subscriber.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_perf_configs.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_profiler.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_run.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_shm.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_state.py +0 -0
- {ezmsg-3.7.2 → ezmsg-3.8.0}/tests/test_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ezmsg
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.8.0
|
|
4
4
|
Summary: A simple DAG-based computation model
|
|
5
5
|
Author-email: Griffin Milsap <griffin.milsap@gmail.com>, Preston Peranich <pperanich@gmail.com>, Chadwick Boulay <chadwick.boulay@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import ezmsg.core as ez
|
|
4
|
+
|
|
5
|
+
TOPIC = "/TEST"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def main(host: str = "127.0.0.1", port: int = 12345) -> None:
|
|
9
|
+
async with ez.GraphContext((host, port), auto_start=True) as ctx:
|
|
10
|
+
pub = await ctx.publisher(TOPIC)
|
|
11
|
+
try:
|
|
12
|
+
print("Publisher Task Launched")
|
|
13
|
+
count = 0
|
|
14
|
+
while True:
|
|
15
|
+
await pub.broadcast(f"{count=}")
|
|
16
|
+
await asyncio.sleep(0.1)
|
|
17
|
+
count += 1
|
|
18
|
+
except asyncio.CancelledError:
|
|
19
|
+
pass
|
|
20
|
+
finally:
|
|
21
|
+
print("Publisher Task Concluded")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
from argparse import ArgumentParser
|
|
26
|
+
|
|
27
|
+
parser = ArgumentParser()
|
|
28
|
+
parser.add_argument("--host", default="127.0.0.1", help="hostname for graphserver")
|
|
29
|
+
parser.add_argument("--port", default=12345, type=int, help="port for graphserver")
|
|
30
|
+
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
asyncio.run(main(host=args.host, port=args.port))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import ezmsg.core as ez
|
|
4
|
+
|
|
5
|
+
TOPIC = "/TEST"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def main(host: str = "127.0.0.1", port: int = 12345) -> None:
|
|
9
|
+
async with ez.GraphContext((host, port), auto_start=True) as ctx:
|
|
10
|
+
sub = await ctx.subscriber(TOPIC)
|
|
11
|
+
try:
|
|
12
|
+
print("Subscriber Task Launched")
|
|
13
|
+
while True:
|
|
14
|
+
async with sub.recv_zero_copy() as msg:
|
|
15
|
+
# Uncomment if you want to witness backpressure!
|
|
16
|
+
# await asyncio.sleep(1.0)
|
|
17
|
+
print(msg)
|
|
18
|
+
except asyncio.CancelledError:
|
|
19
|
+
pass
|
|
20
|
+
finally:
|
|
21
|
+
print("Subscriber Task Concluded")
|
|
22
|
+
print("Detached")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
from argparse import ArgumentParser
|
|
27
|
+
|
|
28
|
+
parser = ArgumentParser()
|
|
29
|
+
parser.add_argument("--host", default="127.0.0.1", help="hostname for graphserver")
|
|
30
|
+
parser.add_argument("--port", default=12345, type=int, help="port for graphserver")
|
|
31
|
+
|
|
32
|
+
args = parser.parse_args()
|
|
33
|
+
|
|
34
|
+
asyncio.run(main(host=args.host, port=args.port))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import ezmsg.core as ez
|
|
4
|
+
|
|
5
|
+
TOPIC = "/TEST"
|
|
6
|
+
|
|
7
|
+
def main(host: str = "127.0.0.1", port: int = 12345) -> None:
|
|
8
|
+
with ez.sync.init((host, port), auto_start=True) as ctx:
|
|
9
|
+
pub = ctx.create_publisher(TOPIC, force_tcp=True)
|
|
10
|
+
|
|
11
|
+
print("Publisher Task Launched")
|
|
12
|
+
count = 0
|
|
13
|
+
try:
|
|
14
|
+
while True:
|
|
15
|
+
output = f"{count=}"
|
|
16
|
+
pub.publish(output)
|
|
17
|
+
print(output)
|
|
18
|
+
time.sleep(0.1)
|
|
19
|
+
count += 1
|
|
20
|
+
except KeyboardInterrupt:
|
|
21
|
+
pass
|
|
22
|
+
print("Publisher Task Concluded")
|
|
23
|
+
|
|
24
|
+
print("Done")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
from argparse import ArgumentParser
|
|
29
|
+
|
|
30
|
+
parser = ArgumentParser()
|
|
31
|
+
parser.add_argument("--host", default="127.0.0.1", help="hostname for graphserver")
|
|
32
|
+
parser.add_argument("--port", default=12345, type=int, help="port for graphserver")
|
|
33
|
+
args = parser.parse_args()
|
|
34
|
+
|
|
35
|
+
main(host=args.host, port=args.port)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import ezmsg.core as ez
|
|
2
|
+
|
|
3
|
+
TOPIC = "/TEST"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main(host: str = "127.0.0.1", port: int = 12345) -> None:
|
|
7
|
+
with ez.sync.init((host, port), auto_start=True) as ctx:
|
|
8
|
+
print("Subscriber Task Launched")
|
|
9
|
+
|
|
10
|
+
def on_message(msg: str) -> None:
|
|
11
|
+
# Uncomment if you want to witness backpressure!
|
|
12
|
+
# import time
|
|
13
|
+
# time.sleep(1.0)
|
|
14
|
+
print(msg)
|
|
15
|
+
|
|
16
|
+
ctx.create_subscription(TOPIC, callback=on_message)
|
|
17
|
+
ez.sync.spin(ctx)
|
|
18
|
+
|
|
19
|
+
print("Subscriber Task Concluded")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
from argparse import ArgumentParser
|
|
24
|
+
|
|
25
|
+
parser = ArgumentParser()
|
|
26
|
+
parser.add_argument("--host", default="127.0.0.1", help="hostname for graphserver")
|
|
27
|
+
parser.add_argument("--port", default=12345, type=int, help="port for graphserver")
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
main(host=args.host, port=args.port)
|
|
@@ -25,6 +25,10 @@ __all__ = [
|
|
|
25
25
|
"NormalTermination",
|
|
26
26
|
"GraphServer",
|
|
27
27
|
"GraphContext",
|
|
28
|
+
"sync",
|
|
29
|
+
"SyncContext",
|
|
30
|
+
"SyncPublisher",
|
|
31
|
+
"SyncSubscriber",
|
|
28
32
|
"run_command",
|
|
29
33
|
"Publisher",
|
|
30
34
|
"Subscriber",
|
|
@@ -45,6 +49,8 @@ from .backend import run, GraphRunner, GraphRunnerStartError
|
|
|
45
49
|
from .backendprocess import Complete, NormalTermination
|
|
46
50
|
from .graphserver import GraphServer
|
|
47
51
|
from .graphcontext import GraphContext
|
|
52
|
+
from . import sync
|
|
53
|
+
from .sync import SyncContext, SyncPublisher, SyncSubscriber
|
|
48
54
|
from .command import run_command
|
|
49
55
|
from .pubclient import Publisher
|
|
50
56
|
from .subclient import Subscriber
|
|
@@ -4,6 +4,7 @@ from collections.abc import Collection as AbstractCollection
|
|
|
4
4
|
import enum
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
|
+
import signal
|
|
7
8
|
from threading import BrokenBarrierError
|
|
8
9
|
from multiprocessing import Event, Barrier
|
|
9
10
|
from multiprocessing.synchronize import Event as EventType
|
|
@@ -23,6 +24,7 @@ from .graphcontext import GraphContext
|
|
|
23
24
|
from .backendprocess import (
|
|
24
25
|
BackendProcess,
|
|
25
26
|
DefaultBackendProcess,
|
|
27
|
+
ShutdownSummary,
|
|
26
28
|
new_threaded_event_loop,
|
|
27
29
|
)
|
|
28
30
|
|
|
@@ -170,6 +172,7 @@ class GraphRunner:
|
|
|
170
172
|
_graph_context: GraphContext | None
|
|
171
173
|
_loop: asyncio.AbstractEventLoop | None
|
|
172
174
|
_loop_cm: object | None
|
|
175
|
+
_loop_shutdown_summary: ShutdownSummary | None
|
|
173
176
|
_main_process: BackendProcess | None
|
|
174
177
|
_spawned_processes: list[BackendProcess]
|
|
175
178
|
_start_participant: bool
|
|
@@ -208,6 +211,7 @@ class GraphRunner:
|
|
|
208
211
|
self._graph_context = None
|
|
209
212
|
self._loop = None
|
|
210
213
|
self._loop_cm = None
|
|
214
|
+
self._loop_shutdown_summary = None
|
|
211
215
|
self._main_process = None
|
|
212
216
|
self._spawned_processes = []
|
|
213
217
|
self._start_participant = False
|
|
@@ -222,6 +226,15 @@ class GraphRunner:
|
|
|
222
226
|
return self._graph_context.graph_address
|
|
223
227
|
return self._graph_address
|
|
224
228
|
|
|
229
|
+
@property
|
|
230
|
+
def strict_shutdown(self) -> bool:
|
|
231
|
+
value = os.environ.get("EZMSG_STRICT_SHUTDOWN", "")
|
|
232
|
+
return value.lower() in ("1", "true", "yes", "on")
|
|
233
|
+
|
|
234
|
+
@strict_shutdown.setter
|
|
235
|
+
def strict_shutdown(self, value: bool) -> None:
|
|
236
|
+
os.environ["EZMSG_STRICT_SHUTDOWN"] = "1" if value else "0"
|
|
237
|
+
|
|
225
238
|
@property
|
|
226
239
|
def graph_server_spawned(self) -> bool:
|
|
227
240
|
return self._graph_server_spawned
|
|
@@ -311,7 +324,10 @@ class GraphRunner:
|
|
|
311
324
|
if self._execution_context is None:
|
|
312
325
|
return False
|
|
313
326
|
|
|
314
|
-
self.
|
|
327
|
+
self._loop_shutdown_summary = ShutdownSummary()
|
|
328
|
+
self._loop_cm = new_threaded_event_loop(
|
|
329
|
+
shutdown_summary=self._loop_shutdown_summary
|
|
330
|
+
)
|
|
315
331
|
self._loop = self._loop_cm.__enter__()
|
|
316
332
|
|
|
317
333
|
try:
|
|
@@ -380,12 +396,15 @@ class GraphRunner:
|
|
|
380
396
|
self._main_process = self._execution_context.processes[0]
|
|
381
397
|
self._start_processes(self._execution_context.processes[1:])
|
|
382
398
|
|
|
399
|
+
interrupts = 0
|
|
400
|
+
forced_sigint = False
|
|
383
401
|
try:
|
|
384
402
|
self._main_process.process(self._loop)
|
|
385
403
|
self._join_spawned_processes()
|
|
386
404
|
logger.info("All processes exited normally")
|
|
387
405
|
|
|
388
406
|
except KeyboardInterrupt:
|
|
407
|
+
interrupts += 1
|
|
389
408
|
logger.info(
|
|
390
409
|
"Attempting graceful shutdown, interrupt again to force quit..."
|
|
391
410
|
)
|
|
@@ -395,6 +414,8 @@ class GraphRunner:
|
|
|
395
414
|
self._join_spawned_processes()
|
|
396
415
|
|
|
397
416
|
except KeyboardInterrupt:
|
|
417
|
+
interrupts += 1
|
|
418
|
+
forced_sigint = True
|
|
398
419
|
logger.warning("Interrupt intercepted, force quitting")
|
|
399
420
|
self._execution_context.start_barrier.abort()
|
|
400
421
|
self._execution_context.stop_barrier.abort()
|
|
@@ -402,10 +423,72 @@ class GraphRunner:
|
|
|
402
423
|
proc.terminate()
|
|
403
424
|
|
|
404
425
|
finally:
|
|
405
|
-
|
|
406
|
-
|
|
426
|
+
while True:
|
|
427
|
+
try:
|
|
428
|
+
self._join_spawned_processes()
|
|
429
|
+
self._cleanup()
|
|
430
|
+
break
|
|
431
|
+
except KeyboardInterrupt:
|
|
432
|
+
interrupts += 1
|
|
433
|
+
if interrupts >= 2:
|
|
434
|
+
forced_sigint = True
|
|
435
|
+
logger.warning("Interrupt intercepted, force quitting")
|
|
436
|
+
if self._execution_context is not None:
|
|
437
|
+
self._execution_context.start_barrier.abort()
|
|
438
|
+
self._execution_context.stop_barrier.abort()
|
|
439
|
+
for proc in self._spawned_processes:
|
|
440
|
+
proc.terminate()
|
|
441
|
+
self._cleanup()
|
|
442
|
+
break
|
|
443
|
+
logger.info(
|
|
444
|
+
"Interrupt received during cleanup; attempting graceful shutdown..."
|
|
445
|
+
)
|
|
446
|
+
if self._execution_context is not None:
|
|
447
|
+
self._execution_context.term_ev.set()
|
|
407
448
|
self._started = False
|
|
408
449
|
self._stopped = True
|
|
450
|
+
if interrupts and not forced_sigint and self._shutdown_was_unclean():
|
|
451
|
+
forced_sigint = True
|
|
452
|
+
if forced_sigint:
|
|
453
|
+
self._exit_with_sigint()
|
|
454
|
+
|
|
455
|
+
def _shutdown_was_unclean(self) -> bool:
|
|
456
|
+
main_shutdown_errors = bool(
|
|
457
|
+
self._main_process is not None
|
|
458
|
+
and getattr(self._main_process, "_shutdown_errors", False)
|
|
459
|
+
)
|
|
460
|
+
summary = self._loop_shutdown_summary
|
|
461
|
+
loop_unclean = bool(summary is not None and summary.unclean)
|
|
462
|
+
return main_shutdown_errors or loop_unclean
|
|
463
|
+
|
|
464
|
+
def _exit_with_sigint(self) -> None:
|
|
465
|
+
code = 0xC000013A if os.name == "nt" else 130
|
|
466
|
+
if os.name == "nt":
|
|
467
|
+
try:
|
|
468
|
+
import ctypes
|
|
469
|
+
except Exception:
|
|
470
|
+
os._exit(1)
|
|
471
|
+
try:
|
|
472
|
+
ctypes.windll.kernel32.ExitProcess(ctypes.c_uint(code).value)
|
|
473
|
+
except Exception:
|
|
474
|
+
os._exit(ctypes.c_int32(code).value)
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
prev_handler = None
|
|
478
|
+
try:
|
|
479
|
+
prev_handler = signal.getsignal(signal.SIGINT)
|
|
480
|
+
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
481
|
+
signal.raise_signal(signal.SIGINT)
|
|
482
|
+
except Exception:
|
|
483
|
+
raise SystemExit(code)
|
|
484
|
+
finally:
|
|
485
|
+
if prev_handler is not None:
|
|
486
|
+
try:
|
|
487
|
+
signal.signal(signal.SIGINT, prev_handler)
|
|
488
|
+
except Exception:
|
|
489
|
+
pass
|
|
490
|
+
|
|
491
|
+
raise SystemExit(code)
|
|
409
492
|
|
|
410
493
|
def _cleanup(self) -> None:
|
|
411
494
|
if self._cleanup_done:
|
|
@@ -2,15 +2,19 @@ import asyncio
|
|
|
2
2
|
import concurrent.futures
|
|
3
3
|
import logging
|
|
4
4
|
import inspect
|
|
5
|
+
import os
|
|
5
6
|
import time
|
|
6
7
|
import traceback
|
|
7
8
|
import threading
|
|
9
|
+
import weakref
|
|
8
10
|
|
|
9
11
|
from abc import abstractmethod
|
|
12
|
+
from dataclasses import dataclass
|
|
10
13
|
from collections import defaultdict
|
|
11
14
|
from collections.abc import Callable, Coroutine, Generator, Sequence
|
|
12
15
|
from functools import wraps, partial
|
|
13
|
-
from
|
|
16
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
17
|
+
from concurrent.futures.thread import _worker
|
|
14
18
|
from multiprocessing import Process
|
|
15
19
|
from multiprocessing.synchronize import Event as EventType
|
|
16
20
|
from multiprocessing.synchronize import Barrier as BarrierType
|
|
@@ -19,8 +23,7 @@ from concurrent.futures import TimeoutError
|
|
|
19
23
|
from typing import Any
|
|
20
24
|
|
|
21
25
|
from .stream import Stream, InputStream, OutputStream
|
|
22
|
-
from .unit import Unit, TIMEIT_ATTR, SUBSCRIBES_ATTR
|
|
23
|
-
from .messagechannel import LeakyQueue
|
|
26
|
+
from .unit import Unit, TIMEIT_ATTR, SUBSCRIBES_ATTR
|
|
24
27
|
|
|
25
28
|
from .graphcontext import GraphContext
|
|
26
29
|
from .pubclient import Publisher
|
|
@@ -29,6 +32,75 @@ from .netprotocol import AddressType
|
|
|
29
32
|
|
|
30
33
|
logger = logging.getLogger("ezmsg")
|
|
31
34
|
|
|
35
|
+
STRICT_SHUTDOWN_ENV = "EZMSG_STRICT_SHUTDOWN"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _strict_shutdown_enabled() -> bool:
|
|
39
|
+
value = os.environ.get(STRICT_SHUTDOWN_ENV, "")
|
|
40
|
+
return value.lower() in ("1", "true", "yes", "on")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ShutdownSummary:
|
|
45
|
+
cancelled_tasks: int = 0
|
|
46
|
+
executor_active: int = 0
|
|
47
|
+
pending_tasks: int = 0
|
|
48
|
+
suppressed_errors: int = 0
|
|
49
|
+
forced_interrupt: bool = False
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def unclean(self) -> bool:
|
|
53
|
+
return bool(
|
|
54
|
+
self.executor_active
|
|
55
|
+
or self.pending_tasks
|
|
56
|
+
or self.suppressed_errors
|
|
57
|
+
or self.forced_interrupt
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _DaemonThreadPoolExecutor(ThreadPoolExecutor):
|
|
62
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
63
|
+
super().__init__(*args, **kwargs)
|
|
64
|
+
self._active_count = 0
|
|
65
|
+
self._active_lock = threading.Lock()
|
|
66
|
+
|
|
67
|
+
def _adjust_thread_count(self) -> None:
|
|
68
|
+
if self._broken:
|
|
69
|
+
return
|
|
70
|
+
num_threads = len(self._threads)
|
|
71
|
+
if num_threads >= self._max_workers:
|
|
72
|
+
return
|
|
73
|
+
thread_name = f"{self._thread_name_prefix or 'ThreadPool'}_{num_threads}"
|
|
74
|
+
thread = threading.Thread(
|
|
75
|
+
name=thread_name,
|
|
76
|
+
target=_worker,
|
|
77
|
+
args=(
|
|
78
|
+
weakref.ref(self),
|
|
79
|
+
self._work_queue,
|
|
80
|
+
self._initializer,
|
|
81
|
+
self._initargs,
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
thread.daemon = True
|
|
85
|
+
thread.start()
|
|
86
|
+
self._threads.add(thread)
|
|
87
|
+
|
|
88
|
+
def submit(self, fn, /, *args, **kwargs):
|
|
89
|
+
fut = super().submit(fn, *args, **kwargs)
|
|
90
|
+
with self._active_lock:
|
|
91
|
+
self._active_count += 1
|
|
92
|
+
|
|
93
|
+
def _decrement(_):
|
|
94
|
+
with self._active_lock:
|
|
95
|
+
self._active_count -= 1
|
|
96
|
+
|
|
97
|
+
fut.add_done_callback(_decrement)
|
|
98
|
+
return fut
|
|
99
|
+
|
|
100
|
+
def active_count(self) -> int:
|
|
101
|
+
with self._active_lock:
|
|
102
|
+
return self._active_count
|
|
103
|
+
|
|
32
104
|
|
|
33
105
|
class Complete(Exception):
|
|
34
106
|
"""
|
|
@@ -146,11 +218,13 @@ class DefaultBackendProcess(BackendProcess):
|
|
|
146
218
|
"""
|
|
147
219
|
|
|
148
220
|
pubs: dict[str, Publisher]
|
|
221
|
+
_shutdown_errors: bool
|
|
149
222
|
|
|
150
223
|
def process(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
151
224
|
main_func = None
|
|
152
225
|
context = GraphContext(self.graph_address)
|
|
153
226
|
coro_callables: dict[str, Callable[[], Coroutine[Any, Any, None]]] = dict()
|
|
227
|
+
self._shutdown_errors = False
|
|
154
228
|
|
|
155
229
|
try:
|
|
156
230
|
self.pubs = dict()
|
|
@@ -228,9 +302,14 @@ class DefaultBackendProcess(BackendProcess):
|
|
|
228
302
|
),
|
|
229
303
|
loop=loop,
|
|
230
304
|
).result()
|
|
305
|
+
|
|
306
|
+
except asyncio.CancelledError:
|
|
307
|
+
pass
|
|
308
|
+
|
|
231
309
|
except Exception:
|
|
232
310
|
self.start_barrier.abort()
|
|
233
|
-
logger.error(f"{traceback.format_exc()}")
|
|
311
|
+
# logger.error(f"{traceback.format_exc()}")
|
|
312
|
+
raise
|
|
234
313
|
|
|
235
314
|
try:
|
|
236
315
|
logger.debug("Waiting at start barrier!")
|
|
@@ -260,9 +339,9 @@ class DefaultBackendProcess(BackendProcess):
|
|
|
260
339
|
fn(unit)
|
|
261
340
|
except NormalTermination:
|
|
262
341
|
self.term_ev.set()
|
|
263
|
-
except Exception:
|
|
264
|
-
|
|
265
|
-
|
|
342
|
+
# except Exception:
|
|
343
|
+
# logger.error(f"Exception in Main: {unit.address}")
|
|
344
|
+
# logger.error(traceback.format_exc())
|
|
266
345
|
|
|
267
346
|
while True:
|
|
268
347
|
try:
|
|
@@ -309,12 +388,24 @@ class DefaultBackendProcess(BackendProcess):
|
|
|
309
388
|
else:
|
|
310
389
|
unit.shutdown() # type: ignore
|
|
311
390
|
|
|
312
|
-
asyncio.run_coroutine_threadsafe(shutdown_units(), loop=loop)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
391
|
+
shutdown_future = asyncio.run_coroutine_threadsafe(shutdown_units(), loop=loop)
|
|
392
|
+
try:
|
|
393
|
+
shutdown_future.result()
|
|
394
|
+
except KeyboardInterrupt:
|
|
395
|
+
logger.warning("Interrupted during unit shutdown. This may indicate units with slow shutdown methods."
|
|
396
|
+
"Re-trying... Press ctrl-c again to terminate immediately.")
|
|
397
|
+
shutdown_future.result()
|
|
316
398
|
|
|
317
|
-
asyncio.run_coroutine_threadsafe(context.revert(), loop=loop)
|
|
399
|
+
revert_future = asyncio.run_coroutine_threadsafe(context.revert(), loop=loop)
|
|
400
|
+
try:
|
|
401
|
+
revert_future.result()
|
|
402
|
+
except KeyboardInterrupt:
|
|
403
|
+
logger.warning("Interrupted during context revert."
|
|
404
|
+
"Re-trying... (will timeout in 10 seconds)")
|
|
405
|
+
try:
|
|
406
|
+
revert_future.result(timeout=10.0)
|
|
407
|
+
except TimeoutError:
|
|
408
|
+
logger.warning("Timed out waiting for retry on context revert")
|
|
318
409
|
|
|
319
410
|
logger.debug(f"Remaining tasks in event loop = {asyncio.all_tasks(loop)}")
|
|
320
411
|
|
|
@@ -340,6 +431,7 @@ class DefaultBackendProcess(BackendProcess):
|
|
|
340
431
|
self, unit: Unit, task: Callable
|
|
341
432
|
) -> Callable[..., Coroutine[Any, Any, None]]:
|
|
342
433
|
task_address = f"{unit.address}:{task.__name__}"
|
|
434
|
+
strict_shutdown = _strict_shutdown_enabled()
|
|
343
435
|
|
|
344
436
|
async def publish(stream: Stream, obj: Any) -> None:
|
|
345
437
|
if stream.address in self.pubs:
|
|
@@ -374,8 +466,6 @@ class DefaultBackendProcess(BackendProcess):
|
|
|
374
466
|
result = call_fn(msg)
|
|
375
467
|
if inspect.isasyncgen(result):
|
|
376
468
|
async for stream, obj in result:
|
|
377
|
-
if obj and getattr(task, ZERO_COPY_ATTR, False) and obj is msg:
|
|
378
|
-
obj = deepcopy(obj)
|
|
379
469
|
await pub_fn(stream, obj)
|
|
380
470
|
|
|
381
471
|
elif asyncio.iscoroutine(result):
|
|
@@ -390,9 +480,17 @@ class DefaultBackendProcess(BackendProcess):
|
|
|
390
480
|
self.term_ev.set()
|
|
391
481
|
raise
|
|
392
482
|
|
|
483
|
+
except asyncio.CancelledError:
|
|
484
|
+
# Normal during shutdown; propagate without logging.
|
|
485
|
+
raise
|
|
486
|
+
|
|
393
487
|
except Exception:
|
|
394
488
|
logger.error(f"Exception in Task: {task_address}")
|
|
395
489
|
logger.error(traceback.format_exc())
|
|
490
|
+
if self.term_ev.is_set():
|
|
491
|
+
self._shutdown_errors = True
|
|
492
|
+
if strict_shutdown:
|
|
493
|
+
raise
|
|
396
494
|
|
|
397
495
|
return wrapped_task
|
|
398
496
|
|
|
@@ -468,6 +566,7 @@ def run_loop(loop: asyncio.AbstractEventLoop):
|
|
|
468
566
|
@contextmanager
|
|
469
567
|
def new_threaded_event_loop(
|
|
470
568
|
ev: threading.Event | None = None,
|
|
569
|
+
shutdown_summary: ShutdownSummary | None = None,
|
|
471
570
|
) -> Generator[asyncio.AbstractEventLoop, None, None]:
|
|
472
571
|
"""
|
|
473
572
|
Create a new asyncio event loop running in a separate thread.
|
|
@@ -477,10 +576,30 @@ def new_threaded_event_loop(
|
|
|
477
576
|
|
|
478
577
|
:param ev: Optional event to signal when the loop is ready.
|
|
479
578
|
:type ev: threading.Event | None
|
|
579
|
+
:param shutdown_summary: Optional shutdown summary object to populate.
|
|
580
|
+
:type shutdown_summary: ShutdownSummary | None
|
|
480
581
|
:return: Context manager yielding the event loop.
|
|
481
582
|
:rtype: Generator[asyncio.AbstractEventLoop, None, None]
|
|
482
583
|
"""
|
|
483
584
|
loop = asyncio.new_event_loop()
|
|
585
|
+
strict_shutdown = _strict_shutdown_enabled()
|
|
586
|
+
shutdown_suppress = threading.Event()
|
|
587
|
+
suppressed_shutdown_errors = {"count": 0}
|
|
588
|
+
suppressed_lock = threading.Lock()
|
|
589
|
+
executor = None
|
|
590
|
+
if not strict_shutdown:
|
|
591
|
+
executor = _DaemonThreadPoolExecutor(thread_name_prefix="EZMSG")
|
|
592
|
+
loop.set_default_executor(executor)
|
|
593
|
+
def _loop_exception_handler(
|
|
594
|
+
loop_obj: asyncio.AbstractEventLoop, context: dict
|
|
595
|
+
) -> None:
|
|
596
|
+
if shutdown_suppress.is_set():
|
|
597
|
+
with suppressed_lock:
|
|
598
|
+
suppressed_shutdown_errors["count"] += 1
|
|
599
|
+
return
|
|
600
|
+
loop_obj.default_exception_handler(context)
|
|
601
|
+
|
|
602
|
+
loop.set_exception_handler(_loop_exception_handler)
|
|
484
603
|
thread = threading.Thread(target=run_loop, name="TaskThread", args=(loop,))
|
|
485
604
|
thread.start()
|
|
486
605
|
|
|
@@ -492,6 +611,67 @@ def new_threaded_event_loop(
|
|
|
492
611
|
logger.debug("Waiting at event...")
|
|
493
612
|
# ev.wait()
|
|
494
613
|
logger.debug("Stopping and closing task thread")
|
|
614
|
+
|
|
615
|
+
if not strict_shutdown:
|
|
616
|
+
shutdown_suppress.set()
|
|
617
|
+
# Cancel and await remaining tasks before stopping the loop.
|
|
618
|
+
async def _cancel_remaining(timeout: float = 1.0) -> tuple[int, int]:
|
|
619
|
+
tasks = [
|
|
620
|
+
t
|
|
621
|
+
for t in asyncio.all_tasks()
|
|
622
|
+
if t is not asyncio.current_task() and not t.done()
|
|
623
|
+
]
|
|
624
|
+
for t in tasks:
|
|
625
|
+
t.cancel()
|
|
626
|
+
if not tasks:
|
|
627
|
+
return 0, 0
|
|
628
|
+
_, pending = await asyncio.wait(tasks, timeout=timeout)
|
|
629
|
+
return len(tasks), len(pending)
|
|
630
|
+
|
|
631
|
+
cancelled_count = 0
|
|
632
|
+
pending_count = 0
|
|
633
|
+
forced_interrupt = False
|
|
634
|
+
fut = asyncio.run_coroutine_threadsafe(_cancel_remaining(), loop)
|
|
635
|
+
try:
|
|
636
|
+
cancelled_count, pending_count = fut.result()
|
|
637
|
+
except KeyboardInterrupt:
|
|
638
|
+
forced_interrupt = True
|
|
639
|
+
fut.cancel()
|
|
640
|
+
except Exception:
|
|
641
|
+
cancelled_count = 0
|
|
642
|
+
pending_count = 0
|
|
643
|
+
|
|
644
|
+
suppressed_count = suppressed_shutdown_errors["count"]
|
|
645
|
+
if cancelled_count or suppressed_count or forced_interrupt or pending_count:
|
|
646
|
+
if forced_interrupt and not cancelled_count and not suppressed_count:
|
|
647
|
+
logger.warning(
|
|
648
|
+
"Shutdown interrupted; tasks may still be running. "
|
|
649
|
+
"Re-run with EZMSG_STRICT_SHUTDOWN=1 to debug tasks with poor shutdown behavior."
|
|
650
|
+
)
|
|
651
|
+
elif pending_count:
|
|
652
|
+
logger.warning(
|
|
653
|
+
"Shutdown timed out waiting for %d task(s). "
|
|
654
|
+
"Re-run with EZMSG_STRICT_SHUTDOWN=1 to debug tasks with poor shutdown behavior.",
|
|
655
|
+
pending_count,
|
|
656
|
+
)
|
|
657
|
+
else:
|
|
658
|
+
logger.warning(
|
|
659
|
+
"Shutdown suppressed %d error(s) and cancelled %d task(s). "
|
|
660
|
+
"Shutdown was NOT clean; re-run with EZMSG_STRICT_SHUTDOWN=1 "
|
|
661
|
+
"to debug tasks with poor shutdown behavior.",
|
|
662
|
+
suppressed_count,
|
|
663
|
+
cancelled_count,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
if shutdown_summary is not None:
|
|
667
|
+
shutdown_summary.cancelled_tasks = cancelled_count
|
|
668
|
+
shutdown_summary.pending_tasks = pending_count
|
|
669
|
+
shutdown_summary.executor_active = (
|
|
670
|
+
executor.active_count() if executor is not None else 0
|
|
671
|
+
)
|
|
672
|
+
shutdown_summary.suppressed_errors = suppressed_count
|
|
673
|
+
shutdown_summary.forced_interrupt = forced_interrupt
|
|
674
|
+
|
|
495
675
|
loop.call_soon_threadsafe(loop.stop)
|
|
496
676
|
thread.join()
|
|
497
677
|
loop.close()
|