Skip to content

FoundationDB Migrations

This guide documents migration behavior in FoundationDBDataStore and operational controls.

Preferred API:

  • pass migration settings as migrationConfiguration = MigrationConfiguration(...)
  • pass distributed lease tuning as migrationLeaseConfiguration = FoundationDBMigrationLeaseConfiguration(...)
  • flat migration arguments on FoundationDBDataStore.open(...) still work for compatibility

On startup, the store compares stored model definitions with configured models.

No migration required (automatic):

  • new model
  • safe property additions
  • compatible index additions (with backfill)
  • relaxed validation

Migration required:

  • incompatible type changes
  • removals/renames without compatibility aliases
  • stricter constraints that break compatibility

All migration hooks receive MigrationContext<FoundationDBDataStore> and return MigrationOutcome:

  • Success
  • Partial
  • Retry
  • Fatal

Available hooks:

  • migrationExpandHandler
  • migrationHandler (Backfill)
  • migrationVerifyHandler
  • migrationContractHandler

Execution phases:

  1. Expand
  2. Backfill
  3. Verify
  4. Contract

Progress is persisted via MigrationState per model (phase/status/attempt/cursor/message). Restart resumes from persisted state.

Current handler hooks:

  • Expand: runs migrationExpandHandler
  • Backfill: runs migrationHandler
  • Verify: runs migrationVerifyHandler
  • Contract: runs migrationContractHandler

For a compatible index addition, versionUpdateHandler runs while the model remains fenced as Rebuilding. Writes through the same FoundationDBDataStore are allowed only after each transaction proves ownership of that exact rebuild fence. Other open stores remain blocked until the hook succeeds and the definition and Ready epoch publish together.

Model migrations are ordered by dependency graph.

  • Referenced models migrate first.
  • A dependent handler does not start until each referenced model has completed finalization and definition publication.
  • Dependency cycles are detected and rejected up front with MigrationException.

Startup budget and background continuation

Section titled “Startup budget and background continuation”

Use:

  • migrationConfiguration.migrationStartupBudgetMs
  • migrationConfiguration.continueMigrationsInBackground = true

Behavior:

  • Startup can hand migration over to background.
  • Pending models remain request-blocked until completion.
  • Dependents waiting on a background migration remain blocked too; they do not delay opening the store or access to unrelated models.
  • Startup clock recovery reads raw version metadata before resuming an unfinished schema rebuild. Request-level schema fencing remains active.
  • pendingMigrations()
  • migrationStatus(modelId)
  • migrationStatuses()
  • awaitMigration(modelId)
  • pauseMigration(modelId)
  • resumeMigration(modelId)
  • cancelMigration(modelId, reason)

cancelMigration is terminal for the current store instance: the model stays blocked and you must reopen the store to resume from persisted state.

Operator semantics:

  • awaitMigration(modelId) returns normally when migration completes successfully.
  • awaitMigration(modelId) completes exceptionally with MigrationException if migration fails or is canceled.
  • pauseMigration(modelId) only affects pending/background progress. It does not interrupt a currently running phase step.
  • While paused, the model remains request-blocked.
  • resumeMigration(modelId) allows the next background loop iteration to continue from the persisted MigrationState.
  • After cancelMigration, the current store instance keeps the model blocked. Reopening the store resumes from the persisted phase/cursor.

Default lease is FoundationDBMigrationLease.

  • Lease key per model in metadata subspace
  • owner token
  • monotonically increasing fencing token, checked in every datastore mutation transaction submitted by a migration or version hook, including mutations for another registered model
  • TTL via migrationLeaseConfiguration.migrationLeaseTimeoutMs
  • heartbeat via migrationLeaseConfiguration.migrationLeaseHeartbeatMs
  • automatic takeover after TTL expiry

Use custom migrationLease only if external orchestrator semantics are needed.

The fencing token protects FoundationDB transactions made through the datastore. It cannot roll back or fence arbitrary external side effects. Hooks that call external systems must be idempotent or use an external coordinator/fencing token appropriate to that system; do not rely on host wall-clock lease expiry alone for those effects.

Retry thresholds:

  • migrationConfiguration.migrationRetryPolicy.maxAttempts
  • migrationConfiguration.migrationRetryPolicy.maxRetryOutcomes

Status payload includes:

  • state
  • phase
  • attempt
  • last error
  • cursor presence
  • retry count
  • ETA estimate

Metrics:

  • migrationMetrics(modelId) / migrationMetrics()

Audit:

  • default sink: migrationConfiguration.migrationAuditEventReporter (default line logger)
  • optional persisted audit store: migrationConfiguration.persistMigrationAuditEvents = true
  • query persisted events: migrationAuditEvents(modelId, limit)
  1. Enable background continuation for long-running migrations.
  2. Set retry thresholds for bounded failure handling.
  3. Monitor runtime status/metrics.
  4. Intervene with pause/resume/cancel only when required.
  5. Keep persisted audit logs opt-in; use default reporter for baseline observability.
FoundationDBDataStore.open(
keepAllVersions = true,
directoryPath = listOf("maryk", "app"),
dataModelsById = mapOf(1u to Account),
migrationConfiguration = MigrationConfiguration(
migrationExpandHandler = { context ->
// Prepare compatibility before data rewrite
MigrationOutcome.Success
},
migrationHandler = { context ->
// Backfill existing rows
MigrationOutcome.Success
},
migrationVerifyHandler = { context ->
// Validate rewritten data
MigrationOutcome.Success
},
migrationContractHandler = { context ->
// Final cleanup after verification
MigrationOutcome.Success
}
)
)