Skip to content

Commit 17bbe44

Browse files
committed
chore: implemented context managers
1 parent 1967dad commit 17bbe44

3 files changed

Lines changed: 290 additions & 0 deletions

File tree

laygo/context/parallel.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""
2+
A context manager for parallel and distributed processing using
3+
multiprocessing.Manager to share state across processes.
4+
"""
5+
6+
from collections.abc import Iterator
7+
import multiprocessing as mp
8+
from multiprocessing.managers import BaseManager
9+
from multiprocessing.managers import DictProxy
10+
from multiprocessing.synchronize import Lock
11+
from typing import Any
12+
13+
from laygo.context.types import IContextHandle
14+
from laygo.context.types import IContextManager
15+
16+
17+
class _ParallelStateManager(BaseManager):
18+
"""A custom manager to expose a shared dictionary and lock."""
19+
20+
pass
21+
22+
23+
class ParallelContextHandle(IContextHandle):
24+
"""
25+
A lightweight, picklable "blueprint" for recreating a connection to the
26+
shared context in a different process.
27+
"""
28+
29+
def __init__(self, address: tuple[str, int], manager_class: type["ParallelContextManager"]):
30+
self.address = address
31+
self.manager_class = manager_class
32+
33+
def create_proxy(self) -> "IContextManager":
34+
"""
35+
Creates a new instance of the ParallelContextManager in "proxy" mode
36+
by initializing it with this handle.
37+
"""
38+
return self.manager_class(handle=self)
39+
40+
41+
class ParallelContextManager(IContextManager):
42+
"""
43+
A context manager that uses a background multiprocessing.Manager to enable
44+
state sharing across different processes.
45+
46+
This single class operates in two modes:
47+
1. Server Mode (when created normally): It starts and manages the background
48+
server process that holds the shared state.
49+
2. Proxy Mode (when created with a handle): It acts as a client, connecting
50+
to an existing server process to manipulate the shared state.
51+
"""
52+
53+
def __init__(self, initial_context: dict[str, Any] | None = None, handle: ParallelContextHandle | None = None):
54+
"""
55+
Initializes the manager. If a handle is provided, it initializes in
56+
proxy mode; otherwise, it starts a new server.
57+
"""
58+
if handle:
59+
# --- PROXY MODE INITIALIZATION ---
60+
# This instance is a client connecting to an existing server.
61+
self._is_proxy = True
62+
self._manager_server = None # Proxies do not own the server process.
63+
64+
manager = _ParallelStateManager(address=handle.address)
65+
manager.connect()
66+
self._manager = manager
67+
68+
else:
69+
# --- SERVER MODE INITIALIZATION ---
70+
# This is the main instance that owns the server process.
71+
self._is_proxy = False
72+
manager = mp.Manager() # type: ignore
73+
_ParallelStateManager.register("get_dict", callable=lambda: manager.dict(initial_context or {}))
74+
_ParallelStateManager.register("get_lock", callable=lambda: manager.Lock())
75+
76+
self._manager_server = _ParallelStateManager(address=("", 0))
77+
self._manager_server.start()
78+
self._manager = self._manager_server
79+
80+
# Common setup for both modes
81+
self._shared_dict: DictProxy = self._manager.get_dict() # type: ignore
82+
self._lock: Lock = self._manager.get_lock() # type: ignore
83+
84+
def get_handle(self) -> ParallelContextHandle:
85+
"""
86+
Returns a picklable handle for reconstruction in a worker.
87+
Only the main server instance can generate handles.
88+
"""
89+
if self._is_proxy or not self._manager_server:
90+
raise TypeError("Cannot get a handle from a proxy context instance.")
91+
92+
return ParallelContextHandle(
93+
address=self._manager_server.address, # type: ignore
94+
manager_class=self.__class__, # Pass its own class for reconstruction
95+
)
96+
97+
def shutdown(self) -> None:
98+
"""
99+
Shuts down the background manager process.
100+
This is a no-op for proxy instances, as only the main instance
101+
should control the server's lifecycle.
102+
"""
103+
if not self._is_proxy and self._manager_server:
104+
self._manager_server.shutdown()
105+
106+
def __enter__(self) -> "ParallelContextManager":
107+
"""Acquires the lock for use in a 'with' statement."""
108+
self._lock.acquire()
109+
return self
110+
111+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
112+
"""Releases the lock."""
113+
self._lock.release()
114+
115+
def __getitem__(self, key: str) -> Any:
116+
with self._lock:
117+
return self._shared_dict[key]
118+
119+
def __setitem__(self, key: str, value: Any) -> None:
120+
with self._lock:
121+
self._shared_dict[key] = value
122+
123+
def __delitem__(self, key: str) -> None:
124+
with self._lock:
125+
del self._shared_dict[key]
126+
127+
def __iter__(self) -> Iterator[str]:
128+
with self._lock:
129+
return iter(list(self._shared_dict.keys()))
130+
131+
def __len__(self) -> int:
132+
with self._lock:
133+
return len(self._shared_dict)

laygo/context/simple.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""
2+
A simple, dictionary-based context manager for single-process pipelines.
3+
"""
4+
5+
from collections.abc import Iterator
6+
from typing import Any
7+
8+
from laygo.context.types import IContextHandle
9+
from laygo.context.types import IContextManager
10+
11+
12+
class SimpleContextHandle(IContextHandle):
13+
"""
14+
A handle for the SimpleContextManager that provides a reference back to the
15+
original manager instance.
16+
17+
In a single-process environment, the "proxy" is the manager itself, ensuring
18+
all transformers in a chain share the exact same context dictionary.
19+
"""
20+
21+
def __init__(self, manager_instance: "IContextManager"):
22+
self._manager_instance = manager_instance
23+
24+
def create_proxy(self) -> "IContextManager":
25+
"""
26+
Returns the original SimpleContextManager instance.
27+
28+
This ensures that in a non-distributed pipeline, all chained transformers
29+
operate on the same shared dictionary.
30+
"""
31+
return self._manager_instance
32+
33+
34+
class SimpleContextManager(IContextManager):
35+
"""
36+
A basic context manager that uses a standard Python dictionary for state.
37+
38+
This manager is suitable for single-threaded, single-process pipelines where
39+
no state needs to be shared across process boundaries. It is the default
40+
context manager for a Laygo pipeline.
41+
"""
42+
43+
def __init__(self, initial_context: dict[str, Any] | None = None) -> None:
44+
"""
45+
Initializes the context manager with an optional dictionary.
46+
47+
Args:
48+
initial_context: An optional dictionary to populate the context with.
49+
"""
50+
self._context = dict(initial_context or {})
51+
52+
def get_handle(self) -> IContextHandle:
53+
"""
54+
Returns a handle that holds a reference back to this same instance.
55+
"""
56+
return SimpleContextHandle(self)
57+
58+
def __enter__(self) -> "SimpleContextManager":
59+
"""
60+
Provides 'with' statement compatibility. No lock is needed for this
61+
simple, single-threaded context manager.
62+
"""
63+
return self
64+
65+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
66+
"""
67+
Provides 'with' statement compatibility. No lock is needed for this
68+
simple, single-threaded context manager.
69+
"""
70+
pass
71+
72+
def __getitem__(self, key: str) -> Any:
73+
return self._context[key]
74+
75+
def __setitem__(self, key: str, value: Any) -> None:
76+
self._context[key] = value
77+
78+
def __delitem__(self, key: str) -> None:
79+
del self._context[key]
80+
81+
def __iter__(self) -> Iterator[str]:
82+
return iter(self._context)
83+
84+
def __len__(self) -> int:
85+
return len(self._context)
86+
87+
def shutdown(self) -> None:
88+
"""No-op for the simple context manager."""
89+
pass

laygo/context/types.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
Defines the abstract base classes for context management in Laygo.
3+
4+
This module provides the core interfaces (IContextHandle and IContextManager)
5+
that all context managers must implement, ensuring a consistent API for
6+
state management across different execution environments (simple, threaded, parallel).
7+
"""
8+
9+
from abc import ABC
10+
from abc import abstractmethod
11+
from collections.abc import MutableMapping
12+
from typing import Any
13+
14+
15+
class IContextHandle(ABC):
16+
"""
17+
An abstract base class for a picklable handle to a context manager.
18+
19+
A handle contains the necessary information for a worker process to
20+
reconstruct a connection (a proxy) to the shared context.
21+
"""
22+
23+
@abstractmethod
24+
def create_proxy(self) -> "IContextManager":
25+
"""
26+
Creates the appropriate context proxy instance from the handle's data.
27+
28+
This method is called within a worker process to establish its own
29+
connection to the shared state.
30+
31+
Returns:
32+
An instance of an IContextManager proxy.
33+
"""
34+
raise NotImplementedError
35+
36+
37+
class IContextManager(MutableMapping[str, Any], ABC):
38+
"""
39+
Abstract base class for managing shared state (context) in a pipeline.
40+
41+
This class defines the contract for all context managers, ensuring they
42+
provide a dictionary-like interface for state manipulation by inheriting
43+
from `collections.abc.MutableMapping`. It also includes methods for
44+
distribution (get_handle) and resource management (shutdown).
45+
"""
46+
47+
@abstractmethod
48+
def get_handle(self) -> IContextHandle:
49+
"""
50+
Returns a picklable handle for connecting from a worker process.
51+
52+
This handle is serialized and sent to distributed workers, which then
53+
use it to create a proxy to the shared context.
54+
55+
Returns:
56+
A picklable IContextHandle instance.
57+
"""
58+
raise NotImplementedError
59+
60+
@abstractmethod
61+
def shutdown(self) -> None:
62+
"""
63+
Performs final synchronization and cleans up any resources.
64+
65+
This method is responsible for releasing connections, shutting down
66+
background processes, or any other cleanup required by the manager.
67+
"""
68+
raise NotImplementedError

0 commit comments

Comments
 (0)