Skip to content

Commit 31dec2e

Browse files
committed
defined client runtime + tests
1 parent 35d507e commit 31dec2e

File tree

14 files changed

+427
-221
lines changed

14 files changed

+427
-221
lines changed

src/app/config/rqs_state.py

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,51 @@
1-
"""
2-
defining a state in a one to one correspondence
3-
with the requests generated that will go through
4-
all the node necessary to accomplish the user request
5-
"""
1+
"""Data structures representing the life-cycle of a single request."""
62

73
from __future__ import annotations
84

95
from dataclasses import dataclass, field
6+
from typing import TYPE_CHECKING, NamedTuple
107

8+
if TYPE_CHECKING:
9+
from app.config.constants import SystemEdges, SystemNodes
1110

12-
@dataclass
13-
class RequestState:
14-
"""
15-
State object carried by each request through the simulation.
16-
17-
Attributes:
18-
id: Unique identifier of the request.
19-
t0: Timestamp (simulated env.now) when the request was generated.
20-
history: List of hop records, each noting a node/edge visit.
21-
finish_time: Timestamp when the requests is satisfied
2211

23-
"""
12+
class Hop(NamedTuple):
13+
"""A single traversal of a node or edge."""
2414

25-
id: int # Unique request identifier
26-
initial_time: float # Generation timestamp (env.now)
27-
finish_time: float | None = None # a requests might be dropped
28-
history: list[str] = field(default_factory=list) # Trace of hops
15+
component_type: SystemNodes | SystemEdges
16+
component_id: str
17+
timestamp: float
2918

30-
def record_hop(self, node_name: str, now: float) -> None:
31-
"""
32-
Append a record of visiting a node or edge.
3319

34-
Args:
35-
node_name: Name of the node or edge being recorded.
36-
now: register the time of the operation
37-
38-
"""
39-
# Record hop as "NodeName@Timestamp"
40-
self.history.append(f"{node_name}@{now:.3f}")
20+
@dataclass
21+
class RequestState:
22+
"""Mutable state carried by each request throughout the simulation."""
23+
24+
id: int
25+
initial_time: float
26+
finish_time: float | None = None
27+
history: list[Hop] = field(default_factory=list)
28+
29+
# ------------------------------------------------------------------ #
30+
# API #
31+
# ------------------------------------------------------------------ #
32+
33+
def record_hop(
34+
self,
35+
component_type: SystemNodes | SystemEdges,
36+
component_id: str,
37+
now: float,
38+
) -> None:
39+
"""Append a new hop in chronological order."""
40+
self.history.append(Hop(component_type, component_id, now))
41+
42+
# ------------------------------------------------------------------ #
43+
# Derived metrics #
44+
# ------------------------------------------------------------------ #
4145

4246
@property
4347
def latency(self) -> float | None:
44-
"""
45-
Return the total time in the system (finish_time - initial_time),
46-
or None if the request hasn't completed yet.
47-
"""
48+
"""Total time inside the system or ``None`` if not yet completed."""
4849
if self.finish_time is None:
4950
return None
5051
return self.finish_time - self.initial_time

src/app/core/runtime/client.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""defining the object client for the simulation"""
2+
3+
from collections.abc import Generator
4+
from typing import TYPE_CHECKING
5+
6+
import simpy
7+
8+
from app.config.constants import SystemNodes
9+
from app.core.runtime.edge import EdgeRuntime
10+
from app.schemas.system_topology_schema.full_system_topology_schema import Client
11+
12+
if TYPE_CHECKING:
13+
from app.config.rqs_state import RequestState
14+
15+
16+
17+
class ClientRuntime:
18+
"""class to define the client runtime"""
19+
20+
def __init__(
21+
self,
22+
env: simpy.Environment,
23+
out_edge: EdgeRuntime,
24+
client_box: simpy.Store,
25+
completed_box: simpy.Store,
26+
client_config: Client,
27+
) -> None:
28+
"""Definition of attributes for the client"""
29+
self.env = env
30+
self.out_edge = out_edge
31+
self.client_config = client_config
32+
self.client_box = client_box
33+
self.completed_box = completed_box
34+
35+
36+
def _forwarder(self) -> Generator[simpy.Event, None, None]:
37+
"""Updtate the state before passing it to another node"""
38+
while True:
39+
state: RequestState = yield self.client_box.get() # type: ignore[assignment]
40+
41+
state.record_hop(
42+
SystemNodes.CLIENT,
43+
self.client_config.id,
44+
self.env.now,
45+
)
46+
47+
# by checking the previous node (-2 the previous component is an edge)
48+
# we are able to understand if the request should be elaborated
49+
# when the type is Generator, or the request is completed, in this case
50+
# the client is the target and the previous node type is not a rqs generator
51+
if state.history[-2].component_type != SystemNodes.GENERATOR:
52+
state.finish_time = self.env.now
53+
yield self.completed_box.put(state)
54+
else:
55+
self.out_edge.transport(state)
56+
57+
def client_run(self) -> simpy.Process:
58+
"""Initialization of the process"""
59+
return self.env.process(self._forwarder())

src/app/core/runtime/edge.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import numpy as np
1313
import simpy
1414

15+
from app.config.constants import SystemEdges
1516
from app.config.rqs_state import RequestState
1617
from app.core.event_samplers.common_helpers import general_sampler
1718
from app.schemas.system_topology_schema.full_system_topology_schema import Edge
@@ -46,12 +47,20 @@ def _deliver(self, state: RequestState) -> Generator[simpy.Event, None, None]:
4647
uniform_variable = self.rng.uniform()
4748
if uniform_variable < self.edge_config.dropout_rate:
4849
state.finish_time = self.env.now
49-
state.record_hop(f"{self.edge_config.id}-dropped", state.finish_time)
50+
state.record_hop(
51+
SystemEdges.NETWORK_CONNECTION,
52+
f"{self.edge_config.id}-dropped",
53+
state.finish_time,
54+
)
5055
return
5156

5257
transit_time = general_sampler(random_variable, self.rng)
5358
yield self.env.timeout(transit_time)
54-
state.record_hop(self.edge_config.id, self.env.now)
59+
state.record_hop(
60+
SystemEdges.NETWORK_CONNECTION,
61+
self.edge_config.id,
62+
self.env.now,
63+
)
5564
yield self.target_box.put(state)
5665

5766

src/app/core/runtime/rqs_generator.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ def _event_arrival(self) -> Generator[simpy.Event, None, None]:
106106
initial_time=self.env.now,
107107

108108
)
109-
state.record_hop(SystemNodes.GENERATOR, self.env.now)
109+
state.record_hop(
110+
SystemNodes.GENERATOR,
111+
self.rqs_generator_data.id,
112+
self.env.now,
113+
)
110114
# transport is a method of the edge runtime
111115
# which define the step of how the state is moving
112116
# from one node to another

src/app/core/simulation_run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020

2121

22-
22+
### TO MODIFY EVERYTHING WORK IN PROGRESS
2323

2424
def run_simulation(
2525
input_data: SimulationPayload,

src/app/schemas/requests_generator_input.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33

44
from pydantic import BaseModel, Field, field_validator
55

6-
from app.config.constants import Distribution, TimeDefaults
6+
from app.config.constants import Distribution, SystemNodes, TimeDefaults
77
from app.schemas.random_variables_config import RVConfig
88

99

1010
class RqsGeneratorInput(BaseModel):
1111
"""Define the expected variables for the simulation"""
1212

13+
id: str
14+
type: SystemNodes = SystemNodes.GENERATOR
1315
avg_active_users: RVConfig
1416
avg_request_per_minute_per_user: RVConfig
1517

tests/conftest.py

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,14 @@ def rng() -> NpGenerator:
153153
return default_rng(0)
154154

155155

156-
# ---------------------------------------------------------------------------
157-
# Metrics sets
158-
# ---------------------------------------------------------------------------
156+
# --------------------------------------------------------------------------- #
157+
# Metric sets #
158+
# --------------------------------------------------------------------------- #
159159

160160

161161
@pytest.fixture(scope="session")
162162
def enabled_sample_metrics() -> set[SampledMetricName]:
163-
"""Default sample-level KPIs tracked in most tests."""
163+
"""Default time-series KPIs collected in most tests."""
164164
return {
165165
SampledMetricName.READY_QUEUE_LEN,
166166
SampledMetricName.RAM_IN_USE,
@@ -169,59 +169,75 @@ def enabled_sample_metrics() -> set[SampledMetricName]:
169169

170170
@pytest.fixture(scope="session")
171171
def enabled_event_metrics() -> set[EventMetricName]:
172-
"""Default event-level KPIs tracked in most tests."""
173-
return {EventMetricName.RQS_LATENCY}
172+
"""Default per-event KPIs collected in most tests."""
173+
return {
174+
EventMetricName.RQS_LATENCY,
175+
}
174176

175177

176-
# ---------------------------------------------------------------------------
177-
# Global simulation settings
178-
# ---------------------------------------------------------------------------
178+
# --------------------------------------------------------------------------- #
179+
# Global simulation settings #
180+
# --------------------------------------------------------------------------- #
179181

180182

181183
@pytest.fixture
182184
def sim_settings(
183185
enabled_sample_metrics: set[SampledMetricName],
184186
enabled_event_metrics: set[EventMetricName],
185187
) -> SimulationSettings:
186-
"""A minimal `SimulationSettings` instance for unit tests."""
188+
"""
189+
Minimal :class:`SimulationSettings` instance.
190+
191+
The simulation horizon is fixed to the lowest allowed value so that unit
192+
tests run quickly.
193+
"""
187194
return SimulationSettings(
188195
total_simulation_time=TimeDefaults.MIN_SIMULATION_TIME,
189196
enabled_sample_metrics=enabled_sample_metrics,
190197
enabled_event_metrics=enabled_event_metrics,
191198
)
192199

193200

194-
# ---------------------------------------------------------------------------
195-
# Traffic profile
196-
# ---------------------------------------------------------------------------
201+
# --------------------------------------------------------------------------- #
202+
# Traffic profile #
203+
# --------------------------------------------------------------------------- #
197204

198205

199206
@pytest.fixture
200207
def rqs_input() -> RqsGeneratorInput:
201-
"""`RqsGeneratorInput` with 1 user and 2 req/min for quick tests."""
208+
"""
209+
One active user issuing two requests per minute—sufficient to
210+
exercise the entire request-generator pipeline with minimal overhead.
211+
"""
202212
return RqsGeneratorInput(
213+
id= "rqs-1",
203214
avg_active_users=RVConfig(mean=1.0),
204215
avg_request_per_minute_per_user=RVConfig(mean=2.0),
205216
user_sampling_window=TimeDefaults.USER_SAMPLING_WINDOW,
206217
)
207218

208219

209-
# ---------------------------------------------------------------------------
210-
# Minimal topology (one client, no servers, no edges)
211-
# ---------------------------------------------------------------------------
220+
# --------------------------------------------------------------------------- #
221+
# Minimal topology (one client, no servers, no edges) #
222+
# --------------------------------------------------------------------------- #
212223

213224

214225
@pytest.fixture
215226
def topology_minimal() -> TopologyGraph:
216-
"""Valid topology with a single client and zero servers/edges."""
227+
"""
228+
A valid topology containing a single client and **no** servers or edges.
229+
230+
Suitable for low-level tests that do not need to traverse the server
231+
layer or network graph.
232+
"""
217233
client = Client(id="client-1")
218234
nodes = TopologyNodes(servers=[], client=client)
219235
return TopologyGraph(nodes=nodes, edges=[])
220236

221237

222-
# ---------------------------------------------------------------------------
223-
# Full simulation payload
224-
# ---------------------------------------------------------------------------
238+
# --------------------------------------------------------------------------- #
239+
# Complete simulation payload #
240+
# --------------------------------------------------------------------------- #
225241

226242

227243
@pytest.fixture
@@ -230,7 +246,12 @@ def payload_base(
230246
sim_settings: SimulationSettings,
231247
topology_minimal: TopologyGraph,
232248
) -> SimulationPayload:
233-
"""End-to-end payload used by high-level simulation tests."""
249+
"""
250+
End-to-end payload used by integration tests and FastAPI endpoint tests.
251+
252+
It wires together the individual fixtures into the single object expected
253+
by the simulation engine.
254+
"""
234255
return SimulationPayload(
235256
rqs_input=rqs_input,
236257
topology_graph=topology_minimal,

0 commit comments

Comments
 (0)