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

@@ -19,6 +19,7 @@ with workflow.unsafe.imports_passed_through():
from orchestrator.utils.orchestrator_functions import (
build_tag_config,
chunk_period_warning,
drift,
frequency_alert_warning,
gather_read_tags,
@@ -371,7 +372,9 @@ class Formatters(SientiaMonitoring):
configuration to determine which schedules need to be created, updated, or deleted.
Uses timestamp comparison to identify schedules that have changed.
Also sends a SIMPLE_METRICS_FREQUENCY_ALERT_BELOW_FLOOR WARNING for laborious schedules
whose frequency_alert is below 1m (the value is kept) or unparseable (it is ignored).
whose frequency_alert is below 1m (the value is kept) or unparseable (it is ignored),
and a DRIFT_CHUNK_PERIOD_INVALID WARNING for drift schedules whose configured
chunk_period is not supported by laborious (replaced by 'min').
Args:
input_data (dict[str, Any]): The input data containing:
@@ -430,6 +433,20 @@ class Formatters(SientiaMonitoring):
block='create_schedule_config',
level=NotificationLevel.WARNING,
)
warning = chunk_period_warning(schedule.get('chunk_period_configured'))
if warning:
self.send_notification(
metadata={
**metadata,
'schedule_name': schedule_name,
'model_id': schedule.get('model_id'),
'model_name': schedule.get('model_name'),
},
notification_id='DRIFT_CHUNK_PERIOD_INVALID',
message=warning,
block='create_schedule_config',
level=NotificationLevel.WARNING,
)
for namespace, schedules in current_schedule_config.items():
for schedule_name in schedules:

View File

@@ -220,6 +220,9 @@ class TemporalManager(SientiaMonitoring):
task_queue_name = self._build_task_queue_name(workflow_type, schedule)
schedule['task_queue'] = task_queue_name
# No `policy`: schedules rely on Temporal's default SKIP overlap. The
# laborious `DriftAnalysis` cache assumes at most one running drift
# workflow per model; revisit its locking if a policy is ever added here.
await client.create_schedule(
schedule_name,
Schedule(

View File

@@ -1,3 +1,4 @@
from math import ceil
from typing import Any
from orchestrator.utils.converters import parse_frequency
@@ -43,6 +44,28 @@ def common_config(config: dict[str, Any]):
}
# Kept in sync by hand with the `chunk_period` validation in
# sientia-dataops-laborious/laborious/activities/model_metrics.py (no shared contract).
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:
"""
Drift window in minutes: `interval_minutes` as configured (no margin), otherwise the
schedule frequency rounded up to whole minutes (min 1) plus DRIFT_WINDOW_MARGIN_MINUTES.
"""
if 'interval_minutes' in config:
return config['interval_minutes']
try:
seconds = parse_frequency(str(frequency))
except ValueError:
# create_schedules fails this schedule on its own; don't kill the tick for every model.
return 60
return max(1, ceil(seconds / 60)) + DRIFT_WINDOW_MARGIN_MINUTES
def drift(config: dict[str, Any]):
"""
Build drift configuration from pipeline config.
@@ -52,25 +75,52 @@ def drift(config: dict[str, Any]):
Args:
config (dict[str, Any]): Pipeline configuration containing:
- interval_minutes (int, optional): Time window for data comparison in minutes (default: 60)
- interval_minutes (int, optional): Time window for data comparison in minutes,
used as-is (no margin). Default: `frequency` rounded up to whole minutes
(min 1) plus a 1-minute margin for late data (2 with the default '1m').
Pipelines whose ingestion lag exceeds 1 minute should set it explicitly
(frequency + lag); `interval_minutes: 60` keeps the legacy window.
- chunk_period (str, optional): Drift bucket size, 'min' or 's' (default: 'min').
Any other value falls back to 'min' and create_schedule_config warns.
- drift_metrics (list[str], optional): Statistical metrics to compute (default:
['kolmogorov_smirnov', 'jensen_shannon', 'wasserstein'])
- Additional fields from common_config
Returns:
dict[str, Any]: Drift detection configuration with source/target tables,
time interval, and metrics specifications. Results stored in 'drift_metrics' table
time interval, chunk period and metrics specifications. Results stored in
'drift_metrics' table
"""
return {
**common_config(config),
base = common_config(config)
configured_chunk_period = config.get('chunk_period')
chunk_period_valid = configured_chunk_period in DRIFT_CHUNK_PERIODS
output = {
**base,
'schema': 'sientia_data',
'source_table_name': 'laborious_data',
'target_table_name': 'drift_metrics',
'interval': config.get('interval_minutes', 60),
'interval': _drift_interval_minutes(config, base['frequency']),
'chunk_period': configured_chunk_period if chunk_period_valid else 'min',
'drift_metrics': config.get(
'drift_metrics', ['kolmogorov_smirnov', 'jensen_shannon', 'wasserstein']
),
}
if configured_chunk_period is not None and not chunk_period_valid:
# Forwarded so create_schedule_config can warn without re-reading the pipeline.
output['chunk_period_configured'] = configured_chunk_period
return output
def chunk_period_warning(chunk_period: Any) -> str | None:
"""
Warning message when a configured drift `chunk_period` is not supported by laborious,
otherwise None. Warn only: `drift()` already replaced it with 'min'.
"""
if chunk_period is None or chunk_period in DRIFT_CHUNK_PERIODS:
return None
return (
f"chunk_period={chunk_period!r} is not one of {DRIFT_CHUNK_PERIODS}; falling back to 'min'"
)
def simple_metrics(config: dict[str, Any]):