|
| 1 | +# AsyncFlow — Public API Reference: `components` |
| 2 | + |
| 3 | +This page documents the **public topology components** you can import from |
| 4 | +`asyncflow.components` to construct a simulation scenario in Python. |
| 5 | +These classes are Pydantic models with strict validation and are the |
| 6 | +**only pieces you need** to define the *structure* of your system: nodes |
| 7 | +(client/servers/LB), endpoints (steps), and network edges. |
| 8 | + |
| 9 | +> The builder (`AsyncFlow`) will assemble these into the internal graph for you. |
| 10 | +> You **do not** need to import internal graph classes. |
| 11 | +
|
| 12 | +--- |
| 13 | + |
| 14 | +## Imports |
| 15 | + |
| 16 | +```python |
| 17 | +from asyncflow.components import ( |
| 18 | + Client, |
| 19 | + Server, |
| 20 | + ServerResources, |
| 21 | + LoadBalancer, |
| 22 | + Endpoint, |
| 23 | + Edge, |
| 24 | +) |
| 25 | +# Optional enums (strings are also accepted): |
| 26 | +from asyncflow.enums import Distribution |
| 27 | +``` |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## Quick example |
| 32 | + |
| 33 | +```python |
| 34 | +from asyncflow.components import ( |
| 35 | + Client, Server, ServerResources, LoadBalancer, Endpoint, Edge |
| 36 | +) |
| 37 | + |
| 38 | +# Nodes |
| 39 | +client = Client(id="client-1") |
| 40 | + |
| 41 | +endpoint = Endpoint( |
| 42 | + endpoint_name="/predict", |
| 43 | + steps=[ |
| 44 | + {"kind": "ram", "step_operation": {"necessary_ram": 64}}, |
| 45 | + {"kind": "initial_parsing", "step_operation": {"cpu_time": 0.002}}, |
| 46 | + {"kind": "io_wait", "step_operation": {"io_waiting_time": 0.010}}, |
| 47 | + ], |
| 48 | +) |
| 49 | + |
| 50 | +server = Server( |
| 51 | + id="srv-1", |
| 52 | + server_resources=ServerResources(cpu_cores=2, ram_mb=2048), |
| 53 | + endpoints=[endpoint], |
| 54 | +) |
| 55 | + |
| 56 | +lb = LoadBalancer(id="lb-1", algorithms="round_robin", server_covered={"srv-1"}) |
| 57 | + |
| 58 | +# Edges (directed) |
| 59 | +edges = [ |
| 60 | + Edge( |
| 61 | + id="gen-to-client", |
| 62 | + source="rqs-1", # external sources allowed (e.g., generator id) |
| 63 | + target="client-1", # targets must be declared nodes |
| 64 | + latency={"mean": 0.003, "distribution": "exponential"}, |
| 65 | + ), |
| 66 | + Edge( |
| 67 | + id="client-to-lb", |
| 68 | + source="client-1", |
| 69 | + target="lb-1", |
| 70 | + latency={"mean": 0.002, "distribution": "exponential"}, |
| 71 | + ), |
| 72 | + Edge( |
| 73 | + id="lb-to-srv1", |
| 74 | + source="lb-1", |
| 75 | + target="srv-1", |
| 76 | + latency={"mean": 0.002, "distribution": "exponential"}, |
| 77 | + ), |
| 78 | + Edge( |
| 79 | + id="srv1-to-client", |
| 80 | + source="srv-1", |
| 81 | + target="client-1", |
| 82 | + latency={"mean": 0.003, "distribution": "exponential"}, |
| 83 | + ), |
| 84 | +] |
| 85 | +``` |
| 86 | + |
| 87 | +You can then feed these to the `AsyncFlow` builder (not shown here) along with |
| 88 | +workload and settings. |
| 89 | + |
| 90 | +--- |
| 91 | + |
| 92 | +## Component reference |
| 93 | + |
| 94 | +### `Client` |
| 95 | + |
| 96 | +```python |
| 97 | +Client(id: str) |
| 98 | +``` |
| 99 | + |
| 100 | +* Represents the client node. |
| 101 | +* `type` is fixed internally to `"client"`. |
| 102 | +* **Validation:** any non-standard `type` is rejected (guardrail). |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +### `ServerResources` |
| 107 | + |
| 108 | +```python |
| 109 | +ServerResources( |
| 110 | + cpu_cores: int = 1, # ≥ 1 NOW MUST BE FIXED TO ONE |
| 111 | + ram_mb: int = 1024, # ≥ 256 |
| 112 | + db_connection_pool: int | None = None, |
| 113 | +) |
| 114 | +``` |
| 115 | + |
| 116 | +* Server capacity knobs used by the runtime (CPU tokens, RAM reservoir, optional DB pool). |
| 117 | +* You may pass a **dict** instead of `ServerResources`; Pydantic will coerce it. |
| 118 | + |
| 119 | +**Bounds & defaults** |
| 120 | + |
| 121 | +* `cpu_cores ≥ 1` |
| 122 | +* `ram_mb ≥ 256` |
| 123 | +* `db_connection_pool` optional |
| 124 | + |
| 125 | +--- |
| 126 | + |
| 127 | +### `Endpoint` |
| 128 | + |
| 129 | +```python |
| 130 | +Endpoint( |
| 131 | + endpoint_name: str, # normalized to lowercase |
| 132 | + steps: list[dict], # or Pydantic Step objects (dict is simpler) |
| 133 | +) |
| 134 | +``` |
| 135 | + |
| 136 | +Each step is a dict with **exactly one** operation: |
| 137 | + |
| 138 | +```python |
| 139 | +{"kind": <step-kind>, "step_operation": { <op-key>: <positive number> }} |
| 140 | +``` |
| 141 | + |
| 142 | +**Valid step kinds and operation keys** |
| 143 | + |
| 144 | +| Kind (enum string) | Operation dict (exactly 1 key) | Units / constraints | | |
| 145 | +| --------------------- | -------------------------------- | ------------------- | ------- | |
| 146 | +| `initial_parsing` | `{ "cpu_time": <float> }` | seconds, > 0 | | |
| 147 | +| `cpu_bound_operation` | `{ "cpu_time": <float> }` | seconds, > 0 | | |
| 148 | +| `ram` | \`{ "necessary\_ram": \<int | float> }\` | MB, > 0 | |
| 149 | +| `io_task_spawn` | `{ "io_waiting_time": <float> }` | seconds, > 0 | | |
| 150 | +| `io_llm` | `{ "io_waiting_time": <float> }` | seconds, > 0 | | |
| 151 | +| `io_wait` | `{ "io_waiting_time": <float> }` | seconds, > 0 | | |
| 152 | +| `io_db` | `{ "io_waiting_time": <float> }` | seconds, > 0 | | |
| 153 | +| `io_cache` | `{ "io_waiting_time": <float> }` | seconds, > 0 | | |
| 154 | + |
| 155 | +**Validation** |
| 156 | + |
| 157 | +* `endpoint_name` is lowercased automatically. |
| 158 | +* `step_operation` must have **one and only one** entry. |
| 159 | +* The operation **must match** the step kind (CPU ↔ `cpu_time`, RAM ↔ `necessary_ram`, IO ↔ `io_waiting_time`). |
| 160 | +* All numeric values must be **strictly positive**. |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +### `Server` |
| 165 | + |
| 166 | +```python |
| 167 | +Server( |
| 168 | + id: str, |
| 169 | + server_resources: ServerResources | dict, |
| 170 | + endpoints: list[Endpoint], |
| 171 | +) |
| 172 | +``` |
| 173 | + |
| 174 | +* Represents a server node hosting one or more endpoints. |
| 175 | +* `type` is fixed internally to `"server"`. |
| 176 | +* **Validation:** any non-standard `type` is rejected. |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +### `LoadBalancer` (optional) |
| 181 | + |
| 182 | +```python |
| 183 | +LoadBalancer( |
| 184 | + id: str, |
| 185 | + algorithms: Literal["round_robin", "least_connection"] = "round_robin", |
| 186 | + server_covered: set[str] = set(), |
| 187 | +) |
| 188 | +``` |
| 189 | + |
| 190 | +* Declares a logical load balancer and the set of server IDs it can route to. |
| 191 | +* **Graph-level rules** (checked when the payload is built): |
| 192 | + |
| 193 | + * `server_covered` must be a subset of declared server IDs. |
| 194 | + * There must be an **edge from the LB to each covered server** (e.g., `lb-1 → srv-1`). |
| 195 | + |
| 196 | +--- |
| 197 | + |
| 198 | +### `Edge` |
| 199 | + |
| 200 | +```python |
| 201 | +Edge( |
| 202 | + id: str, |
| 203 | + source: str, |
| 204 | + target: str, |
| 205 | + latency: dict | RVConfig, # recommend dict: {"mean": <float>, "distribution": <enum>, "variance": <float?>} |
| 206 | + edge_type: Literal["network_connection"] = "network_connection", |
| 207 | + dropout_rate: float = 0.01, # in [0.0, 1.0] |
| 208 | +) |
| 209 | +``` |
| 210 | + |
| 211 | +* Directed link between two nodes. |
| 212 | +* **Latency** is a random variable; most users pass a dict: |
| 213 | + |
| 214 | + * `mean: float` (required) |
| 215 | + * `distribution: "poisson" | "normal" | "log_normal" | "exponential" | "uniform"` (default: `"poisson"`) |
| 216 | + * `variance: float?` (for `normal`/`log_normal`, defaults to `mean` if omitted) |
| 217 | + |
| 218 | +**Validation** |
| 219 | + |
| 220 | +* `mean > 0` |
| 221 | +* if provided, `variance ≥ 0` |
| 222 | +* `dropout_rate ∈ [0.0, 1.0]` |
| 223 | +* `source != target` |
| 224 | + |
| 225 | +**Graph-level rules** (enforced when the full payload is validated) |
| 226 | + |
| 227 | +* Every **target** must be a **declared node** (`client`, `server`, or `load_balancer`). |
| 228 | +* **External IDs** (e.g., `"rqs-1"`) are allowed **only** as **sources**; they cannot appear as targets. |
| 229 | +* **Unique edge IDs**. |
| 230 | +* **No fan-out except LB**: only the load balancer is allowed to have multiple outgoing edges among declared nodes. |
| 231 | + |
| 232 | +--- |
| 233 | + |
| 234 | +## Type coercion & enums |
| 235 | + |
| 236 | +* You may pass strings for enums (`kind`, `distribution`, etc.); they will be validated against the allowed values. |
| 237 | +* For `ServerResources` and `Edge.latency` you can pass dictionaries; Pydantic will coerce them to typed models. |
| 238 | +* If you prefer, you can import and use the enums: |
| 239 | + |
| 240 | + ```python |
| 241 | + from asyncflow.enums import Distribution |
| 242 | + Edge(..., latency={"mean": 0.003, "distribution": Distribution.EXPONENTIAL}) |
| 243 | + ``` |
| 244 | + |
| 245 | +--- |
| 246 | + |
| 247 | +## Best practices & pitfalls |
| 248 | + |
| 249 | +**Do** |
| 250 | + |
| 251 | +* Keep IDs unique across nodes of the same category and across edges. |
| 252 | +* Ensure LB coverage and LB→server edges are in sync. |
| 253 | +* Use small, measurable step values first; iterate once you see where queues and delays form. |
| 254 | + |
| 255 | +**Don’t** |
| 256 | + |
| 257 | +* Create multiple outgoing edges from non-LB nodes (graph validator will fail). |
| 258 | +* Use zero/negative times or RAM (validators will raise). |
| 259 | +* Target external IDs (only sources may be external). |
| 260 | + |
| 261 | +--- |
| 262 | + |
| 263 | +## Where these components fit |
| 264 | + |
| 265 | +You will typically combine these **components** with: |
| 266 | + |
| 267 | +* **workload** (`RqsGenerator`) from `asyncflow.workload` |
| 268 | +* **settings** (`SimulationSettings`) from `asyncflow.settings` |
| 269 | +* the **builder** (`AsyncFlow`) and **runner** (`SimulationRunner`) from the root package |
| 270 | + |
| 271 | +Example (wiring, abbreviated): |
| 272 | + |
| 273 | +```python |
| 274 | +from asyncflow import AsyncFlow, SimulationRunner |
| 275 | +from asyncflow.workload import RqsGenerator |
| 276 | +from asyncflow.settings import SimulationSettings |
| 277 | + |
| 278 | +flow = ( |
| 279 | + AsyncFlow() |
| 280 | + .add_generator(RqsGenerator(...)) |
| 281 | + .add_client(client) |
| 282 | + .add_servers(server) |
| 283 | + .add_edges(*edges) |
| 284 | + .add_load_balancer(lb) # optional |
| 285 | + .add_simulation_settings(SimulationSettings(...)) |
| 286 | +) |
| 287 | +payload = flow.build_payload() # validates graph-level rules |
| 288 | +SimulationRunner(..., simulation_input=payload).run() |
| 289 | +``` |
| 290 | + |
| 291 | +--- |
| 292 | + |
| 293 | +With these `components`, you can model any topology supported by AsyncFlow— |
| 294 | +cleanly, type-checked, and with **clear, early** validation errors when something |
| 295 | +is inconsistent. |
0 commit comments