fix(generator): устранён флаки runtime-гейта стыка backfill/live
- Зачем:
- гейт generated-history-runtime-check падал через раз: docker compose
stop убивал генератор SIGKILL'ом посреди batch (SIGTERM не ловился),
непарные строки маскировались под «смену фактуры» (задача 20).
- Что:
- генератор грациозно завершается по SIGTERM: текущий batch дописывается
во все топики с flush и записью history; compose даёт минуту grace.
- runtime-check ждёт пересекающий визит в STG (предусловие проверки),
seam-check получил precheck непарных live-строк; в STG-запросах
закреплён 'UTC' против сдвига наивных меток в поясе сервера.
- контрактные тесты усилены, задача 20 закрыта, блокер задачи 13 снят.
- Проверка:
- тесты: 192 passed (generator), 21 passed (контракты);
- стенд: 3 подряд зелёных make generated-history-runtime-check;
красный сценарий (искажение referer_url пересекающего визита) валит
гейт прежним сообщением при нулевых непарных счётчиках.
This commit is contained in:
@@ -6,6 +6,7 @@ import logging
|
||||
import random
|
||||
import hashlib
|
||||
import json
|
||||
import signal
|
||||
from dataclasses import replace
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from time import sleep as real_sleep
|
||||
@@ -21,6 +22,7 @@ from generator import (
|
||||
GeneratorState,
|
||||
KafkaBatchHistory,
|
||||
TickStreamGenerator,
|
||||
main,
|
||||
)
|
||||
from clickstream_generator.state import UnsupportedStateVersionError
|
||||
|
||||
@@ -210,8 +212,9 @@ class TestGeneratorServiceSteadyStream:
|
||||
service._running = False
|
||||
|
||||
with caplog.at_level(logging.INFO, logger="generator"), \
|
||||
patch(
|
||||
"clickstream_generator.service.time.sleep",
|
||||
patch.object(
|
||||
service._shutdown_event,
|
||||
"wait",
|
||||
side_effect=stop_after_first_tick,
|
||||
):
|
||||
service._main_loop()
|
||||
@@ -221,6 +224,77 @@ class TestGeneratorServiceSteadyStream:
|
||||
assert "completed:" not in caplog.text
|
||||
assert "sent" not in caplog.text
|
||||
|
||||
def test_sigterm_during_publish_finishes_current_batch(self, base_config):
|
||||
"""SIGTERM посреди публикации не обрывает связанные топики batch."""
|
||||
config = replace(
|
||||
base_config,
|
||||
tick_seconds=60,
|
||||
lambda_base_per_min=600,
|
||||
jitter_pct=0,
|
||||
min_events_per_tick=1,
|
||||
max_events_per_tick=1000,
|
||||
max_session_events=3,
|
||||
metrics_enabled=False,
|
||||
state_enabled=False,
|
||||
)
|
||||
service = GeneratorService(config)
|
||||
publisher = MagicMock()
|
||||
history = MagicMock()
|
||||
published_topics = []
|
||||
|
||||
def publish_and_request_stop(topic, events):
|
||||
published_topics.append(topic)
|
||||
if len(published_topics) == 1:
|
||||
service.request_stop(signal.SIGTERM, None)
|
||||
return len(events), 0
|
||||
|
||||
publisher.publish.side_effect = publish_and_request_stop
|
||||
|
||||
with patch(
|
||||
"clickstream_generator.service.start_http_server",
|
||||
side_effect=AssertionError("тест не должен открывать порт метрик"),
|
||||
), \
|
||||
patch("clickstream_generator.service.ensure_topics"), \
|
||||
patch(
|
||||
"clickstream_generator.service.KafkaPublisher",
|
||||
return_value=publisher,
|
||||
), \
|
||||
patch(
|
||||
"clickstream_generator.service.KafkaBatchHistory",
|
||||
return_value=history,
|
||||
), \
|
||||
patch.object(
|
||||
service.generator,
|
||||
"_calculate_events_count",
|
||||
return_value=int(EXPECTED_VISIT_EVENTS),
|
||||
):
|
||||
service.start()
|
||||
|
||||
assert published_topics == [
|
||||
"browser_events",
|
||||
"location_events",
|
||||
"device_events",
|
||||
"geo_events",
|
||||
]
|
||||
publisher.flush.assert_called_once_with()
|
||||
history.add.assert_called_once()
|
||||
history.flush.assert_called_once_with()
|
||||
publisher.close.assert_called_once_with()
|
||||
history.close.assert_called_once_with()
|
||||
assert service._tick == 1
|
||||
|
||||
def test_main_connects_sigterm_to_graceful_stop(self, base_config):
|
||||
"""Точка входа передаёт SIGTERM сервису как штатный запрос остановки."""
|
||||
service = MagicMock()
|
||||
|
||||
with patch("clickstream_generator.service.Config", return_value=base_config), \
|
||||
patch("clickstream_generator.service.GeneratorService", return_value=service), \
|
||||
patch("clickstream_generator.service.signal.signal") as register_signal:
|
||||
main()
|
||||
|
||||
register_signal.assert_called_once_with(signal.SIGTERM, service.request_stop)
|
||||
service.start.assert_called_once_with()
|
||||
|
||||
def _run_service_ticks(self, config, wall_now: datetime, ticks_count: int):
|
||||
service = GeneratorService(config)
|
||||
service.publisher = MagicMock()
|
||||
@@ -256,7 +330,11 @@ class TestGeneratorServiceSteadyStream:
|
||||
"_calculate_events_count",
|
||||
side_effect=calculate_events_count,
|
||||
), \
|
||||
patch("clickstream_generator.service.time.sleep", side_effect=stop_after_tick):
|
||||
patch.object(
|
||||
service._shutdown_event,
|
||||
"wait",
|
||||
side_effect=stop_after_tick,
|
||||
):
|
||||
service._main_loop()
|
||||
|
||||
published = {}
|
||||
@@ -294,7 +372,7 @@ class TestGeneratorServiceSteadyStream:
|
||||
service.generator,
|
||||
"_visit_pause_seconds",
|
||||
return_value=0.05,
|
||||
), patch("clickstream_generator.service.time.sleep") as sleep_mock:
|
||||
), patch.object(service._shutdown_event, "wait") as sleep_mock:
|
||||
sleep_mock.side_effect = stop_after_second_tick
|
||||
|
||||
service._main_loop()
|
||||
|
||||
Reference in New Issue
Block a user