Skip to content

Scaffolding: Federation sync app creation#298

Open
klpoland wants to merge 11 commits into
masterfrom
feature-kpoland-federation-sync-scaffolding
Open

Scaffolding: Federation sync app creation#298
klpoland wants to merge 11 commits into
masterfrom
feature-kpoland-federation-sync-scaffolding

Conversation

@klpoland

@klpoland klpoland commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

In this PR:

  • [NEW] federation/ (v1) application with:
    • services:
      • bootstrap.py: for initialization of federated data from home site and peers (including when new peer is added)
      • fed_index.py: for handling (external peer) asset indexing on webhook events
      • local_events.py: for handling redis dispatches to fetch local data
      • peer registry.py: for registering new peers and holding site information from site hellos
      • peer_sync.py: for packaging and sending asset data to peer sites (through their webhook routes)
    • routes:
      • health.py: (stub for now) for pinging site health (is connection established?)
      • webhooks.py: for receiving payloads FROM peers to index into LOCAL federated asset indices as well as a site-hello hook to confirm federation between peers
    • schemas and models: for type checking and providing data structures to federation configs, events, and documents (how asset docs are represented by FederatedDatasetDoc, FederatedCaptureDoc may warrant further consideration re: anticipating schema flexibility)
    • a main app that subscribes to redis events for picking up signals from gateway (handled in PR Scaffolding: Federation gateway setup #299)

Note

Medium Risk
Adds new cross-site sync and OpenSearch indexing with webhook allowlisting; operational correctness depends on gateway Redis events and export API contracts staying in sync with this service.

Overview
Introduces a new federation/ FastAPI sync service (mounted at /sync) that keeps federated dataset/capture metadata in sync across SDS sites.

On startup it bootstraps by pulling gateway export lists for the local site and configured peers, indexing into OpenSearch (fed-datasets / fed-captures), then registers with peers via site-hello. A background Redis subscriber on federation:events:{site} reacts to gateway change notifications: resolves public assets from the local gateway export API, updates the local federated index, and fans out dataset-updated / capture-updated webhooks to peers (optional peer CA for HTTPS).

Inbound /api/v1/webhook/* routes accept peer events only from allowlisted site_name values, with Pydantic contracts aligned to gateway federation serializers. /health aggregates Redis, OpenSearch, subscriber, and gateway-export probes.

Repo wiring adds federation-specific pre-commit (ruff/pyrefly/deptry), Docker Compose for local/prod sync containers, dev mTLS cert scripts, and a broad pytest suite including a two-site in-process mesh.

Reviewed by Cursor Bugbot for commit 5f40bf1. Bugbot is set up for automated code reviews on this repo. Configure here.

@klpoland klpoland self-assigned this Jun 19, 2026
@klpoland klpoland added feature New feature or request federation Federation related work (sync service, document storage, peer configuration, etc.) labels Jun 19, 2026
@semanticdiff-com

semanticdiff-com Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  .pre-commit-config.yaml  4% smaller
  federation/.envs/example/sync.env Unsupported file format
  federation/.gitignore Unsupported file format
  federation/Dockerfile Unsupported file format
  federation/certs/.gitignore Unsupported file format
  federation/compose.local.yaml  0% smaller
  federation/compose.production.yaml  0% smaller
  federation/federation.example.toml Unsupported file format
  federation/justfile Unsupported file format
  federation/pyproject.toml Unsupported file format
  federation/scripts/env-selection.sh Unsupported file format
  federation/scripts/generate-dev-certs.sh Unsupported file format
  federation/scripts/simulate_redis_event.py  0% smaller
  federation/sds_federation/__init__.py  0% smaller
  federation/sds_federation/main.py  0% smaller
  federation/sds_federation/models.py  0% smaller
  federation/sds_federation/routes/__init__.py  0% smaller
  federation/sds_federation/routes/health.py  0% smaller
  federation/sds_federation/routes/webhooks.py  0% smaller
  federation/sds_federation/schemas/__init__.py  0% smaller
  federation/sds_federation/schemas/webhooks.py  0% smaller
  federation/sds_federation/services/__init__.py  0% smaller
  federation/sds_federation/services/bootstrap.py  0% smaller
  federation/sds_federation/services/fed_index.py  0% smaller
  federation/sds_federation/services/local_events.py  0% smaller
  federation/sds_federation/services/operational.py  0% smaller
  federation/sds_federation/services/peer_registry.py  0% smaller
  federation/sds_federation/services/peer_sync.py  0% smaller
  federation/sds_federation/services/redis_channel.py  0% smaller
  federation/sds_federation/testing/__init__.py  0% smaller
  federation/sds_federation/testing/sample_data.py  0% smaller
  federation/tests/__init__.py  0% smaller
  federation/tests/conftest.py  0% smaller
  federation/tests/support/__init__.py  0% smaller
  federation/tests/support/federation_mesh.py  0% smaller
  federation/tests/support/gateway_export_mock.py  0% smaller
  federation/tests/support/mock_opensearch.py  0% smaller
  federation/tests/support/mock_peer_registry.py  0% smaller
  federation/tests/test_integration_bootstrap.py  0% smaller
  federation/tests/test_integration_mesh.py  0% smaller
  federation/tests/test_integration_pipeline.py  0% smaller
  federation/tests/test_integration_webhooks.py  0% smaller
  federation/tests/test_operational.py  0% smaller
  federation/tests/test_redis_event_pipeline.py  0% smaller
  federation/tests/test_regression_indexer.py  0% smaller
  federation/tests/test_regression_redis_channel.py  0% smaller
  federation/tests/test_regression_schemas.py  0% smaller
  federation/uv.lock Unsupported file format

@klpoland klpoland changed the title Scaffolding: Federation Sync App Scaffolding: Federation sync app creation Jun 19, 2026
@klpoland klpoland marked this pull request as ready for review June 25, 2026 19:50
@klpoland klpoland requested a review from lucaspar June 25, 2026 19:50
Comment thread federation/sds_federation/services/local_events.py
Comment thread federation/sds_federation/services/local_events.py
Comment thread federation/sds_federation/services/bootstrap.py Outdated
Comment thread federation/compose.yaml Outdated
Comment thread federation/sds_federation/services/local_events.py
Comment thread federation/sds_federation/services/fed_index.py
Comment thread federation/pyproject.toml
Comment thread federation/pyproject.toml Outdated
@lucaspar

Copy link
Copy Markdown
Member

I'll add more comments after those are addressed to reduce the noise

asset=asset,
asset_type=asset_type,
)
await push_asset_updated_to_peers(http, config, payload)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale skip still notifies peers

High Severity

After a Redis event, handle_redis_asset_event always calls push_asset_updated_to_peers, even when FederatedAssetIndexer.apply_asset_event returns early because the event is stale. Peers can receive deletes or older updates that the local indexer intentionally ignored, causing cross-site index drift (for example after bootstrap sets a newer in-memory event_at).

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d0608d2. Configure here.

body = payload.model_dump(mode="json")
path = payload.asset_type.webhook_path
for peer in config.peers:
url = peer_webhook_url(peer, path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outbound sync ignores peer registry

Medium Severity

push_asset_updated_to_peers always uses static config.peers URLs from federation.toml, while site-hello updates PeerRegistry with each peer’s declared sync_service_url. Outbound webhooks never read the registry, so post-hello URL data is unused.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d0608d2. Configure here.

def _is_stale(self, site_name: str, uuid: UUID, event_at: datetime) -> bool:
key = doc_id(site_name, uuid)
prev = self._last_event.get(key)
return bool(prev is not None and event_at <= prev)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naive-aware datetime compare crash

Medium Severity

Stale-event dedupe compares event_at from Redis (datetime.fromisoformat without normalizing timezone) against bootstrap/webhook timestamps that are timezone-aware UTC. If the gateway publishes naive ISO timestamps, Python raises TypeError on comparison and can crash indexing or the Redis subscriber loop.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d0608d2. Configure here.

config = request.app.state.config
allowed = {peer.name for peer in config.peers} | {config.site.name}
if payload.site_name not in allowed:
raise HTTPException(status_code=403, detail="Unknown origin site")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peers may spoof local site

Medium Severity

Inbound asset webhooks allow payload.site_name to match the local config.site.name as well as configured peers. A peer (or any caller that can reach the route) can index documents under the home site identity, which local changes are meant to supply only via Redis and gateway export—not via peer POST bodies.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ce44ac9. Configure here.

Comment thread federation/justfile Outdated
asset_type.value,
exc,
)
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bootstrap misses validation errors

Medium Severity

bootstrap_gateway_exports catches only httpx.HTTPError around fetch_peer_export_list. Pydantic validation failures, unexpected response shapes (TypeError), or ValueError from indexing propagate and can abort all of run_bootstrap, skipping later peer pulls and register_with_peers, with only a top-level lifespan log if the exception reaches startup.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ce44ac9. Configure here.


app = FastAPI(title="SDS Federation Sync", root_path="/sync", lifespan=lifespan)
app.include_router(health_router)
app.include_router(webhooks_router, prefix=API_PREFIX)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sync URL path mismatch

High Severity

Outbound peer webhooks target {sync_service_url}/api/v1/..., which includes a /sync segment when sync_service_url is configured as documented. The running app registers routes at /api/v1 and /health only; root_path="/sync" does not serve those paths. Requests to /sync/api/v1/... hit uvicorn without a matching route unless a proxy strips /sync, so peer POSTs can 404 while tests pass via a /sync mount.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3a7c399. Configure here.

app.state.fed_indexer,
stop,
),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bootstrap misses live Redis events

Medium Severity

Startup fully awaits federation bootstrap before creating the Redis subscriber task. Gateway events published on federation:events during bootstrap are never consumed, so metadata changes in that window can stay missing from the local index and peers until a later update.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3a7c399. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 8 total unresolved issues (including 7 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f40bf1. Configure here.

def _is_stale(self, site_name: str, uuid: UUID, event_at: datetime) -> bool:
key = doc_id(site_name, uuid)
prev = self._last_event.get(key)
return bool(prev is not None and event_at <= prev)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equal timestamps treated as stale

Medium Severity

Stale-event dedupe uses event_at <= prev, so an event with the same timestamp as the last applied one is skipped entirely. Bootstrap applies many documents with one shared event_at, so a later Redis or webhook update at that same instant can be dropped and never written to OpenSearch.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f40bf1. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request federation Federation related work (sync service, document storage, peer configuration, etc.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants