Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions reflexio/models/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,12 @@ class LineageGCConfig(BaseModel):


class GovernanceRetentionConfig(BaseModel):
"""Audit-event retention policy. **Enterprise-only:** reclamation is performed
by the reflexio_ext GovernanceRetentionCapability. In an OSS-only deployment
these knobs are accepted but inert (the OSS lineage scheduler does not reclaim
audit events) and the server logs a startup warning when retention is enabled.
"""

audit_events_retention_enabled: bool = False
audit_events_retention_days: int = Field(default=365, gt=0)
audit_events_delete_batch_limit: int = Field(default=500, gt=0)
Expand Down
102 changes: 47 additions & 55 deletions reflexio/server/services/lineage/gc_scheduler.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Process-local scheduler for lineage cleanup.
"""Process-local scheduler for lineage tombstone garbage collection.

Startup is bootstrap-config-gated. Each tick then evaluates every org
independently, running tombstone GC from ``lineage_gc`` and governance
retention GC from ``governance_retention`` according to that org's config.
One org's failure never stalls the loop; errors are captured as Sentry
anomalies and the scheduler continues to the next org.
Startup is gated on ``lineage_gc.enabled`` (default on). Each tick evaluates
every org independently and hard-deletes expired tombstones per that org's
``lineage_gc`` config. One org's failure never stalls the loop; errors are
captured as Sentry anomalies and the loop continues to the next org.

Governance-retention reclamation is a premium concern handled by the enterprise
GovernanceRetentionCapability (reflexio_ext), not here.
"""

from __future__ import annotations
Expand All @@ -14,7 +16,6 @@
import time
from collections.abc import Callable

from reflexio.models.config_schema import GovernanceRetentionConfig
from reflexio.server.api_endpoints.request_context import RequestContext
from reflexio.server.tracing import capture_anomaly

Expand All @@ -25,20 +26,13 @@

# Window-misconfiguration tripwire: if a single tick deletes more than this
# many tombstones for one org, something is likely wrong with the grace window.
# Governance retention uses separate policy knobs and must not trigger this.
_HIGH_VOLUME_THRESHOLD = 1000

_ENTITY_TYPES = ("user_playbook", "agent_playbook", "profile")


def _is_governance_retention_enabled(
governance_retention: GovernanceRetentionConfig,
) -> bool:
return governance_retention.audit_events_retention_enabled


class LineageGCScheduler:
"""Polling daemon that runs tombstone GC and governance retention per org."""
"""Polling daemon that hard-deletes expired tombstones per org."""

def __init__(
self,
Expand Down Expand Up @@ -104,47 +98,26 @@ def _gc_tick(self, org_ids: list[str]) -> None:
break
try:
ctx = self.request_context_factory(org_id)
cfg = ctx.configurator.get_config()
if ctx.storage is None:
continue
run_tombstone_gc = cfg.lineage_gc.enabled
governance_retention = getattr(
cfg, "governance_retention", GovernanceRetentionConfig()
)
run_governance_gc = _is_governance_retention_enabled(
governance_retention
)
if not run_tombstone_gc and not run_governance_gc:
cfg = ctx.configurator.get_config()
if not cfg.lineage_gc.enabled:
continue

older_than_epoch = (
int(time.time())
- cfg.lineage_gc.tombstone_grace_window_days * 86400
)
tombstone_deleted = 0
if run_tombstone_gc:
older_than_epoch = (
int(time.time())
- cfg.lineage_gc.tombstone_grace_window_days * 86400
for entity_type in _ENTITY_TYPES:
tombstone_deleted += ctx.storage.gc_expired_tombstones(
entity_type=entity_type,
older_than_epoch=older_than_epoch,
)
for entity_type in _ENTITY_TYPES:
count = ctx.storage.gc_expired_tombstones(
entity_type=entity_type,
older_than_epoch=older_than_epoch,
)
tombstone_deleted += count
governance_deleted = 0
if run_governance_gc:
governance_deleted = ctx.storage.gc_governance_retention(
config=governance_retention
)
total_deleted = tombstone_deleted + governance_deleted
if total_deleted:
if tombstone_deleted:
logger.info(
(
"event=lineage_gc_tick org_id=%s deleted=%d "
"tombstone_deleted=%d governance_deleted=%d"
),
"event=lineage_gc_tick org_id=%s tombstone_deleted=%d",
org_id,
total_deleted,
tombstone_deleted,
governance_deleted,
)
if tombstone_deleted > _HIGH_VOLUME_THRESHOLD:
capture_anomaly(
Expand Down Expand Up @@ -175,10 +148,10 @@ def maybe_start_lineage_gc(
*,
bootstrap_org_id: str,
) -> LineageGCScheduler | None:
"""Start the scheduler only when bootstrap config enables some GC work.
"""Start the scheduler only when bootstrap config enables tombstone GC.

Off by default. Startup requires bootstrap-org config to enable tombstone GC
or any governance retention gate. Tombstone-GC enablement criteria (must
Startup requires bootstrap-org config to enable tombstone GC via
``lineage_gc.enabled``. Tombstone-GC enablement criteria (must
ALL hold before enabling for a production org):

1. **Mechanism**: GC ages tombstones by ``retired_at`` (the INTEGER epoch
Expand All @@ -204,12 +177,31 @@ def maybe_start_lineage_gc(
try:
ctx = request_context_factory(bootstrap_org_id)
cfg = ctx.configurator.get_config()
governance_retention = getattr(
cfg, "governance_retention", GovernanceRetentionConfig()

# Dead-knob warning: audit-event retention is an ENTERPRISE-only feature
# (handled by reflexio_ext GovernanceRetentionCapability). In an OSS-only
# deployment the knob is accepted but does nothing — warn loudly. Detected
# via the configurator class: enterprise swaps in EnterpriseConfigurator at
# construction, so the OSS DefaultConfigurator means "no enterprise here".
from reflexio.server.services.configurator.configurator import ( # noqa: PLC0415
DefaultConfigurator,
get_configurator_class,
)
if not cfg.lineage_gc.enabled and not _is_governance_retention_enabled(
governance_retention

gr = getattr(cfg, "governance_retention", None)
if (
gr is not None
and getattr(gr, "audit_events_retention_enabled", False)
and get_configurator_class() is DefaultConfigurator
):
logger.warning(
"event=governance_retention_knob_ignored "
"audit_events_retention_enabled=True but this is an OSS-only "
"deployment — audit-event retention is an enterprise-only feature "
"and will NOT run here."
)

if not cfg.lineage_gc.enabled:
return None
except Exception as exc:
logger.warning(
Expand Down
Loading