SIENTIAPDE-2212

SIENTIAPDE-2212-drift-janela-e-chunk-period: Add configurable chunk period and dynamic window calculation for drift, with warnings for invalid periods.
This commit is contained in:
2026-10-27 16:19:51 -03:00
parent 090456e11f
commit d1786085d0
7 changed files with 235 additions and 7 deletions

View File

@@ -1073,3 +1073,46 @@ def test_create_schedule_config_no_warning_at_or_above_floor(formatters):
formatters.create_schedule_config(_schedule_config_with_frequency_alert('5m'))
formatters.send_notification.assert_not_called()
def _drift_schedule_config(chunk_period_configured):
return {
'metadata': {},
'current_schedule_config': {'laborious': {}},
'schedule_config': {
'laborious': {
'm1-drift-1m': {
'workflow_type': 'drift',
'model_id': 'm1',
'model_name': 'm1',
'frequency': '1m',
'chunk_period': 'min',
'chunk_period_configured': chunk_period_configured,
'updated_at': '2026-10-27',
}
},
'scouter': {},
},
}
def test_create_schedule_config_warns_on_invalid_chunk_period(formatters):
result = formatters.create_schedule_config(_drift_schedule_config('hour'))
assert 'm1-drift-1m' in result['to_create']['laborious']
formatters.send_notification.assert_called_once()
kwargs = formatters.send_notification.call_args.kwargs
assert kwargs['notification_id'] == 'DRIFT_CHUNK_PERIOD_INVALID'
assert kwargs['level'] == NotificationLevel.WARNING
assert kwargs['metadata']['schedule_name'] == 'm1-drift-1m'
assert kwargs['metadata']['model_id'] == 'm1'
assert kwargs['metadata']['model_name'] == 'm1'
def test_create_schedule_config_no_chunk_period_warning_when_unchanged(formatters):
input_data = _drift_schedule_config('hour')
input_data['current_schedule_config'] = {'laborious': {'m1-drift-1m': '2026-10-27'}}
formatters.create_schedule_config(input_data)
formatters.send_notification.assert_not_called()

View File

@@ -3,6 +3,7 @@ from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
from pytest import fixture, mark
from sientia_do.notifications.models import NotificationLevel
from temporalio.client import ScheduleOverlapPolicy
from orchestrator.activities.temporal_manager import TemporalManager
from orchestrator.utils.converters import parse_frequency
@@ -684,3 +685,41 @@ async def test_create_schedules_tenant_runtime_queue(
task_timeout=timedelta(seconds=300),
typed_search_attributes=_mock_typed_search_attributes.return_value,
)
@mark.asyncio
@patch('orchestrator.activities.temporal_manager.Schedule')
@patch('orchestrator.activities.temporal_manager.ScheduleActionStartWorkflow')
@patch('orchestrator.activities.temporal_manager.ScheduleIntervalSpec')
@patch('orchestrator.activities.temporal_manager.ScheduleSpec')
@patch('orchestrator.activities.temporal_manager.TypedSearchAttributes')
@patch('orchestrator.activities.temporal_manager.SearchAttributePair')
async def test_create_schedules_drift_keeps_default_skip_overlap(
_mock_search_attribute_pair,
_mock_typed_search_attributes,
_mock_schedule_spec,
_mock_schedule_interval_spec,
_mock_schedule_action_start_workflow,
mock_schedule,
temporal_manager,
):
# The laborious DriftAnalysis cache assumes no two drift runs of a model overlap.
input_data = {
'schedules': {
'laborious': {
'm1-drift-1m': {
'model_id': 'm1',
'model_name': 'm1',
'workflow_type': 'drift',
'frequency': '1m',
}
}
}
}
temporal_manager.temporal_clients['laborious'].create_schedule = AsyncMock()
await temporal_manager.create_schedules(input_data)
mock_schedule.assert_called_once()
policy = mock_schedule.call_args.kwargs.get('policy')
assert policy is None or policy.overlap == ScheduleOverlapPolicy.SKIP

View File

@@ -5,6 +5,7 @@ from pytest import mark
from orchestrator.utils.orchestrator_functions import (
base_scouter,
build_tag_config,
chunk_period_warning,
common_config,
drift,
frequency_alert_warning,
@@ -79,6 +80,7 @@ def test_drift():
'source_table_name': 'laborious_data',
'target_table_name': 'drift_metrics',
'interval': 120,
'chunk_period': 'min',
'drift_metrics': ['kolmogorov_smirnov', 'jensen_shannon'],
'execution_timeout_seconds': 300,
'task_timeout_seconds': 300,
@@ -87,6 +89,79 @@ def test_drift():
assert result == expected
def _drift_config(**fields):
return {
'workflow_type': 'drift',
'schedule_name': 'm1-drift',
'model_id': 'm1',
'model': {'name': 'm1'},
**fields,
}
def test_drift_default_window_is_frequency_plus_margin():
result = drift(_drift_config())
assert result['frequency'] == '1m'
assert result['interval'] == 2
@mark.parametrize(
('frequency', 'interval'),
[('1h', 61), ('30s', 2), ('90s', 3)],
)
def test_drift_window_derived_from_frequency(frequency, interval):
assert drift(_drift_config(frequency=frequency))['interval'] == interval
def test_drift_explicit_interval_minutes_wins_without_margin():
result = drift(_drift_config(frequency='1m', interval_minutes=60))
assert result['interval'] == 60
def test_drift_invalid_frequency_falls_back_to_legacy_window():
result = drift(_drift_config(frequency='abc'))
assert result['frequency'] == 'abc'
assert result['interval'] == 60
def test_drift_chunk_period_defaults_to_min():
result = drift(_drift_config())
assert result['chunk_period'] == 'min'
assert 'chunk_period_configured' not in result
def test_drift_chunk_period_seconds_kept():
result = drift(_drift_config(chunk_period='s'))
assert result['chunk_period'] == 's'
assert 'chunk_period_configured' not in result
@mark.parametrize('chunk_period', ['hour', 5])
def test_drift_invalid_chunk_period_falls_back_to_min(chunk_period):
result = drift(_drift_config(chunk_period=chunk_period))
assert result['chunk_period'] == 'min'
assert result['chunk_period_configured'] == chunk_period
@mark.parametrize('chunk_period', [None, 'min', 's'])
def test_chunk_period_warning_none_when_absent_or_valid(chunk_period):
assert chunk_period_warning(chunk_period) is None
@mark.parametrize('chunk_period', ['hour', 5])
def test_chunk_period_warning_message_when_invalid(chunk_period):
warning = chunk_period_warning(chunk_period)
assert warning is not None
assert repr(chunk_period) in warning
def test_simple_metrics():
config = {
'workflow_type': 'simple_metrics',