agentsociety2.storage.replay_proxy 源代码
"""Serializable replay proxy + per-process sink builder.
Replay writes used to funnel through a single Ray actor wrapping SQLite. That
central write path is gone.
:class:`ReplayProxy` is now a serializable config dataclass carrying only the
output directory (+ an enable flag). Each consumer — the env router actor, the
society, each agent Ray task — builds its own process-local
:class:`~agentsociety2.storage.replay_sink.ReplaySink` from ``replay_dir`` and
appends directly. No central actor, no Ray round-trip on the write path, no
deadlock. ``enabled=False`` short-circuits to a no-op (the escape hatch for
extreme scale, where replay can be skipped entirely).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from agentsociety2.storage.replay_sink import ReplaySink, build_replay_sink
[文档]
@dataclass
class ReplayProxy:
"""Serializable config for distributed replay writing.
Carries only the replay output directory and an enable flag — no Ray actor
handle. Methods lazily build+cache a process-local
:class:`ReplaySink` (from ``replay_dir``) on first use and delegate, so each
agent Ray task / env actor that receives a deserialized copy gets its own
sink and its own file descriptors. ``enabled=False`` makes every method a
no-op.
"""
replay_dir: str | None = None
enabled: bool = True
# Process-local sink, built lazily on first write. NOT serialized across
# Ray task boundaries — ReplaySink holds open fds + a threading.Lock (both
# unpicklable) and is per-process. Each Ray task that receives a
# deserialized copy rebuilds its own sink from replay_dir.
_sink: Any = None
def _get_sink(self) -> ReplaySink | None:
sink = self._sink
if sink is None:
sink = build_replay_sink(self)
self._sink = sink
return sink
# --- pickle/cloudpickle: drop the process-local sink so the proxy can
# cross Ray task boundaries. Each side rebuilds its own sink. ---
def __getstate__(self) -> dict[str, Any]:
return {"replay_dir": self.replay_dir, "enabled": self.enabled}
def __setstate__(self, state: dict[str, Any]) -> None:
self.replay_dir = state.get("replay_dir")
self.enabled = state.get("enabled", True)
self._sink = None
[文档]
async def write(self, table: str, data: dict[str, Any]) -> None:
if not self.enabled:
return
sink = self._get_sink()
if sink is None:
return
await sink.write(table, data)
[文档]
async def write_batch(self, table: str, rows: list[dict[str, Any]]) -> None:
if not self.enabled:
return
sink = self._get_sink()
if sink is None:
return
await sink.write_batch(table, rows)
[文档]
async def register_table(self, schema: Any) -> None:
if not self.enabled:
return
sink = self._get_sink()
if sink is None:
return
await sink.register_table(schema)
[文档]
async def register_dataset(self, spec: Any, columns: Any) -> None:
if not self.enabled:
return
sink = self._get_sink()
if sink is None:
return
await sink.register_dataset(spec, columns)
[文档]
async def close(self) -> None:
sink = self._sink
if sink is not None:
await sink.close()
self._sink = None
__all__ = ["ReplayProxy", "build_replay_sink"]