SIENTIAPDE-1312

Refactor OpcManager connection error handling and enhance unit tests

- Simplified error handling during connection attempts in OpcManager by removing redundant disconnection logic.
- Updated unit tests to include subscription_period_ms in server configuration and adjusted connection assertions to include a timeout parameter.
- Added new tests for disconnection fallback functionality to ensure robust error handling during disconnect attempts.
This commit is contained in:
vitor-aignosi
2025-10-20 17:08:40 -03:00
parent aaa8d00f56
commit 3eacf27faf
3 changed files with 36 additions and 43 deletions

View File

@@ -210,19 +210,8 @@ class OpcManager(BaseActivity):
).set(1) ).set(1)
self.logger.info(f'Connection to {self.name} successful.') self.logger.info(f'Connection to {self.name} successful.')
except Exception as e: except Exception as e:
try:
await self.disconnect() await self.disconnect()
except Exception as internal_e:
self.logger.error(f'And error occurred while creating connection from {self.name}, and an exception occurred while disconnecting: {internal_e}')
del self.client
self.client = None
metrics.OPC_CONNECTION_STATUS.labels(
pod_id=self.pod_id, server_name=self.name, server_url=self.url
).set(0)
metrics.OPC_CONNECTIONS_FAILED.labels(pod_id=self.pod_id, server_name=self.name).inc()
self.logger.error(f'Failed to connect to {self.name}: {e}')
raise raise
async def create_subscription(self, name: str): async def create_subscription(self, name: str):

View File

@@ -94,6 +94,7 @@ async def test_initialize_opc_from_config(opc_manager, ingestor_manager):
server_config = { server_config = {
'name': 'server1', 'name': 'server1',
'url': 'opc.tcp://localhost:4840', 'url': 'opc.tcp://localhost:4840',
'subscription_period_ms': 1000,
'server_uri': 'http://opcua-server.simulator', 'server_uri': 'http://opcua-server.simulator',
'cert_path': '/path/to/cert', 'cert_path': '/path/to/cert',
'private_key_path': '/path/to/private_key', 'private_key_path': '/path/to/private_key',
@@ -109,6 +110,7 @@ async def test_initialize_opc_from_config(opc_manager, ingestor_manager):
url=server_config['url'], url=server_config['url'],
data_manager=ingestor_manager.data_manager, data_manager=ingestor_manager.data_manager,
logger=ingestor_manager.logger, logger=ingestor_manager.logger,
subscription_period_ms=server_config['subscription_period_ms'],
server_uri=server_config['server_uri'], server_uri=server_config['server_uri'],
notification_handler=ingestor_manager.notification_handler, notification_handler=ingestor_manager.notification_handler,
cert_path=server_config['cert_path'], cert_path=server_config['cert_path'],
@@ -128,6 +130,7 @@ async def test_initialize_opc_from_config_exception(traceback_mock, opc_manager,
server_config = { server_config = {
'name': 'server1', 'name': 'server1',
'url': 'opc.tcp://localhost:4840', 'url': 'opc.tcp://localhost:4840',
'subscription_period_ms': 1000,
'server_uri': 'http://opcua-server.simulator', 'server_uri': 'http://opcua-server.simulator',
'cert_path': '/path/to/cert', 'cert_path': '/path/to/cert',
'private_key_path': '/path/to/private_key', 'private_key_path': '/path/to/private_key',

View File

@@ -1,6 +1,6 @@
import json import json
from datetime import datetime from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, call, patch from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
import pytest import pytest
from asyncua.crypto.security_policies import SecurityPolicyBasic256 from asyncua.crypto.security_policies import SecurityPolicyBasic256
@@ -142,7 +142,7 @@ async def test_connect_no_security(client, mock_metrics, raw_opc_manager):
await raw_opc_manager.connect() await raw_opc_manager.connect()
client.assert_called_once_with(raw_opc_manager.url, watchdog_intervall=3600000) client.assert_called_once_with(raw_opc_manager.url, timeout=10, watchdog_intervall=3600000)
raw_opc_manager.client.connect.assert_called_once() raw_opc_manager.client.connect.assert_called_once()
raw_opc_manager.set_security.assert_not_called() raw_opc_manager.set_security.assert_not_called()
mock_metrics.OPC_CONNECTIONS_TOTAL.labels.assert_called_once_with( mock_metrics.OPC_CONNECTIONS_TOTAL.labels.assert_called_once_with(
@@ -169,7 +169,7 @@ async def test_connect_with_security(client, raw_opc_manager):
await raw_opc_manager.connect() await raw_opc_manager.connect()
client.assert_called_once_with(raw_opc_manager.url, watchdog_intervall=3600000) client.assert_called_once_with(raw_opc_manager.url, timeout=10, watchdog_intervall=3600000)
raw_opc_manager.client.connect.assert_called_once() raw_opc_manager.client.connect.assert_called_once()
raw_opc_manager.set_security.assert_called_once() raw_opc_manager.set_security.assert_called_once()
@@ -184,33 +184,15 @@ async def test_connect_exception_handling_and_metrics(
simulated_error_message = 'Erro de conexão simulado' simulated_error_message = 'Erro de conexão simulado'
mock_client_instance.connect.side_effect = Exception(simulated_error_message) mock_client_instance.connect.side_effect = Exception(simulated_error_message)
raw_opc_manager.disconnect = AsyncMock()
opc_manager_instance = raw_opc_manager opc_manager_instance = raw_opc_manager
opc_manager_instance.cert_path = None opc_manager_instance.cert_path = None
with pytest.raises(Exception, match=simulated_error_message): with pytest.raises(Exception, match=simulated_error_message):
await opc_manager_instance.connect() await opc_manager_instance.connect()
mock_metrics_module.OPC_CONNECTIONS_TOTAL.labels.assert_called_once_with( opc_manager_instance.disconnect.assert_called_once()
pod_id=opc_manager_instance.pod_id, server_name=opc_manager_instance.name
)
mock_metrics_module.OPC_CONNECTIONS_TOTAL.labels.return_value.inc.assert_called_once()
mock_metrics_module.OPC_CONNECTION_STATUS.labels.assert_called_once_with(
pod_id=opc_manager_instance.pod_id,
server_name=opc_manager_instance.name,
server_url=opc_manager_instance.url,
)
mock_metrics_module.OPC_CONNECTION_STATUS.labels.return_value.set.assert_called_once_with(0)
mock_metrics_module.OPC_CONNECTIONS_FAILED.labels.assert_called_once_with(
pod_id=opc_manager_instance.pod_id, server_name=opc_manager_instance.name
)
mock_metrics_module.OPC_CONNECTIONS_FAILED.labels.return_value.inc.assert_called_once()
opc_manager_instance.logger.error.assert_called_once_with(
f'Failed to connect to {opc_manager_instance.name}: {simulated_error_message}'
)
@mark.asyncio @mark.asyncio
async def test_create_subscription_no_client(raw_opc_manager): async def test_create_subscription_no_client(raw_opc_manager):
@@ -260,16 +242,15 @@ async def test_create_subscription_exception_during_client_call(
opc_manager_instance.client = AsyncMock() opc_manager_instance.client = AsyncMock()
subscription_name = 'test_sub_client_error' subscription_name = 'test_sub_client_error'
simulated_period = 750
simulated_error_message = 'Falha ao criar subscrição no cliente OPC' simulated_error_message = 'Falha ao criar subscrição no cliente OPC'
opc_manager_instance.client.create_subscription.side_effect = Exception(simulated_error_message) opc_manager_instance.client.create_subscription.side_effect = Exception(simulated_error_message)
with pytest.raises(Exception, match=simulated_error_message): with pytest.raises(Exception, match=simulated_error_message):
await opc_manager_instance.create_subscription(subscription_name, period=simulated_period) await opc_manager_instance.create_subscription(subscription_name)
opc_manager_instance.client.create_subscription.assert_called_once_with( opc_manager_instance.client.create_subscription.assert_called_once_with(
simulated_period, opc_manager_instance opc_manager_instance.subscription_period_ms, opc_manager_instance
) )
opc_manager_instance.logger.error.assert_called_once_with( opc_manager_instance.logger.error.assert_called_once_with(
@@ -321,6 +302,28 @@ async def test_unsubscribe_success(opc_manager_subscribed):
assert opc_manager_subscribed.subscriptions.get('sub1') is None assert opc_manager_subscribed.subscriptions.get('sub1') is None
@mark.asyncio
async def test_disconnection_fallback_success(opc_manager):
opc_manager.client = AsyncMock()
opc_manager.client.disconnect.return_value = True
result = await opc_manager.disconnection_fallback()
assert result == []
@mark.asyncio
async def test_disconnection_fallback_fail(opc_manager):
opc_manager.client = AsyncMock()
opc_manager.client.disconnect.side_effect = Exception('Test error')
result = await opc_manager.disconnection_fallback()
assert result == [
{'attempt': 1, 'error': 'Test error', 'traceback': ANY},
{'attempt': 2, 'error': 'Test error', 'traceback': ANY},
{'attempt': 3, 'error': 'Test error', 'traceback': ANY},
{'attempt': 4, 'error': 'Test error', 'traceback': ANY},
{'attempt': 5, 'error': 'Test error', 'traceback': ANY}, ]
assert opc_manager.client.disconnect.call_count == 5
@mark.asyncio @mark.asyncio
async def test_disconnect_success(opc_manager_subscribed): async def test_disconnect_success(opc_manager_subscribed):
opc_manager_subscribed.client = MagicMock() opc_manager_subscribed.client = MagicMock()
@@ -368,9 +371,7 @@ async def test_disconnect_error(opc_manager_subscribed):
opc_manager_subscribed.subscriptions['sub1'].delete.assert_called_once() opc_manager_subscribed.subscriptions['sub1'].delete.assert_called_once()
opc_manager_subscribed.client = None opc_manager_subscribed.client = None
opc_manager_subscribed.logger.error.assert_called_once_with(
'Failed to disconnect from OPC UA server: Test error'
)
@patch('ingestor.managers.opc_manager.metrics') @patch('ingestor.managers.opc_manager.metrics')