-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathchannel_reader.py
More file actions
193 lines (161 loc) · 7.25 KB
/
Copy pathchannel_reader.py
File metadata and controls
193 lines (161 loc) · 7.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
from __future__ import annotations
import json
import logging
from typing import Any, AsyncIterator
import aiohttp
from .errors import LivepeerGatewayError
from .trickle_subscriber import TrickleSubscriber
_LOG = logging.getLogger(__name__)
class ChannelReader:
def __init__(self, events_url: str) -> None:
self.events_url = events_url
def __call__(
self,
*,
start_seq: int = -2,
max_retries: int = 5,
max_event_bytes: int = 1_048_576,
) -> AsyncIterator[dict[str, Any]]:
"""
Subscribe to the trickle events channel.
Each yielded item is a decoded JSON object (dict). The underlying network
subscription starts lazily on first iteration.
max_event_bytes applies per segment (per JSON message), not across
the entire stream.
"""
url = self.events_url
async def _read_all(segment: "SegmentReader", *, chunk_size: int = 33 * 1024) -> bytes:
parts = []
try:
reader = segment.make_reader()
while True:
chunk = await reader.read(chunk_size=chunk_size)
if not chunk:
break
parts.append(chunk)
finally:
await segment.close()
return b"".join(parts)
async def _iter() -> AsyncIterator[dict[str, Any]]:
if max_event_bytes < 1:
raise ValueError("max_event_bytes must be >= 1")
try:
async with TrickleSubscriber(
url,
start_seq=start_seq,
max_retries=max_retries,
max_bytes=max_event_bytes,
) as subscriber:
while (segment := await subscriber.next()) is not None:
payload = await _read_all(segment)
if not payload:
raise LivepeerGatewayError("Trickle event segment was empty")
try:
data = json.loads(payload.decode("utf-8"))
except Exception as e:
snippet = payload[:256].decode("utf-8", errors="replace")
raise LivepeerGatewayError(
f"Trickle event JSON decode failed: {e} (payload={snippet!r})"
) from e
if not isinstance(data, dict):
raise LivepeerGatewayError(
f"Trickle event must be JSON, got {type(data).__name__}"
)
yield data
except LivepeerGatewayError:
raise
except aiohttp.ClientPayloadError as e:
# Orchestrator truncated the transfer mid-stream (e.g. TransferEncodingError 400)
# or went unreachable. Treat as a clean network disconnect — stop iterating
# rather than propagating as an application error.
_LOG.warning("Trickle events channel disconnected (network): %s: %s", e.__class__.__name__, e)
return
except Exception as e:
raise LivepeerGatewayError(
f"Trickle events subscription error: {e.__class__.__name__}: {e}"
) from e
return _iter()
class JSONLReader:
def __init__(self, events_url: str) -> None:
self.events_url = events_url
def __call__(
self,
*,
start_seq: int = -2,
max_retries: int = 5,
max_event_bytes: int = 1_048_576,
) -> AsyncIterator[dict[str, Any]]:
"""
Subscribe to a trickle channel containing newline-delimited JSON (JSONL).
Events are yielded incrementally as newline-terminated lines arrive, without
buffering the entire segment in memory first. max_event_bytes applies per
segment, not across the entire stream.
"""
url = self.events_url
def _decode_line(line: bytearray) -> dict[str, Any]:
try:
data = json.loads(line)
except Exception as e:
snippet = bytes(line[:256]).decode("utf-8", errors="replace")
raise LivepeerGatewayError(
f"Trickle event JSONL decode failed: {e} (line={snippet!r})"
) from e
if not isinstance(data, dict):
raise LivepeerGatewayError(
f"Trickle event must be JSON object, got {type(data).__name__}"
)
return data
async def _iter() -> AsyncIterator[dict[str, Any]]:
if max_event_bytes < 1:
raise ValueError("max_event_bytes must be >= 1")
try:
async with TrickleSubscriber(
url,
start_seq=start_seq,
max_retries=max_retries,
max_bytes=max_event_bytes,
) as subscriber:
while (segment := await subscriber.next()) is not None:
reader = segment.make_reader()
buf = bytearray()
start = 0
try:
while True:
chunk = await reader.read(chunk_size=33 * 1024)
if not chunk:
break
buf.extend(chunk)
while True:
nl = buf.find(b"\n", start)
if nl < 0:
break
line = buf[start:nl]
start = nl + 1
if not line:
continue
yield _decode_line(line)
if start == len(buf):
buf.clear()
start = 0
elif start > 64 * 1024 and start > len(buf) // 2:
del buf[:start]
start = 0
tail = bytes(buf[start:]).strip()
if tail:
data = _decode_line(bytearray(tail))
yield data
finally:
await segment.close()
except LivepeerGatewayError:
raise
except aiohttp.ClientPayloadError as e:
# Orchestrator truncated the transfer mid-stream (e.g. TransferEncodingError 400)
# or went unreachable. Treat as a clean network disconnect — stop iterating
# rather than propagating as an application error.
_LOG.warning("Trickle JSONL channel disconnected (network): %s: %s", e.__class__.__name__, e)
return
except Exception as e:
raise LivepeerGatewayError(
f"Trickle JSONL subscription error: {e.__class__.__name__}: {e}"
) from e
return _iter()