feat(ttp): E.3.6 DatabaseRuleStore — ttp_rule/ttp_rule_state + master sync
Implements the DB-backed rule store body left empty at contract phase:
load_compiled reads from ttp_rule + ttp_rule_state; get_state /
set_state hit ttp_rule_state with the same expires_at auto-revert and
bus-event semantics as the FS backend; subscribe_changes returns a
per-subscriber queue. State persists across process restarts — the
swarm property the FS backend deliberately doesn't have.
Also lands two swarm-mode helpers:
- sync_from_filesystem(fs_store) — master-side, subscribes to a
FilesystemRuleStore and projects each RuleChange onto a ttp_rule
upsert/delete.
- tail_db(poll_interval) — worker-side, watermark poll over
ttp_rule.updated_at; emits RuleChange("definition", ...) for each
row that moved.
Why: swarm mode needs rule definitions and operator state to
propagate across hosts. The filesystem backend (E.3.5) was the
single-host-dev variant; this one survives restart and serves N
workers from a shared DB.
Notes:
- DatabaseRuleStore() with no args lazy-inits an in-memory SQLite
repo so the conformance fixture works without test plumbing. In
production the worker bootstrap (E.3.14) passes an explicit repo.
- The conftest.py rule_store fixture became async (pytest_asyncio),
per-backend creates/initializes a SQLite repo for the DB run.
- Adds a `seed_rule(store, rule_id, yaml)` helper to bridge backend
semantics: drop a YAML file (FS) vs insert a ttp_rule row (DB).
Used by the parametrized load_compiled conformance test.
- Late-bound _tracer() in both backends (was module-level get_tracer
binding) so test_tracing's monkeypatch of decnet.telemetry.get_tracer
actually affects span output.
xfails flipped: tests/ttp/store/test_database.py set_state-writes-to-
ttp_rule_state + filesystem-to-DB sync; tests/ttp/store/test_conformance.py
DB-side load_compiled / set_state isolation / round-trip / per-rule
fan-out / expired-state revert / set_state failure / get_state default
(was xfail-only-on-DB); tests/ttp/test_tracing.py set_state span
hierarchy.
Tests: 208 passed, 25 xfailed (gated on E.3.7 + lifters).
mypy: clean on all touched files.
This commit is contained in:
@@ -159,16 +159,38 @@ def test_rule_fire_spans_carry_rule_and_technique_attrs(
|
||||
# ── set_state span hierarchy (xfail until E.3.5/E.3.6) ──────────────
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
strict=True,
|
||||
reason="impl phase E.3.5/E.3.6 — set_state() span hierarchy lands "
|
||||
"with the rule-store implementations",
|
||||
)
|
||||
def test_set_state_span_hierarchy(span_exporter: tuple[InMemorySpanExporter, TracerProvider]) -> None:
|
||||
def test_set_state_span_hierarchy(
|
||||
span_exporter: tuple[InMemorySpanExporter, TracerProvider],
|
||||
) -> None:
|
||||
"""``RuleStore.set_state`` produces a ``ttp.rule.state.change``
|
||||
parent with ``ttp.store.write_state`` + ``ttp.rule.publish``
|
||||
children — operator state changes are auditable."""
|
||||
pytest.fail("set_state spans not yet emitted")
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
if sys.platform != "linux": # pragma: no cover
|
||||
pytest.skip("FilesystemRuleStore is Linux-only (inotify dep)")
|
||||
|
||||
from decnet.ttp.store.base import RuleState
|
||||
from decnet.ttp.store.impl.filesystem import FilesystemRuleStore
|
||||
|
||||
exporter, _provider = span_exporter
|
||||
|
||||
async def _run() -> None:
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
from pathlib import Path
|
||||
store = FilesystemRuleStore(rules_dir=Path(td))
|
||||
await store.set_state(
|
||||
"R0001", RuleState(state="disabled"), set_by="anti",
|
||||
)
|
||||
|
||||
asyncio.run(_run())
|
||||
names = [span.name for span in exporter.get_finished_spans()]
|
||||
assert "ttp.rule.state.change" in names
|
||||
assert "ttp.store.write_state" in names
|
||||
assert "ttp.rule.publish" in names
|
||||
|
||||
|
||||
# ── No-PII property (xfail until E.3.7+) ────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user