{"record":{"id":"90e94da3ce6c39af","repo":"apache/pulsar","slug":"migration-has-already-been-completed-90e94d","errorCode":null,"errorMessage":"Migration has already been completed","messagePattern":"Migration has already been completed","errorType":"exception","errorClass":"MetadataStoreException","httpStatus":null,"severity":"error","filePath":"pulsar-metadata/src/main/java/org/apache/pulsar/metadata/coordination/impl/MigrationCoordinator.java","lineNumber":131,"sourceCode":"    }\n\n    private void setInitialMigrationPhase() throws MetadataStoreException {\n        try {\n            Optional<GetResult> existing = sourceStore.get(MigrationState.MIGRATION_FLAG_PATH).get();\n            Optional<Long> expectedVersion;\n            if (existing.isEmpty()) {\n                // Create-only, to guard against concurrent migration starts\n                expectedVersion = Optional.of(-1L);\n            } else {\n                MigrationState currentState = ObjectMapperFactory.getMapper().reader()\n                        .readValue(existing.get().getValue(), MigrationState.class);\n                expectedVersion = switch (currentState.getPhase()) {\n                    // A leftover flag from a failed (or never started) migration can be replaced. The\n                    // expected version guards against concurrent migration starts.\n                    case NOT_STARTED, FAILED -> Optional.of(existing.get().getStat().getVersion());\n                    case PREPARATION, COPYING -> throw new MetadataStoreException(\n                            \"Migration is already in progress (phase: \" + currentState.getPhase() + \")\");\n                    case COMPLETED -> throw new MetadataStoreException(\"Migration has already been completed\");\n                };\n            }\n\n            sourceStore.put(MigrationState.MIGRATION_FLAG_PATH,\n                    ObjectMapperFactory.getMapper().writer()\n                            .writeValueAsBytes(new MigrationState(MigrationPhase.PREPARATION, targetUrl)),\n                    expectedVersion).get();\n        } catch (MetadataStoreException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new MetadataStoreException(e);\n        }\n    }\n\n    private void updatePhase(MigrationPhase phase) throws MetadataStoreException {\n        try {\n            migrationStateCache.put(MigrationState.MIGRATION_FLAG_PATH,\n                    new MigrationState(phase, targetUrl), EnumSet.noneOf(CreateOption.class)).get();","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/coordination/impl/MigrationCoordinator.java#L113-L149","documentation":"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.","triggerScenarios":"Calling startMigration when MIGRATION_FLAG_PATH holds MigrationState with phase COMPLETED — i.e., the source->Oxia migration finished previously.","commonSituations":"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.","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"],"exampleFix":"// before\ncoordinator.startMigration(); // flag already COMPLETED\n// after\nif (coordinator.getMigrationState().getPhase() != MigrationPhase.COMPLETED) {\n    coordinator.startMigration();\n}","handlingStrategy":"validation","validationCode":"// Java\nOptional<Versioned<byte[]>> flag =\n    sourceStore.get(MigrationState.MIGRATION_FLAG_PATH).get(30, TimeUnit.SECONDS);\nif (flag.isPresent()) {\n    MigrationState s = ObjectMapperFactory.getMapper().reader()\n        .readValue(flag.get().getValue(), MigrationState.class);\n    if (s.getPhase() == MigrationPhase.COMPLETED) {\n        // migration done; skip startMigration\n        return;\n    }\n}\n","typeGuard":"boolean isMigrationCompleted(MetadataStore src) throws Exception {\n    Optional<Versioned<byte[]>> f = src.get(MigrationState.MIGRATION_FLAG_PATH).get(30, TimeUnit.SECONDS);\n    return f.isPresent() && readPhase(f.get()) == MigrationPhase.COMPLETED;\n}\n","tryCatchPattern":"try {\n    coordinator.startMigration();\n} catch (MetadataStoreException e) {\n    if (e.getMessage().contains(\"already been completed\")) {\n        // expected on re-runs; treat as no-op\n    }\n}\n","preventionTips":["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"],"tags":["migration","metadata-store","idempotency","oxia"],"backgroundTag":"migration-already-completed","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}