@@ -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 )
0 commit comments