callserver 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,315 @@
1
+ # CallServer Usage Guide
2
+
3
+ `CallServer` runs Python functions in another process on the same machine. Numpy arrays above a size threshold in arguments and return values are transferred through shared memory, and all other data is sent through a pipe (child process) or socket (standalone server). CallServer can spawn a child server process or connect to an existing server process. It cannot be used for distributed execution across machines.
4
+
5
+ For runnable scripts, see the [examples](../examples) folder.
6
+
7
+ - [Key Features](#key-features)
8
+ - [Limitations](#limitations)
9
+ - [Usage](#usage)
10
+ - [Standalone Server](#standalone-server)
11
+ - [Notes](#notes)
12
+ - [How It Works](#how-it-works)
13
+ - [API Reference](#api-reference)
14
+
15
+ ## Key Features
16
+
17
+ - **Zero-serialization transfer**: Large numpy arrays are automatically promoted to shared memory instead of being pickled.
18
+ - **Zero-copy transfer**: Arrays can be pre-allocated in shared memory, requiring no copy and no serialization.
19
+ - **Transparent buffer management**: Arrays are promoted automatically when passed directly as positional or keyword arguments, or returned directly or inside a tuple, list or dictionary. Shared memory is allocated on the first call and reused on subsequent calls.
20
+ - **Flexible call patterns**: Synchronous and asynchronous calls.
21
+ - **Double-buffering**: Front/back buffer pattern for overlapping server-side computation with client-side processing.
22
+ - **Connection modes**: Spawn a child process, or connect to a standalone server via TCP.
23
+ - **Server-side state**: Context objects can be passed to the server or created on the server.
24
+
25
+ ## Limitations
26
+
27
+ - **Local machine only**: Shared memory cannot be used across machines.
28
+ - **Single async call**: Only one asynchronous call can be in progress at a time per `CallServer` instance.
29
+ - **Simple data structures**: Arrays nested inside argument containers are not promoted. Return values are promoted one level deep (tuple, list or dict).
30
+ - **Fixed shapes**: The shape and dtype of an array argument must not change between calls to the same function.
31
+ - **Picklable functions**: Server-side functions and context initializers are pickled by reference, so they must be defined at module level and importable by the server process.
32
+
33
+ ## Usage
34
+
35
+ > On Windows and macOS, child processes re-import the main module, so client scripts need an `if __name__ == "__main__":` guard. It is omitted from the snippets below for brevity.
36
+
37
+ ### 1. A simple example
38
+ ```python
39
+ import numpy as np
40
+ from callserver import CallServer
41
+
42
+ server = CallServer('adder')
43
+ # start as child process
44
+ server.start()
45
+ # addition executes in the child process.
46
+ # the arguments and return value are exchanged through shared memory
47
+ result = server.call(np.add, np.ones((1024, 1024)), np.ones((1024, 1024)))
48
+ print(result[0, 0]) # 2.0
49
+ server.stop()
50
+ ```
51
+
52
+ `CallServer` is also a context manager, which calls `start()` on entry and `stop()` on exit:
53
+ ```python
54
+ with CallServer('adder') as server:
55
+ result = server.call(np.add, a, b)
56
+ ```
57
+
58
+ ### 2. Server-Side State
59
+ Server-side state can be passed in via the CallServer constructor, or can be created by a server-side initializer.
60
+ If `context` is callable, it is called once inside the server process with the remaining constructor arguments, and its return value becomes the context. Otherwise, the object itself is pickled and sent to the server.
61
+ ```python
62
+ import numpy as np
63
+ from callserver import CallServer
64
+
65
+ # The initializer can be a constructor or a factory function.
66
+ # It executes inside the server process.
67
+ def my_initializer(shape, value):
68
+ # Create and return a state object
69
+ return np.full(shape, value)
70
+
71
+ # arguments after the initializer are passed to it on the server
72
+ server = CallServer('my_server', my_initializer, (1024, 1024), 1000)
73
+ server.start()
74
+ ```
75
+
76
+ Errors raised by the initializer are re-raised on the client by the next `call()` or `get_result()`.
77
+
78
+ ### 3. Server-Side Functions
79
+ Server-side functions receive the context object as their first argument. If no context was given, functions are called with the call arguments only.
80
+ ```python
81
+ # this executes inside the server process
82
+ def increment(state, value):
83
+ return state + value
84
+
85
+ result = server.call(increment, 10) # returns [[1010, 1010, ...
86
+ ```
87
+
88
+ A more realistic example captures and processes camera frames on the server, and returns them through shared memory:
89
+ ```python
90
+ import cv2
91
+ from callserver import CallServer
92
+
93
+ # called on the server side once, at startup
94
+ def create_context(device_id):
95
+ return cv2.VideoCapture(device_id)
96
+
97
+ # executed on the server side on every call
98
+ def read_and_process_frame(cap):
99
+ ret, frame = cap.read()
100
+ # process frame as needed
101
+ # ...
102
+ return frame
103
+
104
+ server = CallServer('my_camera', create_context, 0)
105
+ server.start()
106
+
107
+ while True:
108
+ frame = server.call(read_and_process_frame) # this is the RPC call
109
+ cv2.imshow('frame', frame)
110
+ if cv2.waitKey(1) & 0xFF == ord('q'):
111
+ break
112
+
113
+ server.stop()
114
+ ```
115
+
116
+ ### 4. Using Async Calls
117
+ Remote calls can execute asynchronously, allowing the client process to perform other work in parallel. Only one call can be in progress at a time: `call_async()` raises `RuntimeError` if the previous result has not been retrieved with `get_result()`.
118
+ ```python
119
+ # start processing
120
+ server.call_async(read_and_process_frame)
121
+
122
+ # do local work while the server processes
123
+ do_local_computation()
124
+
125
+ # get the result (blocks until ready; raises TimeoutError if the timeout expires)
126
+ frame = server.get_result(timeout=1.0)
127
+ ```
128
+
129
+ Use `has_result()` to poll without blocking, `wait_for_result(timeout)` to wait without retrieving, and `call_in_progress()` to check whether a call is outstanding.
130
+
131
+ ### 5. Double-Buffering
132
+ Arrays returned by a call are views of shared memory, and that memory is reused by the next call to the same function. With a single allocation set, an async call therefore overwrites the previous result while the client may still be using it.
133
+
134
+ To avoid this, create two allocation sets for the function. `CallServer` rotates between them on every call, so the result of the previous call stays valid while the next call writes into the other set.
135
+ ```python
136
+ # set up double-buffering
137
+ server.add_allocation_set(read_and_process_frame)
138
+ server.add_allocation_set(read_and_process_frame)
139
+
140
+ # prime the pipeline
141
+ server.call_async(read_and_process_frame)
142
+
143
+ while True:
144
+ # get the previous result
145
+ frame = server.get_result()
146
+ # start next frame processing on the server
147
+ server.call_async(read_and_process_frame)
148
+ # use the previous frame buffer locally
149
+ do_local_computation(frame)
150
+ cv2.imshow('frame', frame)
151
+ if cv2.waitKey(1) & 0xFF == ord('q'):
152
+ break
153
+ ```
154
+
155
+ See [examples/async_calls.py](../examples/async_calls.py).
156
+
157
+ ### 6. Pre-Allocation of shared memory
158
+ Buffers allocated with `CallServer.alloc()` live in shared memory, so passing or returning them involves no copy at all.
159
+ ```python
160
+ buffer = CallServer.alloc(server, increment, (1024, 1024), float)
161
+ result = server.call(increment, 10, buffer)
162
+
163
+ # On server side, the buffer arrives as a shared memory numpy array
164
+ def increment(context, value, buffer):
165
+ buffer[:] = context # copy, no allocation
166
+ buffer += value # in-place addition
167
+ return buffer # buffer is already shared memory, so no allocation on return
168
+ ```
169
+
170
+ Buffers can be allocated on the server side as well. The server name argument must match the name provided to the CallServer on the client.
171
+ ```python
172
+ def my_initializer(shape, value):
173
+ buffer = CallServer.alloc('my_server', increment, shape, float)
174
+ buffer[:] = value
175
+ return buffer
176
+ ```
177
+
178
+ See [examples/preallocated_buffers.py](../examples/preallocated_buffers.py).
179
+
180
+ When pre-allocating buffers, follow these rules:
181
+ 1. If the server has background threads pre-processing data directly in shared-memory buffers, you need to allocate an extra buffer (front- and back- buffers) and manage switching between them.
182
+ 2. If the server-side function is only called synchronously, use a single allocation set.
183
+ 3. If the server-side function is called async, use two allocation sets, with the same set of buffers in each (pass `alloc_set_id=0` and `alloc_set_id=1` to `CallServer.alloc()`). The CallServer will handle switching between the allocation sets between calls.
184
+ 4. Mixing the back-buffer pattern and the double allocation-set pattern is not supported.
185
+
186
+ ## Standalone Server
187
+
188
+ A server can also run as a separate, long-lived process. The port is required:
189
+
190
+ ```bash
191
+ callserver --port 5000
192
+ # or
193
+ python -m callserver --port 5000 [--address 127.0.0.1] [--authkey callserver]
194
+ ```
195
+
196
+ or from Python:
197
+
198
+ ```python
199
+ from callserver import run, DEFAULT_AUTHKEY
200
+ run(("127.0.0.1", 5000, DEFAULT_AUTHKEY)) # blocks forever
201
+ ```
202
+
203
+ Clients connect by passing a `(name, endpoint)` tuple, where `endpoint` is a port or an `(address, port)` tuple. The `authkey` must match the server's `--authkey`.
204
+
205
+ ```python
206
+ with CallServer(('my_server', ('127.0.0.1', 5000)), authkey=b'callserver') as server:
207
+ result = server.call(np.add, a, b)
208
+ ```
209
+
210
+ - The server handles one client at a time. When a client disconnects, the server waits for the next one.
211
+ - If the next client uses the same name, context and context arguments, the existing server-side context is reused. Otherwise, it is re-created.
212
+ - Functions and context initializers sent to a standalone server must be importable by the server process, e.g. from installed packages, or from modules in the directory the server was started from with `python -m callserver`.
213
+
214
+ See [examples/standalone_client.py](../examples/standalone_client.py).
215
+
216
+ ## Notes
217
+
218
+ - Always call `get_result()` after an asynchronous call before starting a new one.
219
+ - Returned arrays are views of shared memory that the next call to the same function reuses. Copy them (`result.copy()`) if you need to keep them beyond the next call, or use double-buffering.
220
+ - Arrays of 1024 bytes or less (`AllocationSet.MIN_SIZE`) are pickled and sent through the pipe or socket rather than promoted to shared memory.
221
+ - In child process mode, `stop()` waits for the server process to exit and releases all shared memory. In standalone mode, `stop()` only disconnects; the server keeps running.
222
+
223
+ ## How It Works
224
+
225
+ ```
226
+ ┌─────────────────┐ ┌─────────────────┐
227
+ │ Client process │ │ Server process │
228
+ │ │ │ │
229
+ │ CallServer │ ── Pipe / TCP ─▶ │ run() loop │
230
+ │ │ ◀─ results ───── │ │
231
+ │ Allocation- │ │ Allocation- │
232
+ │ SetManager │ │ SetManager │
233
+ │ │ shared memory │ │
234
+ │ ┌───────────┐ │ │ ┌───────────┐ │
235
+ │ │ Buffer A │◀─┼──────────────────┼─▶│ Buffer A │ │
236
+ │ └───────────┘ │ │ └───────────┘ │
237
+ │ ┌───────────┐ │ │ ┌───────────┐ │
238
+ │ │ Buffer B │◀─┼──────────────────┼─▶│ Buffer B │ │
239
+ │ └───────────┘ │ │ └───────────┘ │
240
+ └─────────────────┘ └─────────────────┘
241
+ ```
242
+
243
+ 1. On each call, the client replaces large array arguments with `AllocationDescriptor`s (shared memory name, shape, dtype), copying the data into shared memory unless the array already lives there.
244
+ 2. The function reference, descriptors and remaining arguments are pickled and sent through the pipe or socket.
245
+ 3. The server maps the descriptors back to numpy arrays, calls the function, and replaces large arrays in the return value with descriptors.
246
+ 4. The client maps the returned descriptors to numpy arrays.
247
+
248
+ Each process has one `AllocationSetManager` per server name, holding one or more `AllocationSet`s per function. Each `AllocationSet` tracks two kinds of shared memory buffers:
249
+
250
+ - **Linked buffers**: created by this process and owned by it (unlinked on close).
251
+ - **Mapped buffers**: created by the other process and only mapped for read/write access.
252
+
253
+ Buffers are created on the first call and reused on later calls, keyed by argument position or keyword name (`__arg0`, `__arg1`, ..., `__retval0`, ...).
254
+
255
+ ## API Reference
256
+
257
+ ### CallServer
258
+
259
+ The main client interface for making RPC calls.
260
+
261
+ **Constructor:**
262
+ ```python
263
+ CallServer(name, context=None, *args, authkey=DEFAULT_AUTHKEY, **kwargs)
264
+ ```
265
+ - `name`: Server identifier (string, spawns a child process), or `(name, endpoint)` tuple to connect to a standalone server, where `endpoint` is a port or an `(address, port)` tuple
266
+ - `context`: Server-side state object, or initializer callable that creates it on the server
267
+ - `*args`, `**kwargs`: Arguments passed to the context initializer
268
+ - `authkey`: Authentication key for connecting to a standalone server
269
+
270
+ **Methods:**
271
+ - `start()`: Start the server process or connect to a standalone server. Returns `self`.
272
+ - `call(fn, *args, **kwargs)`: Synchronous call. Returns the result.
273
+ - `call_async(fn, *args, **kwargs)`: Start an asynchronous call. Raises `RuntimeError` if a call is already in progress.
274
+ - `get_result(timeout=None)`: Retrieve the result of the async call, blocking until it is ready. Raises `TimeoutError` if the timeout expires. Returns the last result if no call is in progress.
275
+ - `wait_for_result(timeout=None)`: Wait for a result without retrieving it. Returns `True` if ready.
276
+ - `has_result()`: Check if a result is ready (non-blocking).
277
+ - `call_in_progress()`: Check if an async call is in progress.
278
+ - `add_allocation_set(fn)`: Add an allocation set for a function (call twice for double-buffering).
279
+ - `get_allocation_set(fn)`: Get the next allocation set for a function.
280
+ - `stop()`: Stop the server (child process mode) or disconnect (standalone mode), and clean up resources.
281
+
282
+ **Static Methods:**
283
+ - `CallServer.alloc(callserver, fn, shape, dtype=float, alloc_set_id=0)`: Pre-allocate a shared memory buffer for `fn`. `callserver` is a `CallServer` instance (client side) or the server name (server side). Returns a numpy array backed by shared memory.
284
+
285
+ ### Functions
286
+
287
+ - `run(endpoint_or_connection)`: Run the server loop. Pass an `(address, port, authkey)` tuple to run a standalone TCP server.
288
+ - `run_console()`: Command-line entry point used by the `callserver` command and `python -m callserver`. Options: `--port` (required), `--address` (default `127.0.0.1`), `--authkey` (default `callserver`).
289
+
290
+ ### Constants
291
+
292
+ - `DEFAULT_AUTHKEY`: Default authentication key (`b"callserver"`).
293
+
294
+ ### AllocationSet
295
+
296
+ Manages shared memory buffers for a single function.
297
+
298
+ - `MIN_SIZE`: Arrays larger than this many bytes are promoted to shared memory (default 1024).
299
+ - `alloc(name, shape, dtype, init=None)`: Allocate a new buffer. Returns an `AllocationDescriptor`.
300
+ - `get(name, init=None)`: Get the descriptor of an existing buffer.
301
+ - `from_sharedmemory(descriptor)`: Map a descriptor to a numpy array.
302
+ - `close()`: Release all shared memory resources.
303
+
304
+ ### AllocationSetManager
305
+
306
+ Manages the allocation sets of all functions for one server name. There is one instance per name in each process.
307
+
308
+ - `add_allocation_set(fn)`: Add a new set for a function.
309
+ - `get_allocation_set(fn, idx=None)`: Get the set at `idx`, creating it if needed. With `idx=None`, rotates to the next set.
310
+ - `get_active_id(fn)`: Index of the most recently used set.
311
+ - `close()`: Release all resources.
312
+
313
+ ### AllocationDescriptor
314
+
315
+ Lightweight, picklable descriptor for a shared memory buffer (`name`, `shape`, `dtype`).