Skip to content

Commit 364017e

Browse files
committed
feat: sniff handshake and publish Kafka events in livepeer proxy
Intercept the WebSocket handshake in _proxy_ws to extract manifest_id and user_id. Publish websocket_connected (after handshake) and websocket_disconnected (on close) events with connection_info, matching fal_app.py event parity. Signed-off-by: emranemran <emran@livepeer.org> Signed-off-by: emranemran <emran.mah@gmail.com>
1 parent 8b795db commit 364017e

1 file changed

Lines changed: 104 additions & 3 deletions

File tree

src/scope/cloud/livepeer_fal_app.py

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,17 +271,58 @@ def _build_runner_command() -> list[str]:
271271
]
272272

273273

274-
async def _proxy_ws(client_ws: WebSocket) -> None:
274+
async def _proxy_ws(client_ws: WebSocket, metadata: dict | None = None) -> None:
275275
"""Connect to the local runner and proxy traffic bidirectionally.
276276
277+
If *metadata* is provided, the initial handshake messages are sniffed
278+
(best-effort) to extract ``manifest_id`` and ``user_id`` before the
279+
bidirectional proxy loop begins.
280+
277281
Raises WebSocketDisconnect if the client disconnects.
278282
Returns normally if the runner connection drops.
279283
"""
284+
import json
285+
280286
import websockets
281287
from websockets.exceptions import ConnectionClosed
282288

283289
async with websockets.connect(RUNNER_LOCAL_WS_URL) as runner_ws:
284-
290+
# --- Sniff the two-message handshake (best-effort) ---
291+
# 1. Runner → Client: {"type": "ready", "connection_id": "..."}
292+
ready_msg = await runner_ws.recv()
293+
if isinstance(ready_msg, bytes):
294+
await client_ws.send_bytes(ready_msg)
295+
else:
296+
if metadata is not None:
297+
try:
298+
parsed = json.loads(ready_msg)
299+
metadata["connection_id"] = parsed.get("connection_id")
300+
except (json.JSONDecodeError, AttributeError):
301+
pass
302+
await client_ws.send_text(ready_msg)
303+
304+
# 2. Client → Runner: job_info with manifest_id, params.daydream_user_id
305+
job_msg = await client_ws.receive()
306+
job_msg_type = job_msg.get("type")
307+
if job_msg_type == "websocket.receive":
308+
text_data = job_msg.get("text")
309+
bytes_data = job_msg.get("bytes")
310+
if text_data is not None:
311+
if metadata is not None:
312+
try:
313+
parsed = json.loads(text_data)
314+
metadata["manifest_id"] = parsed.get("manifest_id")
315+
params = parsed.get("params") or {}
316+
metadata["user_id"] = params.get("daydream_user_id")
317+
except (json.JSONDecodeError, AttributeError):
318+
pass
319+
await runner_ws.send(text_data)
320+
elif bytes_data is not None:
321+
await runner_ws.send(bytes_data)
322+
elif job_msg_type == "websocket.disconnect":
323+
raise WebSocketDisconnect()
324+
325+
# --- Bidirectional proxy loop ---
285326
async def client_to_runner() -> None:
286327
while True:
287328
message = await client_ws.receive()
@@ -437,6 +478,33 @@ async def websocket_handler(self, client_ws: WebSocket) -> None:
437478

438479
await client_ws.accept()
439480

481+
# Initialize Kafka publisher (lazy, once per process)
482+
global kafka_publisher
483+
if kafka_publisher is None:
484+
kafka_publisher = KafkaPublisher()
485+
await kafka_publisher.start()
486+
487+
connection_start_time = time.time()
488+
metadata: dict = {}
489+
connected_event_published = False
490+
491+
import json
492+
493+
fal_log_labels_raw = os.getenv("FAL_LOG_LABELS", "unknown")
494+
try:
495+
fal_log_labels = json.loads(fal_log_labels_raw)
496+
except (json.JSONDecodeError, TypeError):
497+
fal_log_labels = fal_log_labels_raw
498+
499+
connection_info = {
500+
"gpu_type": LivepeerScopeApp.machine_type,
501+
"fal_region": os.getenv("NOMAD_DC", "unknown"),
502+
"fal_runner_id": os.getenv(
503+
"FAL_JOB_ID", os.getenv("FAL_RUNNER_ID", "unknown")
504+
),
505+
"fal_log_labels": fal_log_labels,
506+
}
507+
440508
# Ensure any previous session data is cleaned up
441509
event = _get_cleanup_event()
442510
await event.wait()
@@ -448,7 +516,7 @@ async def websocket_handler(self, client_ws: WebSocket) -> None:
448516
while True:
449517
print(f"Connecting proxy to runner websocket at {RUNNER_LOCAL_WS_URL}")
450518
try:
451-
await _proxy_ws(client_ws)
519+
await _proxy_ws(client_ws, metadata=metadata)
452520
except (
453521
ConnectionClosed,
454522
InvalidStatus,
@@ -457,6 +525,23 @@ async def websocket_handler(self, client_ws: WebSocket) -> None:
457525
) as exc:
458526
print(f"Livepeer fal ws runner connection failed: {exc}")
459527

528+
# Publish websocket_connected once after handshake metadata is available
529+
if (
530+
not connected_event_published
531+
and metadata.get("manifest_id")
532+
and kafka_publisher
533+
and kafka_publisher.is_running
534+
):
535+
await kafka_publisher.publish(
536+
"websocket_connected",
537+
{
538+
"user_id": metadata.get("user_id"),
539+
"connection_id": metadata.get("manifest_id"),
540+
"connection_info": connection_info,
541+
},
542+
)
543+
connected_event_published = True
544+
460545
now = time.monotonic()
461546
cutoff = now - RUNNER_FAILURE_WINDOW_SECONDS
462547
failure_timestamps.append(now)
@@ -478,6 +563,22 @@ async def websocket_handler(self, client_ws: WebSocket) -> None:
478563
except Exception as exc:
479564
print(f"Livepeer fal ws proxy error: {type(exc).__name__}: {exc}")
480565
finally:
566+
# Publish websocket_disconnected event
567+
if kafka_publisher and kafka_publisher.is_running:
568+
end_time = time.time()
569+
elapsed_ms = int((end_time - connection_start_time) * 1000)
570+
await kafka_publisher.publish(
571+
"websocket_disconnected",
572+
{
573+
"user_id": metadata.get("user_id"),
574+
"connection_id": metadata.get("manifest_id"),
575+
"connection_info": connection_info,
576+
"duration_ms": elapsed_ms,
577+
"session_start_time_ms": int(connection_start_time * 1000),
578+
"session_end_time_ms": int(end_time * 1000),
579+
},
580+
)
581+
481582
await run_cleanup()
482583
with suppress(Exception):
483584
await client_ws.close()

0 commit comments

Comments
 (0)