SIENTIAPDE-1974: fix(slots): address the code review of the unsubscribe fix
Code review (code-review skill, xhigh) of057c1fd: 6 findings, all fixed. - unsubscribe_slot handles a failing reader: the legacy OpcManager's delete() raises when the server session is gone, which still stopped the main loop. The failure is notified (OPC_UNSUBSCRIBE_ERROR_<slot>:<server>) and logged, the slot is dropped from that reader's books (as the v2 engine already does) so it can be subscribed again, and the other readers are still unsubscribed. Repo convention: a single server failure must not kill the pod. - update_slot_config unsubscribes a slot deleted from Redis right away (_unsubscribe_removed_slots) instead of leaving it to the snapshot diff in update_ingestor_manager, which leaked the subscription if anything in between failed. The later diff call is now a no-op. - manage_leases unsubscribes the extra slots before giving their leases back, so another pod never takes a slot this pod is still reading. - unsubscribe_slot iterates a snapshot of the readers (list(...) # NOSONAR, as in pool.close_all / ServerReader.shutdown): the loop awaits, so a reader added or removed meanwhile must not break the iteration. - Contract test with the real readers (ServerReader over the real SubscriptionEngine, and the legacy OpcManager): both expose `subscriptions` keyed by slot and both are unsubscribed (subscription deleted, pool unpinned). - The reader test double moves to tests/unit/conftest.py (reader_double). 283 unit tests; the 3 new behavior tests fail on057c1fd. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -549,17 +549,46 @@ class IngestorManager(SientiaMonitoring):
|
||||
also left the subscription open on the OPC server. Unsubscribing a slot that no
|
||||
reader holds is a no-op.
|
||||
|
||||
A reader that fails to unsubscribe (e.g. the legacy OpcManager, whose delete()
|
||||
raises when the server session is gone) is notified and logged, and the slot is
|
||||
dropped from its books, as the v2 engine already does, so it can be subscribed
|
||||
again. The other readers are still unsubscribed and the main loop goes on.
|
||||
|
||||
Args:
|
||||
slot (str): The name of the slot to unsubscribe.
|
||||
|
||||
Side Effects:
|
||||
- Removes the slot's subscription from every OPC reader that has one
|
||||
- Cleans up subscription resources on the OPC servers
|
||||
- Sends OPC_UNSUBSCRIBE_ERROR_<slot>:<server> when a reader fails
|
||||
"""
|
||||
|
||||
for reader in self.opc_managers.values():
|
||||
if slot in reader.subscriptions:
|
||||
# list() snapshots the readers: the loop awaits, so a reader added or removed
|
||||
# meanwhile must not break the iteration.
|
||||
for server, reader in list(self.opc_managers.items()): # NOSONAR
|
||||
if slot not in reader.subscriptions:
|
||||
continue
|
||||
try:
|
||||
await reader.unsubscribe(slot)
|
||||
except Exception as e:
|
||||
trace = traceback.format_exc()
|
||||
await self.send_notification_async(
|
||||
metadata=self.metadata,
|
||||
notification_id=f'OPC_UNSUBSCRIBE_ERROR_{slot}:{server}',
|
||||
message=f'Failed to unsubscribe slot {slot} from {server}: {e}',
|
||||
block='opc_manager',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=trace,
|
||||
)
|
||||
self.logger.error(trace)
|
||||
reader.subscriptions.pop(slot, None)
|
||||
|
||||
async def _unsubscribe_removed_slots(self, slots: list[str]) -> None:
|
||||
"""Unsubscribe and forget slots whose configuration is gone from Redis."""
|
||||
for slot in slots:
|
||||
self.logger.warning(f'Slot {slot} is no longer configured; unsubscribing it')
|
||||
await self.unsubscribe_slot(slot)
|
||||
self.managed_tags.pop(slot, None)
|
||||
|
||||
async def update_slot_config(self):
|
||||
"""
|
||||
@@ -569,7 +598,7 @@ class IngestorManager(SientiaMonitoring):
|
||||
This method performs the following steps:
|
||||
1. Renews the lease for each managed slot using the resource manager.
|
||||
2. Fetches the latest configuration for each slot.
|
||||
3. Logs and removes slots whose configurations are no longer available.
|
||||
3. Logs, unsubscribes and removes slots whose configurations are no longer available.
|
||||
4. Updates the configuration of slots if changes are detected.
|
||||
5. Unsubscribes and re-subscribes to slots with updated configurations.
|
||||
6. Removes slots from the managed tags if they are no longer valid.
|
||||
@@ -602,8 +631,9 @@ class IngestorManager(SientiaMonitoring):
|
||||
continue
|
||||
self.managed_tags[slot] = update
|
||||
|
||||
for slot in removed_slots:
|
||||
self.managed_tags.pop(slot, None)
|
||||
# Unsubscribe right away: leaving it to update_ingestor_manager's snapshot diff
|
||||
# would leak the subscription if anything in between failed
|
||||
await self._unsubscribe_removed_slots(removed_slots)
|
||||
|
||||
def parse_slot_config(self, slot: str, raw: dict) -> dict | None:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user