SIENTIAPDE-2212

SIENTIAPDE-2212-drift-janela-e-chunk-period: Remove default 1-minute margin from drift window calculation. The default interval now directly matches the frequency, ensuring consecutive windows do not overlap.
This commit is contained in:
2026-10-27 16:42:48 -03:00
parent 9e160655f2
commit 2d794d02b2
4 changed files with 14 additions and 16 deletions

View File

@@ -59,7 +59,7 @@ back to `frequency`.
### 1.8.1 Drift window and chunk_period ### 1.8.1 Drift window and chunk_period
Two `drift` pipelines on `frequency='1m'`: one without `interval_minutes` and with an invalid Two `drift` pipelines on `frequency='1m'`: one without `interval_minutes` and with an invalid
`chunk_period='hour'` → workflow input `interval=2` (frequency + 1-minute margin), `chunk_period='hour'` → workflow input `interval=1` (frequency, no overlap),
`chunk_period='min'` and exactly one `DRIFT_CHUNK_PERIOD_INVALID` notification; one with `chunk_period='min'` and exactly one `DRIFT_CHUNK_PERIOD_INVALID` notification; one with
`interval_minutes=60` and `chunk_period='s'` → both forwarded unchanged, no warning `interval_minutes=60` and `chunk_period='s'` → both forwarded unchanged, no warning
(SIENTIAPDE-2212). (SIENTIAPDE-2212).

View File

@@ -336,7 +336,7 @@ async def test_scenario_1_8_1_drift_window_and_chunk_period(
redis_client: Redis, redis_client: Redis,
notification_handler, notification_handler,
): ):
"""1.8.1: Drift window defaults to frequency + 1 min margin; invalid chunk_period warns and falls back.""" """1.8.1: Drift window defaults to frequency; invalid chunk_period warns and falls back."""
scenario = load_scenario_input('orchestrator_drift_window') scenario = load_scenario_input('orchestrator_drift_window')
_apply_orchestrator_seeds(scenario, mongo_uri, redis_client) _apply_orchestrator_seeds(scenario, mongo_uri, redis_client)
@@ -348,7 +348,7 @@ async def test_scenario_1_8_1_drift_window_and_chunk_period(
) )
default = await _schedule_input(temporal_host, 'model-dr-drift-default') default = await _schedule_input(temporal_host, 'model-dr-drift-default')
assert default['interval'] == 2 assert default['interval'] == 1
assert default['chunk_period'] == 'min' assert default['chunk_period'] == 'min'
assert default['chunk_period_configured'] == 'hour' assert default['chunk_period_configured'] == 'hour'

View File

@@ -47,14 +47,13 @@ def common_config(config: dict[str, Any]):
# Kept in sync by hand with the `chunk_period` validation in # Kept in sync by hand with the `chunk_period` validation in
# sientia-dataops-laborious/laborious/activities/model_metrics.py (no shared contract). # sientia-dataops-laborious/laborious/activities/model_metrics.py (no shared contract).
DRIFT_CHUNK_PERIODS = ('min', 's') DRIFT_CHUNK_PERIODS = ('min', 's')
# Re-reads the tail of the previous window so rows ingested up to 1 minute late still count.
DRIFT_WINDOW_MARGIN_MINUTES = 1
def _drift_interval_minutes(config: dict[str, Any], frequency: Any) -> int: def _drift_interval_minutes(config: dict[str, Any], frequency: Any) -> int:
""" """
Drift window in minutes: `interval_minutes` as configured (no margin), otherwise the Drift window in minutes: `interval_minutes` as configured, otherwise the schedule frequency
schedule frequency rounded up to whole minutes (min 1) plus DRIFT_WINDOW_MARGIN_MINUTES. rounded up to whole minutes (min 1). Consecutive default windows do not overlap, so each
drift chunk is stored once.
""" """
if 'interval_minutes' in config: if 'interval_minutes' in config:
return config['interval_minutes'] return config['interval_minutes']
@@ -63,7 +62,7 @@ def _drift_interval_minutes(config: dict[str, Any], frequency: Any) -> int:
except ValueError: except ValueError:
# create_schedules fails this schedule on its own; don't kill the tick for every model. # create_schedules fails this schedule on its own; don't kill the tick for every model.
return 60 return 60
return max(1, ceil(seconds / 60)) + DRIFT_WINDOW_MARGIN_MINUTES return max(1, ceil(seconds / 60))
def drift(config: dict[str, Any]): def drift(config: dict[str, Any]):
@@ -76,10 +75,9 @@ def drift(config: dict[str, Any]):
Args: Args:
config (dict[str, Any]): Pipeline configuration containing: config (dict[str, Any]): Pipeline configuration containing:
- interval_minutes (int, optional): Time window for data comparison in minutes, - interval_minutes (int, optional): Time window for data comparison in minutes,
used as-is (no margin). Default: `frequency` rounded up to whole minutes used as-is. Default: `frequency` rounded up to whole minutes (min 1), so
(min 1) plus a 1-minute margin for late data (2 with the default '1m'). consecutive windows do not overlap (1 with the default '1m');
Pipelines whose ingestion lag exceeds 1 minute should set it explicitly `interval_minutes: 60` keeps the legacy window.
(frequency + lag); `interval_minutes: 60` keeps the legacy window.
- chunk_period (str, optional): Drift bucket size, 'min' or 's' (default: 'min'). - chunk_period (str, optional): Drift bucket size, 'min' or 's' (default: 'min').
Any other value falls back to 'min' and create_schedule_config warns. Any other value falls back to 'min' and create_schedule_config warns.
- drift_metrics (list[str], optional): Statistical metrics to compute (default: - drift_metrics (list[str], optional): Statistical metrics to compute (default:

View File

@@ -99,22 +99,22 @@ def _drift_config(**fields):
} }
def test_drift_default_window_is_frequency_plus_margin(): def test_drift_default_window_is_frequency():
result = drift(_drift_config()) result = drift(_drift_config())
assert result['frequency'] == '1m' assert result['frequency'] == '1m'
assert result['interval'] == 2 assert result['interval'] == 1
@mark.parametrize( @mark.parametrize(
('frequency', 'interval'), ('frequency', 'interval'),
[('1h', 61), ('30s', 2), ('90s', 3)], [('1h', 60), ('30s', 1), ('90s', 2)],
) )
def test_drift_window_derived_from_frequency(frequency, interval): def test_drift_window_derived_from_frequency(frequency, interval):
assert drift(_drift_config(frequency=frequency))['interval'] == interval assert drift(_drift_config(frequency=frequency))['interval'] == interval
def test_drift_explicit_interval_minutes_wins_without_margin(): def test_drift_explicit_interval_minutes_wins():
result = drift(_drift_config(frequency='1m', interval_minutes=60)) result = drift(_drift_config(frequency='1m', interval_minutes=60))
assert result['interval'] == 60 assert result['interval'] == 60