SIENTIAPDE-1811
Enhance OPC UA communication and metrics tracking - Updated README.md to include new OPC UA Communication section and detailed metrics for session and write diagnostics. - Added new metrics in laborious/metrics.py for tracking OPC UA session states and write attempts. - Refactored OPC activity in laborious/activities/opc.py to handle session errors and improve error reporting. - Updated e2e tests to cover new scenarios for OPC session/channel errors and reconnect handling. - Modified .gitignore to include relatorio files and mlruns directory. - Added ipykernel to requirements-dev.txt for Jupyter notebook support.
This commit is contained in:
@@ -253,6 +253,7 @@ def test_disconnect(opc_repository, mock_client):
|
||||
|
||||
opc_repository._disconnection_fallback.assert_called_once()
|
||||
assert opc_repository.client is None
|
||||
assert opc_repository._allow_reconnect is False
|
||||
|
||||
|
||||
def test_disconnect_no_client(opc_repository):
|
||||
@@ -323,12 +324,19 @@ def test_write_data_validate_connection_do_nothing(opc_repository):
|
||||
def test_write_data_validate_connection_failed(opc_repository):
|
||||
opc_repository.validate_connection = MagicMock(return_value=(False, {}))
|
||||
opc_repository.client = MagicMock()
|
||||
opc_repository._start_reconnect = MagicMock()
|
||||
|
||||
result = opc_repository.write_data('ns=2;s=TestNode', 42.0, 'float', metadata['metadata'])
|
||||
is_success, error_data = opc_repository.write_data(
|
||||
'ns=2;s=TestNode', 42.0, 'float', metadata['metadata']
|
||||
)
|
||||
|
||||
opc_repository.validate_connection.assert_called_once()
|
||||
opc_repository.client.get_node.assert_not_called()
|
||||
assert result == (False, {})
|
||||
opc_repository._start_reconnect.assert_called_once()
|
||||
assert opc_repository._start_reconnect.call_args.args[0] == 'ProtocolClosed'
|
||||
assert is_success is False
|
||||
assert error_data['opc_error_kind'] == 'connection_lost'
|
||||
assert error_data['opc_status'] == 'ProtocolClosed'
|
||||
|
||||
|
||||
def test_write_data_get_node_failed(opc_repository):
|
||||
@@ -424,7 +432,7 @@ def test_is_reconnectable_opcua_bad():
|
||||
def test_write_data_bad_session_id_invalid_schedules_reconnect(opc_repository, mock_client):
|
||||
opc_repository.validate_connection = MagicMock(return_value=(True, {}))
|
||||
opc_repository.client = mock_client
|
||||
opc_repository._start_reconnect_on_bad = MagicMock()
|
||||
opc_repository._start_reconnect = MagicMock()
|
||||
mock_node = MagicMock()
|
||||
mock_client.get_node = MagicMock(return_value=mock_node)
|
||||
mock_node.write_value.side_effect = BadSessionIdInvalid()
|
||||
@@ -434,7 +442,7 @@ def test_write_data_bad_session_id_invalid_schedules_reconnect(opc_repository, m
|
||||
)
|
||||
|
||||
mock_node.write_value.assert_called_once()
|
||||
opc_repository._start_reconnect_on_bad.assert_called_once()
|
||||
opc_repository._start_reconnect.assert_called_once()
|
||||
assert is_success is False
|
||||
assert error_data['opc_error_kind'] == 'session_bad'
|
||||
assert error_data['opc_status'] == 'BadSessionIdInvalid'
|
||||
@@ -455,15 +463,83 @@ def test_write_data_reconnect_in_progress_immediate(opc_repository):
|
||||
assert error_data['opc_error_kind'] == 'reconnect_in_progress'
|
||||
|
||||
|
||||
def test_start_reconnect_on_bad_skips_within_interval(opc_repository):
|
||||
def test_start_reconnect_skips_within_interval(opc_repository):
|
||||
opc_repository.last_reconnection_time = datetime.now()
|
||||
opc_repository.reconnection_interval = 3600
|
||||
|
||||
opc_repository._start_reconnect_on_bad('BadSessionIdInvalid', 'tok')
|
||||
opc_repository._start_reconnect('BadSessionIdInvalid', 'tok')
|
||||
|
||||
assert opc_repository._reconnect_thread is None
|
||||
|
||||
|
||||
def test_write_data_protocol_closed_schedules_reconnect(opc_repository):
|
||||
opc_repository.client = MagicMock()
|
||||
opc_repository.client.aio_obj.uaclient.protocol = MagicMock(state='closed')
|
||||
opc_repository._start_reconnect = MagicMock()
|
||||
|
||||
is_success, error_data = opc_repository.write_data(
|
||||
'ns=2;s=TestNode', 42.0, 'float', metadata['metadata']
|
||||
)
|
||||
|
||||
opc_repository._start_reconnect.assert_called_once()
|
||||
assert opc_repository._start_reconnect.call_args.args[0] == 'ProtocolClosed'
|
||||
assert is_success is False
|
||||
assert error_data['opc_error_kind'] == 'connection_lost'
|
||||
assert error_data['opc_status'] == 'ProtocolClosed'
|
||||
|
||||
|
||||
def test_write_data_protocol_closed_skips_reconnect_within_interval(opc_repository):
|
||||
opc_repository.client = MagicMock()
|
||||
opc_repository.client.aio_obj.uaclient.protocol = MagicMock(state='closed')
|
||||
opc_repository.last_reconnection_time = datetime.now()
|
||||
opc_repository.reconnection_interval = 3600
|
||||
|
||||
is_success, error_data = opc_repository.write_data(
|
||||
'ns=2;s=TestNode', 42.0, 'float', metadata['metadata']
|
||||
)
|
||||
|
||||
assert opc_repository._reconnect_thread is None
|
||||
assert is_success is False
|
||||
assert error_data['opc_error_kind'] == 'connection_lost'
|
||||
|
||||
|
||||
def test_write_data_after_failed_reconnect_schedules_again(opc_repository):
|
||||
opc_repository._session_ready.clear()
|
||||
opc_repository.reconnection_interval = 0
|
||||
opc_repository.last_reconnection_time = None
|
||||
opc_repository._reconnect_locked = MagicMock(
|
||||
return_value=(False, {'message': 'connect failed'})
|
||||
)
|
||||
|
||||
opc_repository.write_data('ns=2;s=TestNode', 42.0, 'float', metadata['metadata'])
|
||||
if opc_repository._reconnect_thread is not None:
|
||||
opc_repository._reconnect_thread.join(timeout=2)
|
||||
assert opc_repository._reconnect_locked.call_count == 1
|
||||
assert not opc_repository._reconnect_thread_in_progress()
|
||||
|
||||
opc_repository.write_data('ns=2;s=TestNode', 42.0, 'float', metadata['metadata'])
|
||||
if opc_repository._reconnect_thread is not None:
|
||||
opc_repository._reconnect_thread.join(timeout=2)
|
||||
assert opc_repository._reconnect_locked.call_count == 2
|
||||
|
||||
|
||||
def test_write_data_after_disconnect_does_not_schedule_reconnect(opc_repository, mock_client):
|
||||
opc_repository.client = mock_client
|
||||
proto = MagicMock()
|
||||
proto.state = 'closed'
|
||||
mock_client.aio_obj.uaclient.protocol = proto
|
||||
opc_repository._disconnection_fallback = MagicMock(return_value=[])
|
||||
opc_repository.disconnect()
|
||||
|
||||
is_success, error_data = opc_repository.write_data(
|
||||
'ns=2;s=TestNode', 42.0, 'float', metadata['metadata']
|
||||
)
|
||||
|
||||
assert opc_repository._reconnect_thread is None
|
||||
assert is_success is False
|
||||
assert error_data['opc_error_kind'] == 'connection_lost'
|
||||
|
||||
|
||||
def test_parallel_bad_writes_single_reconnect_task(opc_repository, mock_client):
|
||||
opc_repository.validate_connection = MagicMock(return_value=(True, {}))
|
||||
opc_repository.client = mock_client
|
||||
@@ -472,7 +548,7 @@ def test_parallel_bad_writes_single_reconnect_task(opc_repository, mock_client):
|
||||
mock_node = MagicMock()
|
||||
mock_client.get_node = MagicMock(return_value=mock_node)
|
||||
mock_node.write_value.side_effect = BadSessionIdInvalid()
|
||||
opc_repository._start_reconnect_on_bad = MagicMock()
|
||||
opc_repository._start_reconnect = MagicMock()
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
|
||||
futures = [
|
||||
@@ -487,7 +563,7 @@ def test_parallel_bad_writes_single_reconnect_task(opc_repository, mock_client):
|
||||
]
|
||||
results = [future.result() for future in futures]
|
||||
|
||||
assert 1 <= opc_repository._start_reconnect_on_bad.call_count <= 2
|
||||
assert 1 <= opc_repository._start_reconnect.call_count <= 2
|
||||
assert mock_node.write_value.call_count == 2
|
||||
error_kinds = [r[1].get('opc_error_kind') for r in results]
|
||||
assert error_kinds.count('session_bad') >= 1
|
||||
|
||||
Reference in New Issue
Block a user