feat: per-sink delivery, journal forwarding, and operational API
Persistence: - one delivery state per sink, with claim, crash recovery, and index replacement - retention for delivered and dead records - LMDB writes moved off the event loop Pipeline: - one attempt per dispatch, with the next try scheduled by next_retry_at - backpressure through max_queue_size - reload re-reads YAML; SIGTERM drains and SIGHUP reloads Plugins: - journald source, enricher and filter processors, Loki sink - built-ins register when entry points are missing API and CLI: - health, metrics, per-sink queue, and dead-letter replay - startup prints the pipeline on one line Tests: - dispatch, plugins, metrics, CLI, and persistence coverage
This commit is contained in:
+32
-11
@@ -10,7 +10,7 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from daena.config import DaenaConfig, SinkConfig, SourceConfig
|
||||
from daena.config import DaenaConfig, PipelineConfig, SinkConfig, SourceConfig
|
||||
from daena.core.runtime import DaenaRuntime
|
||||
from daena.logutil import setup_logging
|
||||
from daena.persistence.models import RecordState
|
||||
@@ -20,6 +20,20 @@ setup_logging(level="error", fmt="json")
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
def _fast_pipeline(config: DaenaConfig) -> None:
|
||||
config.pipeline = PipelineConfig(batch_size=100, flush_interval=0.05, max_queue_size=10_000)
|
||||
|
||||
|
||||
async def _wait_until(predicate: Any, timeout: float = 3.0) -> bool:
|
||||
loop = asyncio.get_running_loop()
|
||||
deadline = loop.time() + timeout
|
||||
while loop.time() < deadline:
|
||||
if predicate():
|
||||
return True
|
||||
await asyncio.sleep(0.05)
|
||||
return False
|
||||
|
||||
|
||||
class _TestHTTPServer:
|
||||
"""Minimal async HTTP server to receive forwarded records."""
|
||||
|
||||
@@ -74,11 +88,12 @@ async def test_http_sink_forwards_records() -> None:
|
||||
config.data_dir = str(tmpdir)
|
||||
config.log_dir = str(tmpdir)
|
||||
config.persistence.lmdb.path = str(data_dir / "lmdb")
|
||||
_fast_pipeline(config)
|
||||
config.sources = [
|
||||
SourceConfig(
|
||||
name="test_source",
|
||||
type="dummy",
|
||||
options={"interval": 0.2, "message": "http_sink_test"},
|
||||
options={"interval": 0.05, "count": 1, "message": "http_sink_test"},
|
||||
)
|
||||
]
|
||||
config.sinks = [
|
||||
@@ -91,7 +106,7 @@ async def test_http_sink_forwards_records() -> None:
|
||||
|
||||
runtime = DaenaRuntime(config)
|
||||
await runtime.start()
|
||||
await asyncio.sleep(3.0)
|
||||
await _wait_until(lambda: len(server.received) > 0)
|
||||
await runtime.stop()
|
||||
|
||||
await server.stop()
|
||||
@@ -119,11 +134,12 @@ async def test_http_sink_permanent_failure_on_404() -> None:
|
||||
config.data_dir = str(tmpdir)
|
||||
config.log_dir = str(tmpdir)
|
||||
config.persistence.lmdb.path = str(data_dir / "lmdb")
|
||||
_fast_pipeline(config)
|
||||
config.sources = [
|
||||
SourceConfig(
|
||||
name="test_source",
|
||||
type="dummy",
|
||||
options={"interval": 0.2, "message": "http_404_test"},
|
||||
options={"interval": 0.05, "count": 1, "message": "http_404_test"},
|
||||
)
|
||||
]
|
||||
config.sinks = [
|
||||
@@ -140,11 +156,13 @@ async def test_http_sink_permanent_failure_on_404() -> None:
|
||||
|
||||
runtime = DaenaRuntime(config)
|
||||
await runtime.start()
|
||||
await asyncio.sleep(3.0)
|
||||
assert runtime.backend is not None
|
||||
backend = runtime.backend
|
||||
await _wait_until(lambda: backend.count(RecordState.dead) > 0)
|
||||
|
||||
dead = runtime.backend.count(RecordState.dead)
|
||||
failed = runtime.backend.count(RecordState.failed)
|
||||
total = runtime.backend.count()
|
||||
dead = backend.count(RecordState.dead)
|
||||
failed = backend.count(RecordState.failed)
|
||||
total = backend.count()
|
||||
|
||||
await runtime.stop()
|
||||
|
||||
@@ -168,11 +186,12 @@ async def test_http_sink_delivery_result() -> None:
|
||||
config.data_dir = str(tmpdir)
|
||||
config.log_dir = str(tmpdir)
|
||||
config.persistence.lmdb.path = str(data_dir / "lmdb")
|
||||
_fast_pipeline(config)
|
||||
config.sources = [
|
||||
SourceConfig(
|
||||
name="test_source",
|
||||
type="dummy",
|
||||
options={"interval": 0.2, "message": "http_delivery_test"},
|
||||
options={"interval": 0.05, "count": 1, "message": "http_delivery_test"},
|
||||
)
|
||||
]
|
||||
config.sinks = [
|
||||
@@ -185,9 +204,11 @@ async def test_http_sink_delivery_result() -> None:
|
||||
|
||||
runtime = DaenaRuntime(config)
|
||||
await runtime.start()
|
||||
await asyncio.sleep(3.0)
|
||||
assert runtime.backend is not None
|
||||
backend = runtime.backend
|
||||
await _wait_until(lambda: backend.count(RecordState.delivered) > 0)
|
||||
|
||||
delivered = runtime.backend.count(RecordState.delivered)
|
||||
delivered = backend.count(RecordState.delivered)
|
||||
await runtime.stop()
|
||||
|
||||
await server.stop()
|
||||
|
||||
Reference in New Issue
Block a user