apache/pulsar · error · MetadataStoreException
Migration has already been completed
Error message
Migration has already been completed
What it means
When the existing migration flag on the source store is in COMPLETED phase, startMigration refuses to run again: setInitialMigrationPhase throws MetadataStoreException("Migration has already been completed"). The migration flag is durable, so any later startMigration attempt against the same source store is rejected.
Source
Thrown at pulsar-metadata/src/main/java/org/apache/pulsar/metadata/coordination/impl/MigrationCoordinator.java:131
}
private void setInitialMigrationPhase() throws MetadataStoreException {
try {
Optional<GetResult> existing = sourceStore.get(MigrationState.MIGRATION_FLAG_PATH).get();
Optional<Long> expectedVersion;
if (existing.isEmpty()) {
// Create-only, to guard against concurrent migration starts
expectedVersion = Optional.of(-1L);
} else {
MigrationState currentState = ObjectMapperFactory.getMapper().reader()
.readValue(existing.get().getValue(), MigrationState.class);
expectedVersion = switch (currentState.getPhase()) {
// A leftover flag from a failed (or never started) migration can be replaced. The
// expected version guards against concurrent migration starts.
case NOT_STARTED, FAILED -> Optional.of(existing.get().getStat().getVersion());
case PREPARATION, COPYING -> throw new MetadataStoreException(
"Migration is already in progress (phase: " + currentState.getPhase() + ")");
case COMPLETED -> throw new MetadataStoreException("Migration has already been completed");
};
}
sourceStore.put(MigrationState.MIGRATION_FLAG_PATH,
ObjectMapperFactory.getMapper().writer()
.writeValueAsBytes(new MigrationState(MigrationPhase.PREPARATION, targetUrl)),
expectedVersion).get();
} catch (MetadataStoreException e) {
throw e;
} catch (Exception e) {
throw new MetadataStoreException(e);
}
}
private void updatePhase(MigrationPhase phase) throws MetadataStoreException {
try {
migrationStateCache.put(MigrationState.MIGRATION_FLAG_PATH,
new MigrationState(phase, targetUrl), EnumSet.noneOf(CreateOption.class)).get();View on GitHub (pinned to 820761864e)
Solutions
- Confirm the migration is complete and stop re-running startMigration against this source store
- If a re-migration is truly required, delete/overwrite the MIGRATION_FLAG_PATH node back to NOT_STARTED after taking backups
- Verify you are targeting the correct (new) source store, not the already-migrated one
Example fix
// before
coordinator.startMigration(); // flag already COMPLETED
// after
if (coordinator.getMigrationState().getPhase() != MigrationPhase.COMPLETED) {
coordinator.startMigration();
} Defensive patterns
Strategy: validation
Validate before calling
// Java
Optional<Versioned<byte[]>> flag =
sourceStore.get(MigrationState.MIGRATION_FLAG_PATH).get(30, TimeUnit.SECONDS);
if (flag.isPresent()) {
MigrationState s = ObjectMapperFactory.getMapper().reader()
.readValue(flag.get().getValue(), MigrationState.class);
if (s.getPhase() == MigrationPhase.COMPLETED) {
// migration done; skip startMigration
return;
}
}
Type guard
boolean isMigrationCompleted(MetadataStore src) throws Exception {
Optional<Versioned<byte[]>> f = src.get(MigrationState.MIGRATION_FLAG_PATH).get(30, TimeUnit.SECONDS);
return f.isPresent() && readPhase(f.get()) == MigrationPhase.COMPLETED;
}
Try / catch
try {
coordinator.startMigration();
} catch (MetadataStoreException e) {
if (e.getMessage().contains("already been completed")) {
// expected on re-runs; treat as no-op
}
}
Prevention
- Make migration scripts idempotent: check the flag phase first and skip when COMPLETED
- Never delete the completed flag to re-run migrations without operator approval and backups
- Record migration completion in runbooks/CMDB so re-runs are avoided
When it happens
Trigger: Calling startMigration when MIGRATION_FLAG_PATH holds MigrationState with phase COMPLETED — i.e., the source->Oxia migration finished previously.
Common situations: Re-running a completed migration playbook by mistake; pointing new tooling at a source store that was already migrated; CI script re-executed against an already-migrated cluster.
Related errors
- Expected target metadata store to be Oxia
- Migration is already in progress (phase: ${phase})
- Timed out waiting for migration participants to prepare: ${p
- Etcd metadata store backend has been removed in Pulsar 5.0 (
- Invalid metadata URL. the oxia metadata format should be 'ox
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/90e94da3ce6c39af.
Report an issue: GitHub.