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

  1. Confirm the migration is complete and stop re-running startMigration against this source store
  2. If a re-migration is truly required, delete/overwrite the MIGRATION_FLAG_PATH node back to NOT_STARTED after taking backups
  3. 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

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


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/90e94da3ce6c39af. Report an issue: GitHub.