-
Notifications
You must be signed in to change notification settings - Fork 726
feat(sync): add multiplexer module for destination migrations with database-persisted SyncExecutionConfig #1201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
f46230b
feat(sync): add multiplexer module for destination migrations
orhanrauf 1ab9d1c
feat: add database-persisted SyncExecutionConfig for ARF-only capture
felixsmiller 237988f
add source connection id to manifest
felixsmiller 57e91b3
Revert "add source connection id to manifest"
felixsmiller edc425c
Add execution_config_json column to sync_job table
felixsmiller fb1a4a6
to arf resync
felixsmiller 01fda9f
arf replay: ruff format
felixsmiller 7252404
fix(sync): add execution config validation and skip_cursor_load for A…
felixsmiller 03b3766
arf testing
felixsmiller File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| """Endpoints for sync multiplexing (destination migrations). | ||
|
|
||
| Enables blue-green deployments for vector DB migrations: | ||
| - Fork: Add shadow destination + optionally replay from ARF | ||
| - Switch: Promote shadow to active | ||
| - List: Show all destinations with roles | ||
| - Resync: Force full sync from source to refresh ARF | ||
|
|
||
| Feature-gated: Requires SYNC_MULTIPLEXER feature flag enabled for the organization. | ||
| """ | ||
|
|
||
| from typing import List | ||
| from uuid import UUID | ||
|
|
||
| from fastapi import Depends, HTTPException | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from airweave import schemas | ||
| from airweave.api import deps | ||
| from airweave.api.context import ApiContext | ||
| from airweave.api.router import TrailingSlashRouter | ||
| from airweave.core.shared_models import FeatureFlag | ||
| from airweave.platform.sync.multiplex.multiplexer import SyncMultiplexer | ||
|
|
||
| router = TrailingSlashRouter() | ||
|
|
||
|
|
||
| def _require_multiplexer_feature(ctx: ApiContext) -> None: | ||
| """Check if organization has multiplexer feature enabled. | ||
|
|
||
| Args: | ||
| ctx: API context | ||
|
|
||
| Raises: | ||
| HTTPException: If feature not enabled | ||
| """ | ||
| if not ctx.has_feature(FeatureFlag.SYNC_MULTIPLEXER): | ||
| raise HTTPException( | ||
| status_code=403, | ||
| detail="Sync multiplexer feature is not enabled for this organization", | ||
| ) | ||
|
|
||
|
|
||
| @router.get( | ||
| "/{sync_id}/destinations", | ||
| response_model=List[schemas.DestinationSlotInfo], | ||
| summary="List destination slots", | ||
| description="List all destinations for a sync with their roles (active/shadow/deprecated).", | ||
| ) | ||
| async def list_destinations( | ||
| sync_id: UUID, | ||
| db: AsyncSession = Depends(deps.get_db), | ||
| ctx: ApiContext = Depends(deps.get_context), | ||
| ) -> List[schemas.DestinationSlotInfo]: | ||
| """List all destination slots for a sync. | ||
|
|
||
| Returns slots sorted by role: ACTIVE first, then SHADOW, then DEPRECATED. | ||
| """ | ||
| _require_multiplexer_feature(ctx) | ||
| multiplexer = SyncMultiplexer(db, ctx, ctx.logger) | ||
| return await multiplexer.list_destinations(sync_id) | ||
|
|
||
|
|
||
| @router.post( | ||
| "/{sync_id}/destinations/fork", | ||
| response_model=schemas.ForkDestinationResponse, | ||
| summary="Fork a new destination", | ||
| description="Add a shadow destination for migration testing. Optionally replay from ARF store.", | ||
| ) | ||
| async def fork_destination( | ||
| sync_id: UUID, | ||
| request: schemas.ForkDestinationRequest, | ||
| db: AsyncSession = Depends(deps.get_db), | ||
| ctx: ApiContext = Depends(deps.get_context), | ||
| ) -> schemas.ForkDestinationResponse: | ||
| """Fork a new shadow destination. | ||
|
|
||
| Creates a new destination slot with SHADOW role. If replay_from_arf is True, | ||
| entities will be replayed from the ARF store to populate the new destination. | ||
|
|
||
| Args: | ||
| sync_id: Sync ID to fork destination for | ||
| request: Fork request with destination connection ID and replay flag | ||
| db: Database session | ||
| ctx: API context | ||
|
|
||
| Returns: | ||
| ForkDestinationResponse with slot and optional replay job info | ||
| """ | ||
| _require_multiplexer_feature(ctx) | ||
| multiplexer = SyncMultiplexer(db, ctx, ctx.logger) | ||
| slot, replay_job = await multiplexer.fork( | ||
| sync_id=sync_id, | ||
| destination_connection_id=request.destination_connection_id, | ||
| replay_from_arf=request.replay_from_arf, | ||
| ) | ||
|
|
||
| slot_schema = schemas.SyncConnectionSchema( | ||
| id=slot.id, | ||
| sync_id=slot.sync_id, | ||
| connection_id=slot.connection_id, | ||
| role=slot.role, | ||
| created_at=slot.created_at, | ||
| modified_at=slot.modified_at, | ||
| ) | ||
|
|
||
| return schemas.ForkDestinationResponse( | ||
| slot=slot_schema, | ||
| replay_job_id=replay_job.id if replay_job else None, | ||
| replay_job_status=replay_job.status.value if replay_job else None, | ||
| ) | ||
|
|
||
|
|
||
| @router.post( | ||
| "/{sync_id}/destinations/{slot_id}/switch", | ||
| response_model=schemas.SwitchDestinationResponse, | ||
| summary="Switch active destination", | ||
| description="Promote a shadow destination to active. The current active becomes deprecated.", | ||
| ) | ||
| async def switch_destination( | ||
| sync_id: UUID, | ||
| slot_id: UUID, | ||
| db: AsyncSession = Depends(deps.get_db), | ||
| ctx: ApiContext = Depends(deps.get_context), | ||
| ) -> schemas.SwitchDestinationResponse: | ||
| """Switch the active destination. | ||
|
|
||
| Promotes the specified shadow slot to ACTIVE and demotes the current | ||
| ACTIVE slot to DEPRECATED. | ||
|
|
||
| Args: | ||
| sync_id: Sync ID | ||
| slot_id: Slot ID to promote to active | ||
| db: Database session | ||
| ctx: API context | ||
|
|
||
| Returns: | ||
| Switch response with new and previous active slot IDs | ||
| """ | ||
| _require_multiplexer_feature(ctx) | ||
| multiplexer = SyncMultiplexer(db, ctx, ctx.logger) | ||
| return await multiplexer.switch(sync_id=sync_id, new_active_slot_id=slot_id) | ||
|
|
||
|
|
||
| @router.post( | ||
| "/{sync_id}/resync", | ||
| response_model=schemas.SyncJob, | ||
| summary="Resync from source", | ||
| description="Force a full sync from the source to refresh the ARF store.", | ||
| ) | ||
| async def resync_from_source( | ||
| sync_id: UUID, | ||
| db: AsyncSession = Depends(deps.get_db), | ||
| ctx: ApiContext = Depends(deps.get_context), | ||
| ) -> schemas.SyncJob: | ||
| """Force full sync from source to refresh ARF. | ||
|
|
||
| Triggers a full sync (ignoring cursor) to ensure the ARF store is up-to-date | ||
| before forking to a new destination. | ||
|
|
||
| Args: | ||
| sync_id: Sync ID | ||
| db: Database session | ||
| ctx: API context | ||
|
|
||
| Returns: | ||
| SyncJob for tracking progress | ||
| """ | ||
| _require_multiplexer_feature(ctx) | ||
| multiplexer = SyncMultiplexer(db, ctx, ctx.logger) | ||
| return await multiplexer.resync_from_source(sync_id=sync_id) | ||
|
|
||
|
|
||
| @router.get( | ||
| "/{sync_id}/destinations/active", | ||
| response_model=schemas.DestinationSlotInfo, | ||
| summary="Get active destination", | ||
| description="Get the currently active destination for a sync.", | ||
| ) | ||
| async def get_active_destination( | ||
| sync_id: UUID, | ||
| db: AsyncSession = Depends(deps.get_db), | ||
| ctx: ApiContext = Depends(deps.get_context), | ||
| ) -> schemas.DestinationSlotInfo: | ||
| """Get the active destination slot. | ||
|
|
||
| Args: | ||
| sync_id: Sync ID | ||
| db: Database session | ||
| ctx: API context | ||
|
|
||
| Returns: | ||
| Active destination info | ||
|
|
||
| Raises: | ||
| HTTPException: If no active destination found | ||
| """ | ||
| _require_multiplexer_feature(ctx) | ||
| multiplexer = SyncMultiplexer(db, ctx, ctx.logger) | ||
| active = await multiplexer.get_active_destination(sync_id) | ||
| if not active: | ||
| raise HTTPException( | ||
| status_code=404, | ||
| detail=f"No active destination found for sync {sync_id}", | ||
| ) | ||
| return active |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Rule violated: Check for Cursor Rules Drift
Missing frontend feature flag constant. Per the documented process in
.cursor/rules/feature-flags.mdc, new feature flags must also be added tofrontend/src/lib/constants/feature-flags.ts. The frontend constants file states it "Must match backend FeatureFlag enum exactly."Add to
frontend/src/lib/constants/feature-flags.ts:Prompt for AI agents