"""What one message costs the state backend, in operations. The engine's time goes on round trips, not on Python: a profile against a real Redis put its own code at 8.5% of self-time and Redis I/O at 61%. So the thing worth a regression test is the *count* — a change that quietly turns one pipelined write back into four would not fail any behavioural test and would cost a fifth of the throughput. Counted against the memory backend, because the number is a property of the call sites rather than of the transport. """ from collections import Counter from typing import Any from fluksio.flow.messages import DType, MessageSpec from fluksio.flow.nodes import Node from fluksio.flow.pipeline import Pipeline from fluksio.flow.state import MemoryState COUNTED = ( "get", "set", "get_multi", "get_present", "update", "record", "append_history", "increment_multi", "delete", ) class CountingState(MemoryState): """A state backend that remembers how often it was asked for something.""" # MemoryState uses __slots__; this one needs an attribute of its own. __slots__ = ("calls",) def __init__(self) -> None: super().__init__() self.calls: Counter[str] = Counter() def __getattribute__(self, name: str) -> Any: if name in COUNTED: object.__getattribute__(self, "calls")[name] += 1 return object.__getattribute__(self, name) def spec(name: str) -> MessageSpec: return MessageSpec(name=name, dtype=DType.FLOAT) def make_node(node_id: str, flow: str, f, requires=(), provides=()) -> Node: node = Node(f=f, requires=list(requires), provides=list(provides), name=node_id) node.assign_flow(flow, node_id) return node def build() -> tuple[Pipeline, Node, CountingState]: source = make_node( "source", "chain", lambda params: {"a": 1.0}, provides=[spec("a")] ) relay = make_node( "relay", "chain", lambda a, params: {"b": a + 1}, requires=[spec("a")], provides=[spec("b")], ) sink = make_node( "sink", "chain", lambda b, params: None, requires=[spec("b")] ) state = CountingState() pipeline = Pipeline([source, relay, sink], state=state) return pipeline, source, state def test_recording_a_value_is_one_write() -> None: """Value, timestamp, history and version counter go in one round trip. They were four calls — `update`, `append_history`, `increment_multi` and a `delete` per rate-limited port — and a value crossing an edge pays them twice. """ pipeline, source, state = build() state.calls.clear() pipeline.apply_outputs(source, {"chain.a": 1.0}) assert state.calls["record"] == 1 for method in ("update", "append_history", "increment_multi"): assert state.calls[method] == 0, f"{method} should be folded into record()" def test_a_node_reads_its_inputs_once() -> None: """Readiness and execution share one read of the same keys. The readiness check used to read the triggering inputs and throw the values away, and the node then read the same keys again to run on them. """ pipeline, source, state = build() state.calls.clear() changed = pipeline.apply_outputs(source, {"chain.a": 1.0}) pipeline.run_downstream(source, changed=changed) # Two nodes ran downstream (relay, sink); one bulk read each. assert state.calls["get_present"] == 2 def test_a_hop_costs_a_bounded_number_of_state_operations() -> None: """The whole of one value crossing one edge, counted. A ceiling rather than an exact number, so an unrelated change does not fail it — but low enough that reintroducing a per-value read or a split-up write does. """ pipeline, source, state = build() state.calls.clear() changed = pipeline.apply_outputs(source, {"chain.a": 1.0}) pipeline.run_downstream(source, changed=changed) total = sum(state.calls[name] for name in COUNTED) assert total <= 8, f"state operations per hop grew: {dict(state.calls)}"