Skip to content

Commit e9d95aa

Browse files
committed
Enhanced flexibility of failure policy by adding rules.
1 parent 87e0a04 commit e9d95aa

8 files changed

Lines changed: 1001 additions & 460 deletions

File tree

ngraph/failure_policy.py

Lines changed: 226 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,235 @@
11
from dataclasses import dataclass, field
2-
from random import random
2+
from typing import Any, Dict, List, Literal
3+
from random import random, sample
34

45

5-
@dataclass(slots=True)
6+
@dataclass
7+
class FailureCondition:
8+
"""
9+
A single condition for matching an entity's attribute with an operator and value.
10+
11+
Example usage:
12+
13+
.. code-block:: yaml
14+
15+
conditions:
16+
- attr: "capacity"
17+
operator: "<"
18+
value: 100
19+
20+
:param attr:
21+
The name of the attribute to inspect, e.g. "type", "capacity".
22+
:param operator:
23+
The comparison operator: "==", "!=", "<", "<=", ">", ">=".
24+
:param value:
25+
The value to compare against, e.g. "node", 100, True, etc.
26+
"""
27+
28+
attr: str # e.g. "type", "capacity", "region"
29+
operator: str # "==", "!=", "<", "<=", ">", ">="
30+
value: Any # e.g. "node", 100, "east_coast"
31+
32+
33+
@dataclass
34+
class FailureRule:
35+
"""
36+
A single rule defining how to match entities and then select them for failure.
37+
38+
- conditions: list of conditions
39+
- logic: how to combine conditions ("and", "or", "any")
40+
- rule_type: how to pick from matched entities ("random", "choice", "all")
41+
- probability: used by "random" (a float in [0,1])
42+
- count: used by "choice" (e.g. pick 2)
43+
44+
:param conditions:
45+
A list of :class:`FailureCondition` to filter matching entities.
46+
:param logic:
47+
How to combine the conditions for matching: "and", "or", or "any".
48+
- "and": all conditions must be true
49+
- "or": at least one condition is true
50+
- "any": skip condition checks; everything is matched
51+
:param rule_type:
52+
The selection strategy. One of:
53+
- "random": pick each matched entity with `probability`
54+
- "choice": pick exactly `count` from matched
55+
- "all": pick all matched
56+
:param probability:
57+
Probability of selecting any matched entity (used only if rule_type="random").
58+
:param count:
59+
Number of matched entities to pick (used only if rule_type="choice").
60+
"""
61+
62+
conditions: List[FailureCondition] = field(default_factory=list)
63+
logic: Literal["and", "or", "any"] = "and"
64+
rule_type: Literal["random", "choice", "all"] = "all"
65+
probability: float = 1.0
66+
count: int = 1
67+
68+
69+
@dataclass
670
class FailurePolicy:
771
"""
8-
Mapping from element tag to failure probability.
72+
A container for multiple FailureRules and arbitrary metadata in `attrs`.
73+
74+
The method :meth:`apply_failures` merges nodes and links into a single
75+
dictionary (by their unique ID), and then applies each rule in turn,
76+
building a union of all failed entities.
77+
78+
:param rules:
79+
A list of :class:`FailureRule` objects to apply.
80+
:param attrs:
81+
A dictionary for storing policy-wide metadata (e.g. "name", "description").
982
"""
1083

11-
failure_probabilities: dict[str, float] = field(default_factory=dict)
12-
distribution: str = "uniform"
84+
rules: List[FailureRule] = field(default_factory=list)
85+
attrs: Dict[str, Any] = field(default_factory=dict)
1386

14-
def test_failure(self, tag: str) -> bool:
15-
if self.distribution == "uniform":
16-
return random() < self.failure_probabilities.get(tag, 0)
87+
def apply_failures(
88+
self, nodes: Dict[str, Dict[str, Any]], links: Dict[str, Dict[str, Any]]
89+
) -> List[str]:
90+
"""
91+
Identify which entities (nodes or links) fail according to the
92+
defined rules.
93+
94+
:param nodes:
95+
A mapping of node_name -> node.attrs, where node.attrs has at least
96+
a "type" = "node".
97+
:param links:
98+
A mapping of link_id -> link.attrs, where link.attrs has at least
99+
a "type" = "link".
100+
:returns:
101+
A list of failed entity IDs. For nodes, that ID is typically the
102+
node's name. For links, it's the link's ID.
103+
"""
104+
# Merge nodes and links into a single map of entity_id -> entity_attrs
105+
# e.g. { "SEA": { "type": "node", ...}, "SEA-DEN-xxx": { "type": "link", ...} }
106+
all_entities = {**nodes, **links}
107+
108+
failed_entities = set()
109+
110+
# Evaluate each rule to find matched entities and union them
111+
for rule in self.rules:
112+
matched = self._match_entities(all_entities, rule.conditions, rule.logic)
113+
selected = self._select_entities(matched, all_entities, rule)
114+
failed_entities.update(selected)
115+
116+
return list(failed_entities)
117+
118+
def _match_entities(
119+
self,
120+
all_entities: Dict[str, Dict[str, Any]],
121+
conditions: List[FailureCondition],
122+
logic: str,
123+
) -> List[str]:
124+
"""
125+
Find which entities (by ID) satisfy the given list of conditions
126+
combined by 'and'/'or' logic (or 'any' to skip checks).
127+
128+
:param all_entities:
129+
Mapping of entity_id -> attribute dict.
130+
:param conditions:
131+
List of :class:`FailureCondition` to apply.
132+
:param logic:
133+
"and", "or", or "any".
134+
:returns:
135+
A list of entity IDs that match.
136+
"""
137+
matched = []
138+
for entity_id, attr_dict in all_entities.items():
139+
if self._evaluate_conditions(attr_dict, conditions, logic):
140+
matched.append(entity_id)
141+
return matched
142+
143+
@staticmethod
144+
def _evaluate_conditions(
145+
entity_attrs: Dict[str, Any], conditions: List[FailureCondition], logic: str
146+
) -> bool:
147+
"""
148+
Check if the given entity (via entity_attrs) meets all/any of the conditions.
149+
150+
:param entity_attrs:
151+
The dictionary of attributes for a single entity (node or link).
152+
:param conditions:
153+
A list of conditions to evaluate.
154+
:param logic:
155+
"and" -> all must be true
156+
"or" -> at least one true
157+
"any" -> skip condition checks (always true)
158+
:returns:
159+
True if conditions pass for the specified logic, else False.
160+
"""
161+
if logic == "any":
162+
return True # means "select everything"
163+
if not conditions:
164+
return False # no conditions => no match, unless logic='any'
165+
166+
results = []
167+
for cond in conditions:
168+
results.append(_evaluate_condition(entity_attrs, cond))
169+
170+
if logic == "and":
171+
return all(results)
172+
elif logic == "or":
173+
return any(results)
17174
else:
18-
raise ValueError(f"Unsupported distribution: {self.distribution}")
175+
raise ValueError(f"Unsupported logic: {logic}")
176+
177+
@staticmethod
178+
def _select_entities(
179+
entity_ids: List[str],
180+
all_entities: Dict[str, Dict[str, Any]],
181+
rule: FailureRule,
182+
) -> List[str]:
183+
"""
184+
Select which entity IDs will fail from the matched set, based on rule_type.
185+
186+
:param entity_ids:
187+
IDs that matched the rule's conditions.
188+
:param all_entities:
189+
The full entity dictionary (not strictly needed for some rule_types).
190+
:param rule:
191+
The FailureRule specifying how to pick the final subset.
192+
:returns:
193+
The final list of entity IDs that fail from this rule.
194+
"""
195+
if rule.rule_type == "random":
196+
return [e for e in entity_ids if random() < rule.probability]
197+
elif rule.rule_type == "choice":
198+
count = min(rule.count, len(entity_ids))
199+
# Use sorted(...) to ensure consistent picks when testing
200+
return sample(sorted(entity_ids), k=count)
201+
elif rule.rule_type == "all":
202+
return entity_ids
203+
else:
204+
raise ValueError(f"Unsupported rule_type: {rule.rule_type}")
205+
206+
207+
def _evaluate_condition(entity: Dict[str, Any], cond: FailureCondition) -> bool:
208+
"""
209+
Evaluate one condition (attr, operator, value) against an entity's attrs.
210+
211+
:param entity:
212+
The entity's attribute dictionary (node.attrs or link.attrs).
213+
:param cond:
214+
A single :class:`FailureCondition` specifying 'attr', 'operator', 'value'.
215+
:returns:
216+
True if the condition passes, else False.
217+
:raises ValueError:
218+
If the condition's operator is not recognized.
219+
"""
220+
derived_value = entity.get(cond.attr, None)
221+
op = cond.operator
222+
if op == "==":
223+
return derived_value == cond.value
224+
elif op == "!=":
225+
return derived_value != cond.value
226+
elif op == "<":
227+
return derived_value < cond.value
228+
elif op == "<=":
229+
return derived_value <= cond.value
230+
elif op == ">":
231+
return derived_value > cond.value
232+
elif op == ">=":
233+
return derived_value >= cond.value
234+
else:
235+
raise ValueError(f"Unsupported operator: {op}")

ngraph/network.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
def new_base64_uuid() -> str:
99
"""
10-
Generate a Base64-encoded UUID without padding (~22 characters).
10+
Generate a Base64-encoded UUID without padding (a string with 22 characters).
1111
"""
1212
return base64.urlsafe_b64encode(uuid.uuid4().bytes).decode("ascii").rstrip("=")
1313

@@ -21,7 +21,12 @@ class Node:
2121
in the Network's node dictionary.
2222
2323
:param name: The unique name of the node.
24-
:param attrs: Optional extra metadata for the node.
24+
:param attrs: Optional extra metadata for the node. For example:
25+
{
26+
"type": "node", # auto-tagged upon add_node
27+
"coords": [lat, lon], # user-provided
28+
"region": "west_coast" # user-provided
29+
}
2530
"""
2631

2732
name: str
@@ -42,8 +47,13 @@ class Link:
4247
:param capacity: Link capacity (default 1.0).
4348
:param latency: Link latency (default 1.0).
4449
:param cost: Link cost (default 1.0).
45-
:param attrs: Optional extra metadata for the link.
46-
:param id: Auto-generated unique link identifier.
50+
:param attrs: Optional extra metadata for the link. For example:
51+
{
52+
"type": "link", # auto-tagged upon add_link
53+
"distance_km": 1500, # user-provided
54+
"fiber_provider": "Lumen", # user-provided
55+
}
56+
:param id: Auto-generated unique link identifier, e.g. "SEA-DEN-abCdEf..."
4757
"""
4858

4959
source: str
@@ -67,13 +77,13 @@ class Network:
6777
"""
6878
A container for network nodes and links.
6979
70-
Nodes are stored in a dictionary keyed by their unique names.
71-
Links are stored in a dictionary keyed by their auto-generated IDs.
80+
Nodes are stored in a dictionary keyed by their unique names (:attr:`Node.name`).
81+
Links are stored in a dictionary keyed by their auto-generated IDs (:attr:`Link.id`).
7282
The 'attrs' dict allows extra network metadata.
7383
74-
:param nodes: Mapping from node name to Node.
75-
:param links: Mapping from link id to Link.
76-
:param attrs: Optional extra metadata for the network.
84+
:param nodes: Mapping from node name -> Node object.
85+
:param links: Mapping from link id -> Link object.
86+
:param attrs: Optional extra metadata for the network itself.
7787
"""
7888

7989
nodes: Dict[str, Node] = field(default_factory=dict)
@@ -82,21 +92,33 @@ class Network:
8292

8393
def add_node(self, node: Node) -> None:
8494
"""
85-
Add a node to the network, keyed by its name.
95+
Add a node to the network, keyed by its :attr:`Node.name`.
96+
97+
This method also auto-tags the node with ``node.attrs["type"] = "node"``
98+
if it's not already set.
8699
87100
:param node: The Node to add.
101+
:raises ValueError: If a node with the same name is already in the network.
88102
"""
103+
node.attrs.setdefault("type", "node")
104+
if node.name in self.nodes:
105+
raise ValueError(f"Node '{node.name}' already exists in the network.")
89106
self.nodes[node.name] = node
90107

91108
def add_link(self, link: Link) -> None:
92109
"""
93-
Add a link to the network. Both source and target nodes must exist.
110+
Add a link to the network, keyed by its auto-generated :attr:`Link.id`.
111+
112+
This method also auto-tags the link with ``link.attrs["type"] = "link"``
113+
if it's not already set.
94114
95115
:param link: The Link to add.
96-
:raises ValueError: If the source or target node is not present.
116+
:raises ValueError: If the source/target node is not present in the network.
97117
"""
98118
if link.source not in self.nodes:
99119
raise ValueError(f"Source node '{link.source}' not found in network.")
100120
if link.target not in self.nodes:
101121
raise ValueError(f"Target node '{link.target}' not found in network.")
122+
123+
link.attrs.setdefault("type", "link")
102124
self.links[link.id] = link

0 commit comments

Comments
 (0)