SIENTIAPDE-1646

Harden retrain merge and OPC write against silent failures

- pandas merge now uses validate='one_to_one' so duplicated timestamps
  from wrapper.retrain fail loudly instead of silently multiplying rows
- OPC write_data raises OpcClientNotInitializedError instead of a bare
  assert when client is torn down between validation and write, so
  callers get a reported error instead of an AssertionError
- add regression test covering the torn-down client path

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 12:27:17 -03:00
parent 5c3c9292fc
commit f323da7b56
3 changed files with 35 additions and 11 deletions

View File

@@ -529,21 +529,26 @@ class MLFlow(SientiaMonitoring):
) )
# Keep heavy model fitting outside MLflow run timing. # Keep heavy model fitting outside MLflow run timing.
prediction_data = wrapper.retrain(data) prediction_data = wrapper.retrain(data).rename(columns={target: 'prediction'})
prediction_data.rename(columns={target: 'prediction'}, inplace=True)
# Merge prediction data with retrain data # Merge prediction data with retrain data. Both sides are indexed by the pivoted
# timestamp, so one row per timestamp on each side; `validate` fails loudly instead
# of silently multiplying rows if the wrapper ever returns duplicated timestamps.
evaluation_data = pd.merge( evaluation_data = pd.merge(
data, prediction_data, left_index=True, right_index=True, how='left' data,
prediction_data,
left_index=True,
right_index=True,
how='left',
validate='one_to_one',
) )
# Rename target column to "target" # Rename target column to "target", then move the index into a "timestamp" column
evaluation_data.rename(columns={target: 'target'}, inplace=True) evaluation_data = evaluation_data.rename(columns={target: 'target'})
# Reset index and put as column "timestamp"
evaluation_data['timestamp'] = evaluation_data.index evaluation_data['timestamp'] = evaluation_data.index
evaluation_data.reset_index(drop=True, inplace=True) evaluation_data = evaluation_data.reset_index(drop=True).sort_values(
evaluation_data.sort_values(by='timestamp', inplace=True, ascending=True) by='timestamp', ascending=True
)
run_name = f'{model_name}-retrain-{datetime.now().strftime("%Y%m%d%H%M%S")}' run_name = f'{model_name}-retrain-{datetime.now().strftime("%Y%m%d%H%M%S")}'

View File

@@ -843,7 +843,10 @@ class OpcRepository(SientiaMonitoring):
session_id = _opc_authentication_token_str(self.client) session_id = _opc_authentication_token_str(self.client)
try: try:
assert self.client is not None if self.client is None:
raise OpcClientNotInitializedError(
f'OPC client is not initialized for server {self.id}; cannot write {node}'
)
node_obj = self.client.get_node(node) node_obj = self.client.get_node(node)
except Exception as e: except Exception as e:
if is_reconnectable_opcua_bad(e): if is_reconnectable_opcua_bad(e):

View File

@@ -444,6 +444,22 @@ def test_write_data_get_node_failed(opc_repository):
assert error_data['attachment_content'] is not None assert error_data['attachment_content'] is not None
def test_write_data_without_client_fails_as_get_node_error(opc_repository):
"""A client torn down between validation and the write is reported, not asserted away."""
opc_repository.validate_connection = MagicMock(return_value=(True, {}))
opc_repository.client = None
is_success, error_data = opc_repository.write_data(
'ns=2;s=TestNode', 42.0, 'float', metadata['metadata']
)
assert is_success is False
assert error_data['notification_id'] == f'OPC_WRITE_GET_NODE_ERROR_{opc_repository.id}'
assert 'OPC client is not initialized' in error_data['message']
last_call = opc_repository.emit_metric_sync.call_args_list[-1]
assert last_call.args[1]['result'] == 'GetNodeError:OpcClientNotInitializedError'
def test_write_data_invalid_data_type(opc_repository, mock_client): def test_write_data_invalid_data_type(opc_repository, mock_client):
opc_repository.validate_connection = MagicMock(return_value=(True, {})) opc_repository.validate_connection = MagicMock(return_value=(True, {}))
opc_repository.client = mock_client opc_repository.client = mock_client