You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -176,19 +176,42 @@ An `EdgeRuntime` is a direct analog for a **physical or virtual network link**.
176
176
177
177
-----
178
178
179
-
### **5.3`ServerRuntime` — The Workhorse 📦**
179
+
### **5.3`ServerRuntime` — The Workhorse 📦 (2025 edition)**
180
180
181
-
`ServerRuntime`models an application server that owns finite CPU/RAM resources and executes a chain of steps for every incoming request.
182
-
With the 2025 refactor it now uses a **dispatcher / handler** pattern: the dispatcher sits in an infinite loop, and each request is handled in its own SimPy subprocess. This enables many concurrent in-flight requests while keeping the code easy to reason about.
181
+
`ServerRuntime`emulates an application server that owns **finite CPU / RAM containers** and executes an ordered chain of **Step** objects for every incoming request.
182
+
The 2025 refactor keeps the classic **dispatcher / handler** pattern, adds **live metric counters** (ready‑queue length, I/O‑queue length, RAM‑in‑use) and implements the **lazy‑CPU lock** algorithm described earlier.
|**`env`**| The shared `simpy.Environment`. Every timeout and resource operation is scheduled here. |
187
-
|**`server_resources`**| A `ServerContainers` mapping (`{"CPU": Container, "RAM": Container}`) created by `ResourcesRuntime`. The containers are **pre-filled** (`level == capacity`) so the server can immediately pull tokens. |
188
-
|**`server_config`**| The validated Pydantic `Server` model: server-wide ID, resource spec, and a list of `Endpoint` objects (each endpoint is an ordered list of `Step`s). |
189
-
|**`out_edge`**| The `EdgeRuntime` (or stub) that receives the `RequestState` once processing finishes. |
190
-
|**`server_box`**| A `simpy.Store` acting as the server’s inbox. Up-stream actors drop `RequestState`s here. |
191
-
|**`rng`**| Instance of `numpy.random.Generator`; defaults to `default_rng()`. Used to pick a random endpoint. |
|**`env`**| Shared `simpy.Environment`. Every timeout or resource operation is scheduled here. |
187
+
|**`server_resources`**| A `ServerContainers` mapping `{"CPU": Container, "RAM": Container}` created by `ResourcesRuntime`. Containers start **full** so a server can immediately pull tokens. |
|**Record arrival**|`state.record_hop(SystemNodes.SERVER, self.server_config.id, env.now)` – leaves a breadcrumb for tracing. |
232
-
|**Endpoint selection**| Uniform random index `rng.integers(0, len(endpoints))`. (Hook point for custom routing later.) |
233
-
|**Reserve RAM (back-pressure)**| Compute `total_ram` (sum of all `StepOperation.NECESSARY_RAM`). `yield RAM.get(total_ram)`. If not enough RAM is free, the coroutine blocks, creating natural memory pressure. |
234
-
|**Execute steps in order**||
235
-
| – CPU-bound step |`yield CPU.get(1)` → `yield env.timeout(cpu_time)` → `yield CPU.put(1)` – exactly one core is busy for the duration. |
236
-
| – I/O-bound step |`yield env.timeout(io_wait)` – no core is held, modelling non-blocking I/O. |
237
-
|**Release RAM**|`yield RAM.put(total_ram)`. |
238
-
|**Forward**|`out_edge.transport(state)` – hands the request to the next hop without waiting for network latency. |
|**Forward**|`out_edge.transport(state)` — send to next hop without awaiting latency |
261
+
262
+
---
263
+
264
+
#### **CPU / I‑O loop details**
265
+
266
+
***Lazy‑CPU lock** – first CPU step acquires one core; all following contiguous CPU steps reuse it.
267
+
***Release on I/O** – on the first I/O step the core is released; it remains free until the next CPU step.
268
+
***Metric updates** – counters are modified only on the **state transition** (CPU→I/O, I/O→CPU) so there is never double‑counting.
269
+
270
+
```python
271
+
ifisinstance(step.kind, EndpointStepCPU):
272
+
ifnot core_locked:
273
+
yieldCPU.get(1)
274
+
core_locked =True
275
+
self._el_ready_queue_len +=1# entered ready queue
276
+
if is_in_io_queue:
277
+
self._el_io_queue_len -=1
278
+
is_in_io_queue =False
279
+
yield env.timeout(cpu_time)
280
+
281
+
elifisinstance(step.kind, EndpointStepIO):
282
+
if core_locked:
283
+
yieldCPU.put(1)
284
+
core_locked =False
285
+
self._el_ready_queue_len -=1
286
+
ifnot is_in_io_queue:
287
+
self._el_io_queue_len +=1
288
+
is_in_io_queue =True
289
+
yield env.timeout(io_time)
290
+
```
291
+
292
+
**Handler epilogue**
293
+
294
+
```python
295
+
# at exit, remove ourselves from whichever queue we are in
296
+
if core_locked: # we are still in ready queue
297
+
self._el_ready_queue_len -=1
298
+
yieldCPU.put(1)
299
+
elif is_in_io_queue: # finished while awaiting I/O
300
+
self._el_io_queue_len -=1
301
+
```
302
+
303
+
> This guarantees both queues always balance back to 0 after the last request completes.
239
304
240
305
---
241
306
242
307
#### **Concurrency Guarantees**
243
308
244
-
***CPU contention** – because CPUis a token bucket (`simpy.Container`) the maximum number of concurrent CPU-bound steps equals`cpu_cores`.
245
-
***RAM contention** – large requests can stall entirely until enough RAM frees up, accurately modelling out-of-memory throttling.
246
-
***Non-blocking I/O** – while a handler waits on an I/O step it releases the core, allowing other handlers to run; this mirrors an async framework where the eventloop can service other sockets.
309
+
***CPU contention** – the `CPU` container is a token bucket; max concurrent CPU‑bound steps =`cpu_cores`.
310
+
***RAM contention** – requests block at `RAM.get()` until memory is free (models cgroup / OOM throttling).
311
+
***Non‑blocking I/O** – while in `env.timeout(io_wait)` no core token is held, so other handlers can run; mirrors an async server where workers return to the event‑loop on each `await`.
***Endpoint finishes** – epilogue removes the request from whichever queue it still occupies, avoiding “ghost” entries.
324
+
325
+
---
258
326
259
-
Thus a **CPU-bound step** is a tight Python loop holding the GIL, while an **I/O-bound step** is `await cursor.execute(...)` that frees the event loop.
|`server_box`| Web server accept queue (e.g., `accept()` backlog). |
332
+
|`CPU.get(1)` / `CPU.put(1)`| Claiming / releasing a worker thread or GIL slot (Gunicorn, uWSGI, Node.js event‑loop). |
333
+
|`env.timeout(io_wait)` (without a core) |`await redis.get()` – coroutine parked while the kernel handles the socket. |
334
+
| RAM token bucket | cgroup memory limit / container hard‑RSS; requests block when heap is exhausted. |
335
+
336
+
Thus a **CPU‑bound step** models tight Python code holding the GIL, while an **I/O‑bound step** models an `await` that yields control back to the event loop, freeing the core.
260
337
261
338
---
262
339
263
340
341
+
264
342
### **5.4. ClientRuntime: The Destination**
265
343
266
344
This actor typically represents the end-user or system that initiated the request, serving as the final destination.
0 commit comments