Skip to content

Commit 5265c0e

Browse files
authored
Merge pull request #521 from atlanticwave-sdx/recover-error-connection
Recover error connection when SDX domain becomes UP
2 parents 5a34ae5 + c74dead commit 5265c0e

3 files changed

Lines changed: 362 additions & 28 deletions

File tree

sdx_controller/handlers/connection_handler.py

Lines changed: 238 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,229 @@ def __init__(self, db_instance):
5555
self.db_instance = db_instance
5656
self.parse_helper = ParseHelper()
5757

58+
def save_error_connection(self, connection, reason, details=None) -> None:
59+
if not connection:
60+
return
61+
62+
service_id = connection.get("id")
63+
if not service_id:
64+
return
65+
66+
connection_to_save = dict(connection)
67+
connection_to_save["status"] = str(ConnectionStateMachine.State.ERROR)
68+
69+
error_record = {
70+
"connection": connection_to_save,
71+
"reason": reason,
72+
"updated_at": str(int(time.time())),
73+
}
74+
if details is not None:
75+
error_record["details"] = details
76+
77+
self.db_instance.add_key_value_pair_to_db(
78+
MongoCollections.ERROR_CONNECTIONS,
79+
service_id,
80+
error_record,
81+
)
82+
logger.info(f"Saved connection {service_id} to error collection")
83+
84+
def delete_error_connection(self, service_id) -> None:
85+
self.db_instance.delete_one_entry(
86+
MongoCollections.ERROR_CONNECTIONS, service_id
87+
)
88+
89+
def get_latest_archived_connection(self, service_id):
90+
historical_connections = self.db_instance.get_value_from_db(
91+
MongoCollections.HISTORICAL_CONNECTIONS, service_id
92+
)
93+
if not historical_connections:
94+
return None, None
95+
96+
latest_record = historical_connections[-1]
97+
if not latest_record:
98+
return None, None
99+
100+
latest_timestamp = next(reversed(latest_record))
101+
latest_entry = latest_record.get(latest_timestamp, {})
102+
return latest_entry.get("connection"), latest_entry.get("reason")
103+
104+
def save_error_connection_from_archive(
105+
self, service_id, reason, details=None, expected_archive_reason=None
106+
) -> bool:
107+
connection, archive_reason = self.get_latest_archived_connection(service_id)
108+
if not connection:
109+
return False
110+
111+
if (
112+
expected_archive_reason is not None
113+
and archive_reason != expected_archive_reason
114+
):
115+
return False
116+
117+
self.save_error_connection(connection, reason=reason, details=details)
118+
return True
119+
120+
def retry_error_connections(self, te_manager) -> None:
121+
error_connections = self.db_instance.get_all_entries_in_collection(
122+
MongoCollections.ERROR_CONNECTIONS
123+
)
124+
125+
for error_connection in error_connections:
126+
service_id = next(iter(error_connection))
127+
error_record = error_connection.get(service_id, {})
128+
connection = error_record.get("connection", error_record)
129+
130+
if not connection:
131+
continue
132+
133+
active_connection = self.db_instance.get_value_from_db(
134+
MongoCollections.CONNECTIONS, service_id
135+
)
136+
if active_connection:
137+
active_status = active_connection.get("status")
138+
if active_status in {
139+
str(ConnectionStateMachine.State.RECOVERING),
140+
str(ConnectionStateMachine.State.UNDER_PROVISIONING),
141+
str(ConnectionStateMachine.State.UP),
142+
str(ConnectionStateMachine.State.MODIFYING),
143+
}:
144+
logger.info(
145+
f"Skipping retry for {service_id}; active status is {active_status}"
146+
)
147+
continue
148+
connection = active_connection
149+
150+
connection_to_retry = dict(connection)
151+
connection_to_retry["status"] = str(ConnectionStateMachine.State.RECOVERING)
152+
connection_to_retry["oxp_success_count"] = 0
153+
connection_to_retry.pop("oxp_response", None)
154+
155+
self.db_instance.add_key_value_pair_to_db(
156+
MongoCollections.CONNECTIONS, service_id, connection_to_retry
157+
)
158+
159+
reason, code = self.place_connection(te_manager, connection_to_retry)
160+
logger.info(
161+
f"Retry error connection result: ID: {service_id} reason='{reason}', code={code}"
162+
)
163+
164+
if code // 100 != 2:
165+
connection_to_retry["status"] = str(ConnectionStateMachine.State.ERROR)
166+
self.db_instance.add_key_value_pair_to_db(
167+
MongoCollections.CONNECTIONS, service_id, connection_to_retry
168+
)
169+
self.save_error_connection(
170+
connection_to_retry,
171+
reason="Retry failed",
172+
details={"place_connection_reason": reason, "code": code},
173+
)
174+
175+
def _response_is_success(self, oxp_response, domain):
176+
if not isinstance(oxp_response, dict):
177+
return False
178+
179+
domain_name = self._get_domain_name(domain)
180+
for response_key in (domain, domain_name):
181+
response = oxp_response.get(response_key)
182+
if response is None:
183+
continue
184+
if isinstance(response, (list, tuple)) and response:
185+
status_code = response[0]
186+
elif isinstance(response, dict):
187+
status_code = response.get("status_code") or response.get("code")
188+
else:
189+
continue
190+
try:
191+
return int(status_code) // 100 == 2
192+
except (TypeError, ValueError):
193+
return False
194+
195+
return False
196+
197+
def _count_successful_oxp_responses(self, oxp_response, breakdown):
198+
return sum(
199+
1 for domain in breakdown if self._response_is_success(oxp_response, domain)
200+
)
201+
202+
def recover_domain_connections(self, recovered_domain):
203+
"""
204+
Re-publish missing OXP breakdowns when an SDX-LC comes back online.
205+
"""
206+
connections = self.db_instance.get_all_entries_in_collection(
207+
MongoCollections.CONNECTIONS
208+
)
209+
recovered = 0
210+
211+
for connection_entry in connections:
212+
service_id = next(iter(connection_entry))
213+
connection = self.db_instance.get_value_from_db(
214+
MongoCollections.CONNECTIONS, service_id
215+
)
216+
if not connection:
217+
continue
218+
219+
breakdown = self.db_instance.get_value_from_db(
220+
MongoCollections.BREAKDOWNS, service_id
221+
)
222+
if not breakdown:
223+
continue
224+
225+
if recovered_domain not in {
226+
self._get_domain_name(domain) for domain in breakdown
227+
}:
228+
continue
229+
230+
connection_status = connection.get("status")
231+
if connection_status == str(ConnectionStateMachine.State.UP):
232+
continue
233+
234+
domain_breakdown = {}
235+
oxp_response = connection.get("oxp_response") or {}
236+
if connection.get("partial_cleanup_requested"):
237+
domain_breakdown = breakdown
238+
connection["oxp_response"] = {}
239+
connection["oxp_success_count"] = 0
240+
connection["partial_cleanup_requested"] = False
241+
connection["late_cleanup_domains"] = []
242+
else:
243+
for domain, segment in breakdown.items():
244+
if self._response_is_success(oxp_response, domain):
245+
continue
246+
domain_breakdown[domain] = segment
247+
connection["oxp_success_count"] = self._count_successful_oxp_responses(
248+
oxp_response, breakdown
249+
)
250+
251+
if not domain_breakdown:
252+
continue
253+
254+
logger.info(
255+
f"Recovering service {service_id} for recovered LC domain {recovered_domain}"
256+
)
257+
connection["provisioning_timeout_handled"] = False
258+
connection["provisioning_started_at"] = time.time()
259+
connection.pop("timeout_reason", None)
260+
if connection.get("status") in {
261+
str(ConnectionStateMachine.State.DOWN),
262+
str(ConnectionStateMachine.State.ERROR),
263+
}:
264+
connection["status"] = str(ConnectionStateMachine.State.RECOVERING)
265+
266+
self.db_instance.add_key_value_pair_to_db(
267+
MongoCollections.CONNECTIONS, service_id, connection
268+
)
269+
270+
_reason, code = self._send_breakdown_to_lc(
271+
domain_breakdown, "post", connection
272+
)
273+
logger.info(
274+
f"LC recovery publish result: ID: {service_id} reason='{_reason}', code={code}"
275+
)
276+
if code // 100 == 2:
277+
recovered += 1
278+
279+
return recovered
280+
58281
def _wait_for_provisioning_to_settle(self, service_id, expected_domains):
59282
deadline = time.time() + UNDER_PROVISIONING_DELETE_SETTLE_TIMEOUT_SECONDS
60283
latest_connection = self.db_instance.get_value_from_db(
@@ -770,11 +993,21 @@ def handle_link_failure(self, te_manager, failed_links):
770993
archive=False,
771994
)
772995
if code // 100 != 2:
996+
self.save_error_connection(
997+
connection,
998+
reason="Failure handling delete failed",
999+
details={"code": code},
1000+
)
7731001
logger.info(
7741002
f"Do not remove connection, may be already removed: {connection['id']}, code: {code}"
7751003
)
7761004
continue
7771005
except Exception as err:
1006+
self.save_error_connection(
1007+
connection,
1008+
reason="Failure handling delete raised exception",
1009+
details={"error": str(err)},
1010+
)
7781011
logger.info(
7791012
f"Encountered error when deleting connection: {err}"
7801013
)
@@ -822,7 +1055,11 @@ def handle_link_failure(self, te_manager, failed_links):
8221055
"place_connection failed during link failure rerouting"
8231056
)
8241057
code = 400
825-
1058+
self.save_error_connection(
1059+
connection,
1060+
reason="Failure handling recovery failed",
1061+
details={"place_connection_reason": _reason, "code": code},
1062+
)
8261063
logger.info(
8271064
f"place_connection result: ID: {service_id} reason='{_reason}', code={code}"
8281065
)

sdx_controller/handlers/lc_message_handler.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ def process_lc_json_msg(
220220
elif msg_json.get("msg_type") and msg_json["msg_type"] == "oxp_conn_response":
221221
logger.info("Received OXP connection response.")
222222
service_id = msg_json.get("service_id")
223+
operation = msg_json.get("operation")
224+
oxp_response_code = msg_json.get("oxp_response_code")
225+
oxp_response_msg = msg_json.get("oxp_response")
223226

224227
if not service_id:
225228
return
@@ -229,6 +232,20 @@ def process_lc_json_msg(
229232
)
230233

231234
if not connection:
235+
if operation == "delete" and oxp_response_code // 100 != 2:
236+
saved = self.connection_handler.save_error_connection_from_archive(
237+
service_id,
238+
reason="Delete failed during failure handling",
239+
details={
240+
"oxp_response_code": oxp_response_code,
241+
"oxp_response": oxp_response_msg,
242+
},
243+
expected_archive_reason="Failure",
244+
)
245+
if saved:
246+
logger.info(
247+
f"Saved archived connection {service_id} to error collection after delete failure"
248+
)
232249
return
233250

234251
breakdown = self.db_instance.get_value_from_db(
@@ -243,9 +260,6 @@ def process_lc_json_msg(
243260
oxp_success_count = connection.get("oxp_success_count", 0)
244261
lc_domain = msg_json.get("lc_domain")
245262
response_domain = msg_json.get("breakdown_domain") or lc_domain
246-
oxp_response_code = msg_json.get("oxp_response_code")
247-
oxp_response_msg = msg_json.get("oxp_response")
248-
operation = msg_json.get("operation")
249263
oxp_response = connection.get("oxp_response")
250264
if not oxp_response:
251265
oxp_response = {}
@@ -302,7 +316,17 @@ def process_lc_json_msg(
302316
connection, _ = connection_state_machine(
303317
connection, ConnectionStateMachine.State.UP
304318
)
319+
self.connection_handler.delete_error_connection(service_id)
305320
else:
321+
if operation == "delete":
322+
self.connection_handler.save_error_connection(
323+
connection,
324+
reason="Delete failed during failure handling",
325+
details={
326+
"oxp_response_code": oxp_response_code,
327+
"oxp_response": oxp_response_msg,
328+
},
329+
)
306330
if connection.get("status") and (
307331
connection.get("status")
308332
== str(ConnectionStateMachine.State.MODIFYING)
@@ -364,8 +388,24 @@ def process_lc_json_msg(
364388
logger.info("Save to database complete.")
365389
logger.info("message ID:" + str(db_msg_id))
366390

391+
previous_domain_status = domain_dict.get(domain_name)
392+
is_new_domain = domain_name not in domain_dict
393+
is_recovered_domain = (
394+
previous_domain_status is not None
395+
and previous_domain_status != DomainStatus.UP
396+
)
397+
367398
# Update existing topology
368399
if domain_name in domain_dict:
400+
if is_recovered_domain:
401+
logger.info(
402+
f"Domain {domain_name} status changed from "
403+
f"{previous_domain_status} to {DomainStatus.UP}"
404+
)
405+
domain_dict[domain_name] = DomainStatus.UP
406+
self.db_instance.add_key_value_pair_to_db(
407+
MongoCollections.DOMAINS, Constants.DOMAIN_DICT, domain_dict
408+
)
369409
logger.info("Updating topo")
370410
logger.debug(msg_json)
371411
(
@@ -420,4 +460,6 @@ def process_lc_json_msg(
420460
self.db_instance.add_key_value_pair_to_db(
421461
MongoCollections.TOPOLOGIES, Constants.LATEST_TOPOLOGY, latest_topo
422462
)
463+
if is_new_domain or is_recovered_domain:
464+
self.connection_handler.retry_error_connections(self.te_manager)
423465
logger.info("Save to database complete.")

0 commit comments

Comments
 (0)