reactor-runtime 3.2.4__tar.gz → 3.2.6__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.
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/PKG-INFO +15 -2
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/README.md +13 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/pyproject.toml +2 -2
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/pyproject.toml.orig +2 -2
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/__init__.py +2 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/core/model.py +6 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/core/session.py +7 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/model/reactor_model.py +5 -1
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/log.py +148 -3
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/recording/recorder.py +16 -4
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/runner/runner.py +64 -15
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/runner/state_machine.py +5 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/serve.py +39 -3
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/config.py +18 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/peer.py +47 -4
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/signaling.py +2 -1
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/LICENSE +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/NOTICE +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/codes.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/core/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/core/fields.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/core/naming.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/core/service.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/core/transport.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/core/typespec.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/core/values.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/event_stream.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/http/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/http/events.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/http/routes.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/http/server.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/http/spec.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/client.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/events/decorators.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/events/errors.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/events/messages.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/internal/bridge.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/model/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/model/contract.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/model/schema.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/manifest.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/message_gateway.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/metrics.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/paths.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/protocol/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/protocol/base.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/protocol/common.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/protocol/v0/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/protocol/v0/codec.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/protocol/v1/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/protocol/v1/codec.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/py.typed +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/recording/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/recording/markers.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/runner/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/runner/connection_manager.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/runner/offer_epochs.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/schema.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/service.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/acceptor.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/router.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/__init__.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/acceptor.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/connection.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/frames.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/pacer.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/router.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/sdp.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/stats.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/version.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/upload_store.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/common_pb2.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/common_pb2.pyi +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/control_pb2.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/control_pb2.pyi +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/data_pb2.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/data_pb2.pyi +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/model_pb2.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/model_pb2.pyi +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/platform_pb2.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/platform_pb2.pyi +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/track_pb2.py +0 -0
- {reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_wire/v1/track_pb2.pyi +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactor-runtime
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.6
|
|
4
4
|
Summary: A Python framework for building real-time, interactive video models
|
|
5
5
|
Author: Reactor
|
|
6
6
|
Author-email: Reactor <team@reactor.inc>
|
|
@@ -18,7 +18,7 @@ Requires-Dist: numpy>=2.1
|
|
|
18
18
|
Requires-Dist: prometheus-client>=0.26.0
|
|
19
19
|
Requires-Dist: protobuf>=7.35.1
|
|
20
20
|
Requires-Dist: pyyaml>=6.0.3
|
|
21
|
-
Requires-Dist: reactor-webrtc==0.
|
|
21
|
+
Requires-Dist: reactor-webrtc==0.14.0
|
|
22
22
|
Requires-Dist: uvicorn>=0.49.0
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
24
|
Project-URL: Source, https://github.com/reactor-team/reactor-runtime
|
|
@@ -44,6 +44,7 @@ Reactor Runtime turns an inference pipeline into a real-time, interactive media
|
|
|
44
44
|
- 🎮 **Live interaction.** Clients send commands mid-generation: change a prompt, move a camera, adjust a parameter. The next frame reflects it.
|
|
45
45
|
- 🔌 **No transport code.** You never import a WebRTC library, manage a WebSocket, or encode video. The runtime ships its own media engine as a wheel, so a plain Python container is all a model needs.
|
|
46
46
|
- ✅ **Typed, validated commands.** Declare the commands your model accepts with standard Python types and constraints. The runtime validates every payload before your handler runs and compiles the surface into an OpenAPI schema that drives typed client SDKs.
|
|
47
|
+
- 🔎 **Traceable logs.** `get_logger()` writes structured records — readable `key=value` in a terminal, JSON for a log pipeline. Every record a session writes carries that session's id automatically, so one filter recovers everything a single run logged.
|
|
47
48
|
- 📦 **One container, anywhere.** The `reactor` CLI scaffolds a workspace, builds a small image, and runs it locally. The same image deploys to [Reactor](https://reactor.inc)'s GPU cloud unchanged.
|
|
48
49
|
|
|
49
50
|
## How it works
|
|
@@ -93,6 +94,18 @@ reactor run
|
|
|
93
94
|
|
|
94
95
|
`reactor run` builds a container with the runtime inside and serves WebRTC signaling on port 8080. Point a browser at it with the [JS SDK](https://docs.reactor.inc), or connect from the [Reactor Sandbox](https://reactor-sandbox.vercel.app/) and watch frames stream immediately.
|
|
95
96
|
|
|
97
|
+
Log from the same import, passing context as keyword arguments:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from reactor_runtime import get_logger
|
|
101
|
+
|
|
102
|
+
logger = get_logger(__name__)
|
|
103
|
+
|
|
104
|
+
logger.info("scene changed", prompt=self.prompt)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Records render as `key=value` text by default, or as one JSON object per line under `REACTOR_LOG_FORMAT=json`. While a session is live, its id is stamped on every record, so tracing one run's logs never requires threading an id through your call sites. Every record also carries the lifecycle phase it was written in, at both granularities: `state`, the session state machine's word, and `runtime_state`, the coarse word the health endpoint serves — so the logs of one phase — loading weights, a live session, teardown — are filterable by whichever vocabulary you are reading off another surface. The stamp is applied where records are written rather than where they are made, so a plain `logging.getLogger(__name__)` and the libraries your model imports are covered too.
|
|
108
|
+
|
|
96
109
|
## Install
|
|
97
110
|
|
|
98
111
|
Everything runs through the [`reactor` CLI](https://docs.reactor.inc/deploy/platform/installation). There is nothing to install on your host but the CLI and Docker; the runtime ships inside the image the CLI builds for your workspace.
|
|
@@ -18,6 +18,7 @@ Reactor Runtime turns an inference pipeline into a real-time, interactive media
|
|
|
18
18
|
- 🎮 **Live interaction.** Clients send commands mid-generation: change a prompt, move a camera, adjust a parameter. The next frame reflects it.
|
|
19
19
|
- 🔌 **No transport code.** You never import a WebRTC library, manage a WebSocket, or encode video. The runtime ships its own media engine as a wheel, so a plain Python container is all a model needs.
|
|
20
20
|
- ✅ **Typed, validated commands.** Declare the commands your model accepts with standard Python types and constraints. The runtime validates every payload before your handler runs and compiles the surface into an OpenAPI schema that drives typed client SDKs.
|
|
21
|
+
- 🔎 **Traceable logs.** `get_logger()` writes structured records — readable `key=value` in a terminal, JSON for a log pipeline. Every record a session writes carries that session's id automatically, so one filter recovers everything a single run logged.
|
|
21
22
|
- 📦 **One container, anywhere.** The `reactor` CLI scaffolds a workspace, builds a small image, and runs it locally. The same image deploys to [Reactor](https://reactor.inc)'s GPU cloud unchanged.
|
|
22
23
|
|
|
23
24
|
## How it works
|
|
@@ -67,6 +68,18 @@ reactor run
|
|
|
67
68
|
|
|
68
69
|
`reactor run` builds a container with the runtime inside and serves WebRTC signaling on port 8080. Point a browser at it with the [JS SDK](https://docs.reactor.inc), or connect from the [Reactor Sandbox](https://reactor-sandbox.vercel.app/) and watch frames stream immediately.
|
|
69
70
|
|
|
71
|
+
Log from the same import, passing context as keyword arguments:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from reactor_runtime import get_logger
|
|
75
|
+
|
|
76
|
+
logger = get_logger(__name__)
|
|
77
|
+
|
|
78
|
+
logger.info("scene changed", prompt=self.prompt)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Records render as `key=value` text by default, or as one JSON object per line under `REACTOR_LOG_FORMAT=json`. While a session is live, its id is stamped on every record, so tracing one run's logs never requires threading an id through your call sites. Every record also carries the lifecycle phase it was written in, at both granularities: `state`, the session state machine's word, and `runtime_state`, the coarse word the health endpoint serves — so the logs of one phase — loading weights, a live session, teardown — are filterable by whichever vocabulary you are reading off another surface. The stamp is applied where records are written rather than where they are made, so a plain `logging.getLogger(__name__)` and the libraries your model imports are covered too.
|
|
82
|
+
|
|
70
83
|
## Install
|
|
71
84
|
|
|
72
85
|
Everything runs through the [`reactor` CLI](https://docs.reactor.inc/deploy/platform/installation). There is nothing to install on your host but the CLI and Docker; the runtime ships inside the image the CLI builds for your workspace.
|
|
@@ -92,7 +92,7 @@ pythonpath = ["."]
|
|
|
92
92
|
|
|
93
93
|
[project]
|
|
94
94
|
name = "reactor-runtime"
|
|
95
|
-
version = "3.2.
|
|
95
|
+
version = "3.2.6"
|
|
96
96
|
description = "A Python framework for building real-time, interactive video models"
|
|
97
97
|
readme = "README.md"
|
|
98
98
|
license = "Apache-2.0"
|
|
@@ -115,7 +115,7 @@ dependencies = [
|
|
|
115
115
|
"prometheus-client>=0.26.0",
|
|
116
116
|
"protobuf>=7.35.1",
|
|
117
117
|
"pyyaml>=6.0.3",
|
|
118
|
-
"reactor-webrtc==0.
|
|
118
|
+
"reactor-webrtc==0.14.0",
|
|
119
119
|
"uvicorn>=0.49.0",
|
|
120
120
|
]
|
|
121
121
|
|
|
@@ -20,7 +20,7 @@ version = "1.20260814.7"
|
|
|
20
20
|
|
|
21
21
|
[project]
|
|
22
22
|
name = "reactor-runtime"
|
|
23
|
-
version = "3.2.
|
|
23
|
+
version = "3.2.6"
|
|
24
24
|
description = "A Python framework for building real-time, interactive video models"
|
|
25
25
|
readme = "README.md"
|
|
26
26
|
license = "Apache-2.0"
|
|
@@ -41,7 +41,7 @@ dependencies = [
|
|
|
41
41
|
"prometheus-client>=0.26.0",
|
|
42
42
|
"protobuf>=7.35.1",
|
|
43
43
|
"pyyaml>=6.0.3",
|
|
44
|
-
"reactor-webrtc==0.
|
|
44
|
+
"reactor-webrtc==0.14.0",
|
|
45
45
|
"uvicorn>=0.49.0",
|
|
46
46
|
]
|
|
47
47
|
|
|
@@ -49,6 +49,7 @@ from reactor_runtime.interface import (
|
|
|
49
49
|
session_ended,
|
|
50
50
|
session_started,
|
|
51
51
|
)
|
|
52
|
+
from reactor_runtime.log import get_logger
|
|
52
53
|
from reactor_runtime.paths import get_weights_path
|
|
53
54
|
|
|
54
55
|
__version__ = version("reactor-runtime")
|
|
@@ -89,6 +90,7 @@ __all__ = [
|
|
|
89
90
|
"disconnected",
|
|
90
91
|
"event",
|
|
91
92
|
"file_uploaded",
|
|
93
|
+
"get_logger",
|
|
92
94
|
"get_weights_path",
|
|
93
95
|
"session_ended",
|
|
94
96
|
"session_started",
|
|
@@ -238,10 +238,16 @@ class SessionEnded(ReactorEvent):
|
|
|
238
238
|
Attributes:
|
|
239
239
|
session_id: Identifier for the session that ended.
|
|
240
240
|
reason: Why the session ended.
|
|
241
|
+
_log_binding: The log session binding to retire once the
|
|
242
|
+
``@session_ended`` hook has run. Internal plumbing between the
|
|
243
|
+
runner and the reactor loop, never surfaced to a hook: dispatch is
|
|
244
|
+
what proves the hook's records were written while the binding was
|
|
245
|
+
live, so dispatch is where it is released.
|
|
241
246
|
"""
|
|
242
247
|
|
|
243
248
|
session_id: str
|
|
244
249
|
reason: EndReason
|
|
250
|
+
_log_binding: int = 0
|
|
245
251
|
|
|
246
252
|
|
|
247
253
|
@dataclass(frozen=True)
|
|
@@ -69,6 +69,12 @@ class SessionEvent(Enum):
|
|
|
69
69
|
``CLOSING``, and carries an :class:`~reactor_runtime.core.model.EndReason`
|
|
70
70
|
in ``detail.reason`` (and, for a crash, the error) so a consumer learns why.
|
|
71
71
|
|
|
72
|
+
``INITIALIZING`` records that the runtime is still loading its weights. It is
|
|
73
|
+
a self-loop legal only in ``CREATED``, emitted once at boot before the load
|
|
74
|
+
blocks, so a consumer replaying the journal observes the loading phase — the
|
|
75
|
+
one phase otherwise silent, since the runner emits nothing until it leaves
|
|
76
|
+
``CREATED`` on ``INITIALIZATION_SUCCESS``.
|
|
77
|
+
|
|
72
78
|
``CHUNK_READY``, ``CLIP_READY``, ``COMMAND``, ``ERROR``, and ``METRIC`` are
|
|
73
79
|
the journal-only events (:data:`JOURNAL_EVENTS`): facts recorded for an
|
|
74
80
|
external consumer rather than moves of the lifecycle. Each is a pure
|
|
@@ -81,6 +87,7 @@ class SessionEvent(Enum):
|
|
|
81
87
|
|
|
82
88
|
INITIALIZATION_SUCCESS = auto()
|
|
83
89
|
INITIALIZATION_FAIL = auto()
|
|
90
|
+
INITIALIZING = auto()
|
|
84
91
|
START_SESSION = auto()
|
|
85
92
|
STOP_SESSION = auto()
|
|
86
93
|
TIMEOUT = auto()
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/model/reactor_model.py
RENAMED
|
@@ -43,7 +43,7 @@ from reactor_runtime.interface.internal.reactor_core import (
|
|
|
43
43
|
RequestId,
|
|
44
44
|
)
|
|
45
45
|
from reactor_runtime.interface.model.contract import ModelContract
|
|
46
|
-
from reactor_runtime.log import get_logger
|
|
46
|
+
from reactor_runtime.log import get_logger, release_session_id
|
|
47
47
|
|
|
48
48
|
logger = get_logger(__name__)
|
|
49
49
|
|
|
@@ -188,6 +188,10 @@ class ReactorModel(ReactorCore):
|
|
|
188
188
|
self._set_connected(0)
|
|
189
189
|
await self._invoke_hook(hooks.session_ended, None)
|
|
190
190
|
self._clients.clear()
|
|
191
|
+
# The hook has returned, so its records were written while the
|
|
192
|
+
# session's log binding was live; the session's last ambient writer
|
|
193
|
+
# is done and the binding retires here, on the model thread.
|
|
194
|
+
release_session_id(event._log_binding)
|
|
191
195
|
elif isinstance(event, FileUploaded):
|
|
192
196
|
await self._invoke_hook(hooks.file_uploaded, event.conn_id, uploaded_file=event.file)
|
|
193
197
|
|
|
@@ -8,10 +8,25 @@ of two shapes, chosen by the ``REACTOR_LOG_FORMAT`` environment variable:
|
|
|
8
8
|
- ``json``: one JSON object per line, ready for a log pipeline to parse.
|
|
9
9
|
|
|
10
10
|
Call sites pass structured context as keyword arguments —
|
|
11
|
-
``log.info("
|
|
12
|
-
|
|
11
|
+
``log.info("chunk encoded", chunk_idx=idx)`` — and the active formatter renders
|
|
12
|
+
them; the wire shape is the formatter's concern, not the call site's.
|
|
13
13
|
``configure`` installs the chosen formatter on the root logger, and
|
|
14
14
|
``get_logger`` returns a logger to write through.
|
|
15
|
+
|
|
16
|
+
Three fields arrive without a call site naming them, stamped by
|
|
17
|
+
:class:`SessionContextFilter` on the handler ``configure`` installs. While a
|
|
18
|
+
session is live, every record carries its ``session_id``. From the moment the
|
|
19
|
+
runtime boots, every record carries the lifecycle it was written in, at both
|
|
20
|
+
granularities: ``state``, the session state machine's word — the same words the
|
|
21
|
+
session descriptor's ``state`` field serves — and ``runtime_state``, its coarse
|
|
22
|
+
projection, the words the health route serves (``loading`` / ``available`` /
|
|
23
|
+
``serving`` / ``terminated``). Carrying both means a reader can filter by
|
|
24
|
+
whichever vocabulary they read off a surface: the model-load window is
|
|
25
|
+
``state="created"`` and equally ``runtime_state="loading"``. Because the stamp
|
|
26
|
+
happens where records are written rather than where they are made, it reaches a
|
|
27
|
+
model's own ``logging.getLogger(__name__)`` and any third-party library that
|
|
28
|
+
propagates to root, so a line can be traced to the session and phase that
|
|
29
|
+
produced it without model code threading any of it through its call sites.
|
|
15
30
|
"""
|
|
16
31
|
|
|
17
32
|
from __future__ import annotations
|
|
@@ -35,6 +50,94 @@ _JSON_RESERVED = frozenset({"ts", "level", "logger", "msg", "exc_info"})
|
|
|
35
50
|
|
|
36
51
|
_QUOTE_TRIGGERS = (" ", "=", '"', "\n", "\r", "\t")
|
|
37
52
|
|
|
53
|
+
# The live session's id, stamped on every record while it is set. A module global
|
|
54
|
+
# rather than a ContextVar because a session fans its work across plain worker
|
|
55
|
+
# threads, which do not inherit context; the runtime hosts one session at a time,
|
|
56
|
+
# so a single value is unambiguous.
|
|
57
|
+
_session_id: str | None = None
|
|
58
|
+
|
|
59
|
+
# Counts bindings, so a release can name the one it retires. Two sessions may
|
|
60
|
+
# carry the same id — nothing stops a caller reusing one — and a release that
|
|
61
|
+
# matched on the id alone would unbind the session that reused it.
|
|
62
|
+
_session_binding = 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def set_session_id(session_id: str | None) -> int:
|
|
66
|
+
"""Stamp *session_id* on every record written from now on.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
session_id: The live session's id, or ``None`` to stamp nothing.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A token naming this binding, which :func:`release_session_id` takes to
|
|
73
|
+
retire it.
|
|
74
|
+
"""
|
|
75
|
+
global _session_id, _session_binding
|
|
76
|
+
_session_id = session_id
|
|
77
|
+
_session_binding += 1
|
|
78
|
+
return _session_binding
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def clear_session_id() -> None:
|
|
82
|
+
"""Stop stamping a session id, for the window between sessions."""
|
|
83
|
+
set_session_id(None)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def release_session_id(binding: int) -> None:
|
|
87
|
+
"""Retire the binding *binding* names, leaving a later one in place.
|
|
88
|
+
|
|
89
|
+
A session's teardown outlives the move that ends it, so the release that
|
|
90
|
+
follows one is deferred until that work has finished. By then the next
|
|
91
|
+
session may already have bound its own id — the same id, even, since nothing
|
|
92
|
+
stops a caller reusing one — so a release names the binding it retires rather
|
|
93
|
+
than the value that binding held.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
binding: The token :func:`set_session_id` returned, ignored once a later
|
|
97
|
+
binding has replaced the one it names.
|
|
98
|
+
"""
|
|
99
|
+
global _session_id
|
|
100
|
+
if _session_binding == binding:
|
|
101
|
+
_session_id = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_session_id() -> str | None:
|
|
105
|
+
"""Return the id currently being stamped, or ``None`` between sessions."""
|
|
106
|
+
return _session_id
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# The runtime's lifecycle, stamped on every record while set: the state
|
|
110
|
+
# machine's own word and its coarse projection. One fact at two granularities,
|
|
111
|
+
# so one setter binds both and they cannot drift apart. Unlike the session id
|
|
112
|
+
# they need no binding token: there is always exactly one current state and the
|
|
113
|
+
# latest write is by definition the truth, so last-write-wins is the correct
|
|
114
|
+
# semantics rather than a race to guard against.
|
|
115
|
+
_state: str | None = None
|
|
116
|
+
_runtime_state: str | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def set_state(state: str | None, runtime_state: str | None) -> None:
|
|
120
|
+
"""Stamp *state* and *runtime_state* on every record written from now on.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
state: The session state machine's word, or ``None`` to stamp nothing.
|
|
124
|
+
runtime_state: Its coarse lifecycle projection, the health route's
|
|
125
|
+
vocabulary, or ``None`` to stamp nothing.
|
|
126
|
+
"""
|
|
127
|
+
global _state, _runtime_state
|
|
128
|
+
_state = state
|
|
129
|
+
_runtime_state = runtime_state
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_state() -> str | None:
|
|
133
|
+
"""Return the machine word currently being stamped, or ``None`` before boot."""
|
|
134
|
+
return _state
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_runtime_state() -> str | None:
|
|
138
|
+
"""Return the coarse word currently being stamped, or ``None`` before boot."""
|
|
139
|
+
return _runtime_state
|
|
140
|
+
|
|
38
141
|
|
|
39
142
|
def _logfmt_value(value: Any) -> str:
|
|
40
143
|
"""Render *value* as a logfmt-safe token.
|
|
@@ -63,6 +166,37 @@ def _record_fields(record: logging.LogRecord) -> dict[str, Any]:
|
|
|
63
166
|
return {key: value for key, value in raw.items() if value is not None}
|
|
64
167
|
|
|
65
168
|
|
|
169
|
+
class SessionContextFilter(logging.Filter):
|
|
170
|
+
"""Stamp the live session's id and the runtime's state on every record.
|
|
171
|
+
|
|
172
|
+
Sits on the handler rather than on one logger, so it sees every record a
|
|
173
|
+
handler writes: the runtime's own, a model's ``logging.getLogger(__name__)``,
|
|
174
|
+
and a third-party library's that propagates to root. A call site that names
|
|
175
|
+
``session_id``, ``state``, or ``runtime_state`` itself keeps its own value —
|
|
176
|
+
a model logging its own ``state`` claims that record's field, deliberately —
|
|
177
|
+
and a field with nothing bound, the session id between sessions or the
|
|
178
|
+
states before boot, is absent rather than empty.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
182
|
+
"""Merge the ambient context into *record*'s structured fields."""
|
|
183
|
+
stamped = {
|
|
184
|
+
"session_id": _session_id,
|
|
185
|
+
"state": _state,
|
|
186
|
+
"runtime_state": _runtime_state,
|
|
187
|
+
}
|
|
188
|
+
context = {key: value for key, value in stamped.items() if value is not None}
|
|
189
|
+
if not context:
|
|
190
|
+
return True
|
|
191
|
+
fields = getattr(record, _REACTOR_FIELDS_ATTR, None)
|
|
192
|
+
if not isinstance(fields, dict):
|
|
193
|
+
setattr(record, _REACTOR_FIELDS_ATTR, context)
|
|
194
|
+
return True
|
|
195
|
+
for key, value in context.items():
|
|
196
|
+
fields.setdefault(key, value)
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
|
|
66
200
|
class TextFormatter(logging.Formatter):
|
|
67
201
|
"""Render a record as its message followed by ``key=value`` field tokens."""
|
|
68
202
|
|
|
@@ -159,7 +293,9 @@ def configure(*, level: int = logging.INFO, stream: IO[str] | None = None) -> No
|
|
|
159
293
|
The shape is chosen by ``REACTOR_LOG_FORMAT``: ``json`` for one JSON object
|
|
160
294
|
per line, anything else (the default) for human-readable ``key=value`` text.
|
|
161
295
|
Replaces any handlers already on the root logger so output has a single,
|
|
162
|
-
predictable shape.
|
|
296
|
+
predictable shape. The handler carries a :class:`SessionContextFilter`, so
|
|
297
|
+
every record written through it is stamped with the live session's id and
|
|
298
|
+
the runtime's lifecycle state at both granularities.
|
|
163
299
|
|
|
164
300
|
Args:
|
|
165
301
|
level: The level the root logger is set to.
|
|
@@ -171,6 +307,7 @@ def configure(*, level: int = logging.INFO, stream: IO[str] | None = None) -> No
|
|
|
171
307
|
)
|
|
172
308
|
handler = logging.StreamHandler(stream)
|
|
173
309
|
handler.setFormatter(formatter)
|
|
310
|
+
handler.addFilter(SessionContextFilter())
|
|
174
311
|
root = logging.getLogger()
|
|
175
312
|
for existing in root.handlers[:]:
|
|
176
313
|
root.removeHandler(existing)
|
|
@@ -180,8 +317,16 @@ def configure(*, level: int = logging.INFO, stream: IO[str] | None = None) -> No
|
|
|
180
317
|
|
|
181
318
|
__all__ = [
|
|
182
319
|
"JsonFormatter",
|
|
320
|
+
"SessionContextFilter",
|
|
183
321
|
"StructuredLogger",
|
|
184
322
|
"TextFormatter",
|
|
323
|
+
"clear_session_id",
|
|
185
324
|
"configure",
|
|
186
325
|
"get_logger",
|
|
326
|
+
"get_runtime_state",
|
|
327
|
+
"get_session_id",
|
|
328
|
+
"get_state",
|
|
329
|
+
"release_session_id",
|
|
330
|
+
"set_session_id",
|
|
331
|
+
"set_state",
|
|
187
332
|
]
|
|
@@ -369,7 +369,10 @@ class Recorder:
|
|
|
369
369
|
try:
|
|
370
370
|
(self._session_dir / _COMPLETE_MARKER).write_text("")
|
|
371
371
|
except OSError:
|
|
372
|
-
logger.exception(
|
|
372
|
+
logger.exception(
|
|
373
|
+
"failed to write recording completion marker",
|
|
374
|
+
session_id=self._session_id,
|
|
375
|
+
)
|
|
373
376
|
self._fire_ready_chunks()
|
|
374
377
|
self._fire_ready_clips()
|
|
375
378
|
self._watch_stop.set()
|
|
@@ -378,9 +381,15 @@ class Recorder:
|
|
|
378
381
|
if watch_thread is not None:
|
|
379
382
|
watch_thread.join(timeout=2.0)
|
|
380
383
|
self._started = False
|
|
384
|
+
# session_id is named explicitly, like the start's: the recorder outlives
|
|
385
|
+
# the session's ambient log context, which the model retires at its own
|
|
386
|
+
# session-ended dispatch, so a record written on the way out — this one,
|
|
387
|
+
# the marker and callback failures, the feed thread's exit — attributes
|
|
388
|
+
# itself.
|
|
381
389
|
logger.info(
|
|
382
390
|
"recorder stopped",
|
|
383
391
|
recording_id=self._session_id,
|
|
392
|
+
session_id=self._session_id,
|
|
384
393
|
dropped=self._dropped_frames,
|
|
385
394
|
)
|
|
386
395
|
|
|
@@ -651,7 +660,10 @@ class Recorder:
|
|
|
651
660
|
if self._has_audio and audio is not None:
|
|
652
661
|
encoder.feed_audio(audio)
|
|
653
662
|
except Exception:
|
|
654
|
-
logger.exception(
|
|
663
|
+
logger.exception(
|
|
664
|
+
"recorder encoder feed failed; disabling recording",
|
|
665
|
+
session_id=self._session_id,
|
|
666
|
+
)
|
|
655
667
|
self._disabled = True
|
|
656
668
|
self._drain_feed_queue()
|
|
657
669
|
return
|
|
@@ -812,7 +824,7 @@ class Recorder:
|
|
|
812
824
|
try:
|
|
813
825
|
callback(clip)
|
|
814
826
|
except Exception:
|
|
815
|
-
logger.exception("clip-ready callback raised")
|
|
827
|
+
logger.exception("clip-ready callback raised", session_id=self._session_id)
|
|
816
828
|
|
|
817
829
|
def _fire_ready_chunks(self) -> None:
|
|
818
830
|
"""Announce every recording segment that has closed since the last poll.
|
|
@@ -839,7 +851,7 @@ class Recorder:
|
|
|
839
851
|
try:
|
|
840
852
|
callback(recording_id, idx)
|
|
841
853
|
except Exception:
|
|
842
|
-
logger.exception("chunk-ready callback raised")
|
|
854
|
+
logger.exception("chunk-ready callback raised", session_id=recording_id)
|
|
843
855
|
|
|
844
856
|
# -- HTTP serving ---------------------------------------------------------
|
|
845
857
|
|
|
@@ -54,7 +54,7 @@ from reactor_runtime.interface.events.messages import ModelMessage
|
|
|
54
54
|
from reactor_runtime.interface.internal.bridge import ModelBridge
|
|
55
55
|
from reactor_runtime.interface.internal.reactor_core import MediaOps
|
|
56
56
|
from reactor_runtime.interface.model.contract import ModelContract
|
|
57
|
-
from reactor_runtime.log import get_logger
|
|
57
|
+
from reactor_runtime.log import get_logger, set_session_id, set_state
|
|
58
58
|
from reactor_runtime.manifest import import_model_class
|
|
59
59
|
from reactor_runtime.message_gateway import InboundCommand, MessageGateway
|
|
60
60
|
from reactor_runtime.metrics import (
|
|
@@ -98,6 +98,17 @@ _RUNTIME_STATES: dict[SessionState, RuntimeState] = {
|
|
|
98
98
|
SessionState.TERMINATED: RuntimeState.TERMINATED,
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
|
|
102
|
+
def _stamp_log_state(state: SessionState) -> None:
|
|
103
|
+
"""Bind the log's state context to *state*, at both granularities.
|
|
104
|
+
|
|
105
|
+
Records carry the machine's own word and the coarse word the health route
|
|
106
|
+
serves, so a reader can filter by whichever vocabulary the surface they are
|
|
107
|
+
looking at showed them.
|
|
108
|
+
"""
|
|
109
|
+
set_state(state.name.lower(), _RUNTIME_STATES[state].value)
|
|
110
|
+
|
|
111
|
+
|
|
101
112
|
# How long to wait for an upload's bytes to arrive when a command or notification
|
|
102
113
|
# references it before they are written. A client references an upload over the
|
|
103
114
|
# data channel while its bytes are still being delivered on a separate request,
|
|
@@ -169,6 +180,10 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
169
180
|
self._cfg = cfg
|
|
170
181
|
self._metrics = metrics or RuntimeMetrics(version=_server_version(), model=cfg.model_ref)
|
|
171
182
|
self._sm = SessionStateMachine()
|
|
183
|
+
# The log's state context starts at the machine's starting state, so the
|
|
184
|
+
# model-load window — records written before any transition — is already
|
|
185
|
+
# stamped; every later move re-stamps in _dispatch_transition.
|
|
186
|
+
_stamp_log_state(self._sm.current_state)
|
|
172
187
|
self._sm.on_transition(self._dispatch_transition)
|
|
173
188
|
# The session surface of the metrics is one listener over the same moves
|
|
174
189
|
# the journal carries, so no session code below calls an instrument.
|
|
@@ -206,12 +221,18 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
206
221
|
self._teardown: set[asyncio.Task[None]] = set()
|
|
207
222
|
self._orphan_task: asyncio.Task[None] | None = None
|
|
208
223
|
self._session_id = SESSION_ID
|
|
209
|
-
# The
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
224
|
+
# The session's own id, set per session in start_session: the id a
|
|
225
|
+
# recording is stored and addressed under, and the id stamped on the
|
|
226
|
+
# session's log records. Separate from the fixed transport session id so a
|
|
227
|
+
# caller can align both with the id it knows the session by; a session
|
|
228
|
+
# started without one mints a fresh id, so sequential recordings in a
|
|
229
|
+
# reused process never share a directory and the logs of one session are
|
|
230
|
+
# never read as another's. The construction value is an unused placeholder.
|
|
214
231
|
self._recording_id = SESSION_ID
|
|
232
|
+
# Names the log's current session binding, so the release that follows a
|
|
233
|
+
# session retires that binding and not a later session's. Zero until the
|
|
234
|
+
# first session binds one.
|
|
235
|
+
self._log_binding = 0
|
|
215
236
|
self._accepting = True
|
|
216
237
|
# The process-shutdown hook, wired by the assembly so the runner can ask
|
|
217
238
|
# the service to bring the process down when the session is terminated
|
|
@@ -237,11 +258,17 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
237
258
|
|
|
238
259
|
The model load runs off the event loop (it may block while it reads
|
|
239
260
|
weights), so the HTTP surface — already up by the time this runs — stays
|
|
240
|
-
responsive throughout
|
|
241
|
-
|
|
261
|
+
responsive throughout: a client subscribed to ``/events`` observes the
|
|
262
|
+
``initializing`` self-loop journalled before the load, then the
|
|
263
|
+
``initialization_success``/``initialization_fail`` transition when it ends.
|
|
242
264
|
"""
|
|
243
265
|
self._loop = asyncio.get_running_loop()
|
|
244
266
|
logger.info("loading model", model=self._cfg.model_ref)
|
|
267
|
+
# Journal the loading phase before the (blocking) load, so a consumer
|
|
268
|
+
# replaying /events sees the runtime is initializing during the load
|
|
269
|
+
# window rather than nothing until READY. A self-loop on CREATED: no
|
|
270
|
+
# state change, no side effect (the bridge is not built yet).
|
|
271
|
+
self._sm.send(SessionEvent.INITIALIZING)
|
|
245
272
|
started_at = time.monotonic()
|
|
246
273
|
try:
|
|
247
274
|
model_cls = import_model_class(self._cfg.model_ref)
|
|
@@ -580,11 +607,12 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
580
607
|
The rejection surfaces the current state so the caller can report the
|
|
581
608
|
precise reason. The parameters seed the session's initial state.
|
|
582
609
|
|
|
583
|
-
A ``session_id`` in *params* is adopted as
|
|
584
|
-
is stored and addressed under,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
610
|
+
A ``session_id`` in *params* is adopted as this session's own id: the id
|
|
611
|
+
its recording is stored and addressed under, and the id stamped on every
|
|
612
|
+
log record the session writes. A caller can therefore align both clips and
|
|
613
|
+
logs with the id it knows the session by. Absent one, a fresh id is minted
|
|
614
|
+
per session so sequential recordings never overwrite each other. The
|
|
615
|
+
transport session id is unaffected — it is always :data:`SESSION_ID`.
|
|
588
616
|
|
|
589
617
|
Args:
|
|
590
618
|
params: The initial session parameters supplied by the caller.
|
|
@@ -1078,11 +1106,27 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
1078
1106
|
declare a dead model ready again. Real moves log at info; journal
|
|
1079
1107
|
self-loops log at debug so a per-segment ``chunk_ready`` does not flood
|
|
1080
1108
|
the log.
|
|
1109
|
+
|
|
1110
|
+
The session boundary is also where the log's session context binds, so
|
|
1111
|
+
every record written while a session is live names it, the opening move
|
|
1112
|
+
included. The release travels differently: it rides the ``SessionEnded``
|
|
1113
|
+
event into the model, whose dispatch retires the binding once the
|
|
1114
|
+
``@session_ended`` hook has returned. A terminal move dispatches no
|
|
1115
|
+
``SessionEnded`` and releases nothing — the process is exiting, and its
|
|
1116
|
+
last records belong to the session that brought it down. The log's state
|
|
1117
|
+
context re-stamps here too, before the move's own line, so a record
|
|
1118
|
+
reads the state the process was in when it was written.
|
|
1081
1119
|
"""
|
|
1120
|
+
if transition.is_session_start:
|
|
1121
|
+
self._log_binding = set_session_id(self._recording_id)
|
|
1122
|
+
if transition.from_state is not transition.to_state:
|
|
1123
|
+
_stamp_log_state(transition.to_state)
|
|
1082
1124
|
log = logger.debug if transition.event in JOURNAL_EVENTS else logger.info
|
|
1125
|
+
# The fixed transport id (SESSION_ID) is deliberately not a field here:
|
|
1126
|
+
# one constant value per process carries nothing, and squatting on
|
|
1127
|
+
# session_id would mask the id the session is known by.
|
|
1083
1128
|
log(
|
|
1084
1129
|
"session transition",
|
|
1085
|
-
session_id=self._session_id,
|
|
1086
1130
|
event=transition.event.name.lower(),
|
|
1087
1131
|
from_state=transition.from_state.name.lower(),
|
|
1088
1132
|
to_state=transition.to_state.name.lower(),
|
|
@@ -1138,7 +1182,12 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
1138
1182
|
bridge.dispatch_reactor_event(SessionStarted(self._session_id))
|
|
1139
1183
|
if transition.is_session_end:
|
|
1140
1184
|
reason = transition.detail.get("reason", EndReason.STOPPED)
|
|
1141
|
-
bridge.dispatch_reactor_event(
|
|
1185
|
+
bridge.dispatch_reactor_event(
|
|
1186
|
+
# The event carries the log binding so its dispatch — the point
|
|
1187
|
+
# where the @session_ended hook has provably returned — is what
|
|
1188
|
+
# retires it, on the model thread.
|
|
1189
|
+
SessionEnded(self._session_id, reason, self._log_binding)
|
|
1190
|
+
)
|
|
1142
1191
|
if transition.event is SessionEvent.CONNECTION_OPENED:
|
|
1143
1192
|
bridge.dispatch_reactor_event(
|
|
1144
1193
|
ClientConnected(transition.detail["conn_id"], self._connections.count)
|
|
@@ -42,6 +42,11 @@ from reactor_runtime.core import JOURNAL_EVENTS, SessionEvent, SessionState, Tra
|
|
|
42
42
|
_TRANSITIONS: dict[SessionEvent, dict[SessionState, SessionState]] = {
|
|
43
43
|
SessionEvent.INITIALIZATION_SUCCESS: {SessionState.CREATED: SessionState.READY},
|
|
44
44
|
SessionEvent.INITIALIZATION_FAIL: {SessionState.CREATED: SessionState.TERMINATED},
|
|
45
|
+
# INITIALIZING is the runtime's "still loading" fact: a self-loop legal only
|
|
46
|
+
# in CREATED, emitted once at boot so a journal consumer sees the loading
|
|
47
|
+
# phase that is otherwise silent until INITIALIZATION_SUCCESS leaves CREATED.
|
|
48
|
+
# It changes no state and leaves the connection count alone (see _update_count).
|
|
49
|
+
SessionEvent.INITIALIZING: {SessionState.CREATED: SessionState.CREATED},
|
|
45
50
|
SessionEvent.START_SESSION: {SessionState.READY: SessionState.WAITING},
|
|
46
51
|
SessionEvent.STOP_SESSION: {
|
|
47
52
|
SessionState.STREAMING: SessionState.CLOSING,
|
|
@@ -8,8 +8,9 @@ platform.
|
|
|
8
8
|
|
|
9
9
|
This is also the runtime's one configuration boundary: the manifest names the
|
|
10
10
|
model, and the surrounding deployment names everything else (bind address, the
|
|
11
|
-
ICE servers and port range, the congestion-control
|
|
12
|
-
codec preference order, the lifecycle timeouts) through
|
|
11
|
+
ICE servers and port range, the congestion-control and per-sender bitrate
|
|
12
|
+
limits, the video codec preference order, the lifecycle timeouts) through
|
|
13
|
+
environment variables.
|
|
13
14
|
The transport and lifecycle config objects stay free of environment reads; the
|
|
14
15
|
small adapter here is the only place that translates the outside world into
|
|
15
16
|
them.
|
|
@@ -26,7 +27,7 @@ import sys
|
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
|
|
28
29
|
from reactor_runtime import log
|
|
29
|
-
from reactor_runtime.core import RuntimeConfig
|
|
30
|
+
from reactor_runtime.core import RuntimeConfig, RuntimeState, SessionState
|
|
30
31
|
from reactor_runtime.http import HttpServer
|
|
31
32
|
from reactor_runtime.manifest import MANIFEST, load_config
|
|
32
33
|
from reactor_runtime.metrics import RuntimeMetrics
|
|
@@ -200,6 +201,32 @@ def _bwe_limits_from_env() -> tuple[int, int, int]:
|
|
|
200
201
|
return min_kbps, max_kbps, initial_kbps
|
|
201
202
|
|
|
202
203
|
|
|
204
|
+
def _sender_limits_from_env() -> tuple[int, int]:
|
|
205
|
+
"""Read the per-sender bitrate bounds, checked at boot like the BWE ones.
|
|
206
|
+
|
|
207
|
+
These bound one track's encoder, where the BWE limits bound the whole
|
|
208
|
+
connection's estimate. Both must allow a rate for a stream to reach it, and
|
|
209
|
+
the per-sender ceiling is the one that lifts libwebrtc's resolution-keyed
|
|
210
|
+
default of 2500 kbps.
|
|
211
|
+
|
|
212
|
+
``0`` or less means "leave this bound at the libwebrtc default", matching how
|
|
213
|
+
the rest of this config spells an absent limit. A negative is not an error
|
|
214
|
+
here for that reason, but ``min`` above ``max`` is: libwebrtc refuses the
|
|
215
|
+
pair, and finding out at boot beats finding out on every negotiation.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
SystemExit: If the ordering does not hold.
|
|
219
|
+
"""
|
|
220
|
+
max_kbps = _int_env("WEBRTC_SENDER_MAX_KBPS", WebRtcConfig.sender_max_kbps)
|
|
221
|
+
min_kbps = _int_env("WEBRTC_SENDER_MIN_KBPS", WebRtcConfig.sender_min_kbps)
|
|
222
|
+
if min_kbps > 0 < max_kbps and min_kbps > max_kbps:
|
|
223
|
+
raise SystemExit(
|
|
224
|
+
"WEBRTC_SENDER_MIN_KBPS must not exceed WEBRTC_SENDER_MAX_KBPS; "
|
|
225
|
+
f"got {min_kbps} > {max_kbps}"
|
|
226
|
+
)
|
|
227
|
+
return max_kbps, min_kbps
|
|
228
|
+
|
|
229
|
+
|
|
203
230
|
def _webrtc_config_from_env() -> WebRtcConfig:
|
|
204
231
|
"""Build the WebRTC transport config from the environment.
|
|
205
232
|
|
|
@@ -207,6 +234,7 @@ def _webrtc_config_from_env() -> WebRtcConfig:
|
|
|
207
234
|
single place the outside world is translated into it.
|
|
208
235
|
"""
|
|
209
236
|
bwe_min_kbps, bwe_max_kbps, bwe_initial_kbps = _bwe_limits_from_env()
|
|
237
|
+
sender_max_kbps, sender_min_kbps = _sender_limits_from_env()
|
|
210
238
|
return WebRtcConfig(
|
|
211
239
|
ice_servers=_ice_servers_from_env(),
|
|
212
240
|
port_range=_port_range_from_env(),
|
|
@@ -216,6 +244,8 @@ def _webrtc_config_from_env() -> WebRtcConfig:
|
|
|
216
244
|
bwe_min_kbps=bwe_min_kbps,
|
|
217
245
|
bwe_max_kbps=bwe_max_kbps,
|
|
218
246
|
bwe_initial_kbps=bwe_initial_kbps,
|
|
247
|
+
sender_max_kbps=sender_max_kbps,
|
|
248
|
+
sender_min_kbps=sender_min_kbps,
|
|
219
249
|
)
|
|
220
250
|
|
|
221
251
|
|
|
@@ -342,6 +372,12 @@ def main() -> None:
|
|
|
342
372
|
from reactor_runtime.transport.webrtc.peer import libwebrtc_peer_factory
|
|
343
373
|
|
|
344
374
|
log.configure(level=_log_level_from_env())
|
|
375
|
+
# The runner re-stamps this at construction and on every move; stamping here
|
|
376
|
+
# too covers the lines written before it exists — the process is up and the
|
|
377
|
+
# model is not loaded, which is exactly what CREATED names and what LOADING
|
|
378
|
+
# projects it to — so every record the process writes carries its lifecycle,
|
|
379
|
+
# the first one included.
|
|
380
|
+
log.set_state(SessionState.CREATED.name.lower(), RuntimeState.LOADING.value)
|
|
345
381
|
manifest = Path.cwd() / MANIFEST
|
|
346
382
|
if not manifest.is_file():
|
|
347
383
|
raise SystemExit(f"no {MANIFEST} found in {Path.cwd()}")
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/config.py
RENAMED
|
@@ -97,6 +97,22 @@ class WebRtcConfig:
|
|
|
97
97
|
bwe_initial_kbps: Starting bitrate before estimates arrive.
|
|
98
98
|
bwe_target_update_threshold: Relative change below which a new bitrate
|
|
99
99
|
estimate is ignored rather than re-applied to the encoders.
|
|
100
|
+
sender_max_kbps: Ceiling for each sendonly *video* track's own encoder,
|
|
101
|
+
which is
|
|
102
|
+
a different limit from ``bwe_max_kbps`` and the one that actually
|
|
103
|
+
caps a video stream. The two are conjunctive — the lower wins — and
|
|
104
|
+
without this a sender's maximum comes from libwebrtc's
|
|
105
|
+
resolution-keyed default, which is 2500 kbps for anything above
|
|
106
|
+
960x540. Every frame size we send at 720p or larger would cap at
|
|
107
|
+
2.5 Mbps no matter how much headroom the estimate had. Audio senders
|
|
108
|
+
are left alone: that default is keyed on frame size, so there is no
|
|
109
|
+
equivalent for them to clear. ``0`` or less leaves the libwebrtc
|
|
110
|
+
default in place.
|
|
111
|
+
sender_min_kbps: Floor for each sendonly video track's own encoder. ``0`` or
|
|
112
|
+
less leaves it unset, which is the default: a floor stops the
|
|
113
|
+
encoder degrading gracefully, so on a link that cannot sustain it
|
|
114
|
+
the trade is lower quality for packet loss. Useful mainly when
|
|
115
|
+
several tracks compete and one must be preserved.
|
|
100
116
|
rtx_max_size_packets: Retransmission history depth, in packets.
|
|
101
117
|
rtx_max_size_time_ms: Retransmission history depth, in milliseconds;
|
|
102
118
|
``0`` means no time limit.
|
|
@@ -134,6 +150,8 @@ class WebRtcConfig:
|
|
|
134
150
|
bwe_max_kbps: int = 10000
|
|
135
151
|
bwe_initial_kbps: int = 4000
|
|
136
152
|
bwe_target_update_threshold: float = 0.05
|
|
153
|
+
sender_max_kbps: int = 10000
|
|
154
|
+
sender_min_kbps: int = 0
|
|
137
155
|
rtx_max_size_packets: int = 512
|
|
138
156
|
rtx_max_size_time_ms: int = 200
|
|
139
157
|
rtp_payload_mtu: int = 1200
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/peer.py
RENAMED
|
@@ -17,8 +17,8 @@ garble.
|
|
|
17
17
|
Per-peer audio isolation
|
|
18
18
|
------------------------
|
|
19
19
|
Each outbound audio track is created with
|
|
20
|
-
``PeerConnectionFactory.
|
|
21
|
-
track with a ``LocalAudioSource`` — a custom ``AudioSourceInterface`` that
|
|
20
|
+
``PeerConnectionFactory.create_audio_track_with_options(source=LocalPush)``,
|
|
21
|
+
which backs the track with a ``LocalAudioSource`` — a custom ``AudioSourceInterface`` that
|
|
22
22
|
maintains the list of sinks registered by each peer connection's voice send
|
|
23
23
|
channel. When ``track.push_pcm()`` is called, it delivers PCM directly to that
|
|
24
24
|
track's encoder via ``AudioTrackSinkInterface::OnData``, bypassing the shared
|
|
@@ -272,6 +272,36 @@ async def _apply_bitrate_limits(pc: rw.PeerConnection, config: WebRtcConfig) ->
|
|
|
272
272
|
)
|
|
273
273
|
|
|
274
274
|
|
|
275
|
+
async def _apply_sender_bitrate(transceiver: rw.Transceiver, config: WebRtcConfig) -> None:
|
|
276
|
+
"""Apply the configured per-sender bitrate bounds to one sendonly video transceiver.
|
|
277
|
+
|
|
278
|
+
A different limit from ``_apply_bitrate_limits``: that one bounds the whole
|
|
279
|
+
connection's congestion-control estimate, this one bounds what a single
|
|
280
|
+
track's encoder may spend of it. They are conjunctive, so a stream runs fast
|
|
281
|
+
only when both allow it — and this is the one that has to be raised, because
|
|
282
|
+
with nothing set libwebrtc derives a sender's ceiling from the frame size
|
|
283
|
+
alone and it is 2500 kbps for anything above 960x540.
|
|
284
|
+
|
|
285
|
+
**Video only**, for two reasons that agree. The default being lifted is
|
|
286
|
+
``GetMaxDefaultVideoBitrateKbps``, keyed on frame size, so there is no
|
|
287
|
+
equivalent for an audio sender to clear — the config's 10 Mbps ceiling would
|
|
288
|
+
say nothing to a 64 kbps Opus stream. (The bounds themselves do apply to
|
|
289
|
+
audio and would cap its allocation; we simply have no reason to.) And an
|
|
290
|
+
audio transceiver materialised from a remote offer has no encodings to write
|
|
291
|
+
until the answer is applied, where a video one has them as soon as the
|
|
292
|
+
transceiver exists, so calling this here would fail on every audio track.
|
|
293
|
+
|
|
294
|
+
``0`` or less in the config leaves that bound at the libwebrtc default, which
|
|
295
|
+
is how the rest of this config spells an absent limit; the binding takes
|
|
296
|
+
``None`` for it. Applied per negotiation, since every offer rebuilds the peer
|
|
297
|
+
and its transceivers come up on library defaults.
|
|
298
|
+
"""
|
|
299
|
+
await transceiver.set_send_bitrate(
|
|
300
|
+
min_bps=config.sender_min_kbps * 1000 if config.sender_min_kbps > 0 else None,
|
|
301
|
+
max_bps=config.sender_max_kbps * 1000 if config.sender_max_kbps > 0 else None,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
275
305
|
def _is_terminal_state(state: rw.PeerConnectionState) -> bool:
|
|
276
306
|
"""Return whether a peer-connection state means the wire is gone.
|
|
277
307
|
|
|
@@ -513,9 +543,16 @@ class WebRTCPeer:
|
|
|
513
543
|
if info.kind is TrackKind.VIDEO:
|
|
514
544
|
track = factory.create_video_track(info.name)
|
|
515
545
|
else:
|
|
516
|
-
|
|
546
|
+
# LocalPush, not the factory ADM: each outbound audio track
|
|
547
|
+
# gets its own source, so one peer's audio cannot reach
|
|
548
|
+
# another's encoder. See the module docstring.
|
|
549
|
+
track = factory.create_audio_track_with_options(
|
|
550
|
+
info.name, source=rw.AudioTrackSource.LocalPush
|
|
551
|
+
)
|
|
517
552
|
await transceiver.set_track(track)
|
|
518
553
|
await transceiver.set_direction(rw.TransceiverDirection.SendOnly)
|
|
554
|
+
if info.kind is TrackKind.VIDEO:
|
|
555
|
+
await _apply_sender_bitrate(transceiver, self._config)
|
|
519
556
|
self._out_tracks[info.name] = track
|
|
520
557
|
if codec_preferences and transceiver.kind() == rw.MediaKind.Video:
|
|
521
558
|
await transceiver.set_codec_preferences(codec_preferences)
|
|
@@ -873,7 +910,11 @@ class WebRTCPeer:
|
|
|
873
910
|
logger.debug("data-channel send failed", exc_info=True)
|
|
874
911
|
|
|
875
912
|
async def add_ice(self, candidate: IceCandidate) -> None:
|
|
876
|
-
"""Add a trickle-ICE candidate; valid before and after the wire connects.
|
|
913
|
+
"""Add a trickle-ICE candidate; valid before and after the wire connects.
|
|
914
|
+
|
|
915
|
+
An empty candidate string is the end-of-candidates marker (RFC 8838);
|
|
916
|
+
the native binding accepts it as a no-op.
|
|
917
|
+
"""
|
|
877
918
|
pc = self._pc
|
|
878
919
|
if self._stop_event.is_set() or pc is None:
|
|
879
920
|
return
|
|
@@ -1003,6 +1044,8 @@ class WebRTCPeer:
|
|
|
1003
1044
|
if info.name in paused
|
|
1004
1045
|
else rw.TransceiverDirection.SendOnly
|
|
1005
1046
|
)
|
|
1047
|
+
if info.kind is TrackKind.VIDEO:
|
|
1048
|
+
await _apply_sender_bitrate(transceiver, self._config)
|
|
1006
1049
|
|
|
1007
1050
|
# =========================================================================
|
|
1008
1051
|
# Seam: stats and teardown
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/signaling.py
RENAMED
|
@@ -46,7 +46,8 @@ class IceCandidate:
|
|
|
46
46
|
"""One trickle-ICE candidate from the client.
|
|
47
47
|
|
|
48
48
|
Attributes:
|
|
49
|
-
candidate: The candidate string (SDP ``a=candidate`` form)
|
|
49
|
+
candidate: The candidate string (SDP ``a=candidate`` form); an empty
|
|
50
|
+
string is the end-of-candidates marker (RFC 8838).
|
|
50
51
|
sdp_mid: The media-stream identifier the candidate belongs to, or
|
|
51
52
|
``None`` when addressed by m-line index instead.
|
|
52
53
|
sdp_mline_index: The index of the m-line the candidate belongs to, or
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/events/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/events/decorators.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/events/errors.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/events/messages.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/internal/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/internal/bridge.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/model/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/model/contract.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/model/schema.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/pipeline/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/pipeline/idle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/tracks/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/tracks/descriptors.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/tracks/input.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/interface/tracks/output.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/recording/chunk_encoder.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/runner/connection_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/__init__.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/acceptor.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/connection.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/frames.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/pacer.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/router.py
RENAMED
|
File without changes
|
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/stats.py
RENAMED
|
File without changes
|
{reactor_runtime-3.2.4 → reactor_runtime-3.2.6}/src/reactor_runtime/transport/webrtc/version.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|