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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user