{"record":{"id":"d2c26dea3f2b1399","repo":"apache/pulsar","slug":"migration-has-already-been-completed","errorCode":null,"errorMessage":"Migration has already been completed","messagePattern":"Migration has already been completed","errorType":"http","errorClass":"RestException","httpStatus":409,"severity":"info","filePath":"pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/MetadataMigrationBase.java","lineNumber":112,"sourceCode":"\n        try {\n            // Check if metadata store is wrapped with DualMetadataStore\n            if (!(pulsar().getLocalMetadataStore() instanceof DualMetadataStore dualStore)) {\n                throw new RestException(Response.Status.BAD_REQUEST, \"Metadata store is not configured for migration. \"\n                        + \"Please ensure you're using a supported source metadata store (e.g., ZooKeeper).\");\n            }\n\n            // Reject the request if a migration is already in progress or was completed. The migration\n            // flag is always kept in the source store, so read it from there: after a completed\n            // migration the dual store would route the read to the target store.\n            var existingFlag = dualStore.getSourceStore().get(MigrationState.MIGRATION_FLAG_PATH).get();\n            if (existingFlag.isPresent()) {\n                MigrationState currentState = ObjectMapperFactory.getMapper().reader()\n                        .readValue(existingFlag.get().getValue(), MigrationState.class);\n                switch (currentState.getPhase()) {\n                    case PREPARATION, COPYING -> throw new RestException(Response.Status.CONFLICT,\n                            \"Migration is already in progress (phase: \" + currentState.getPhase() + \")\");\n                    case COMPLETED -> throw new RestException(Response.Status.CONFLICT,\n                            \"Migration has already been completed\");\n                    default -> {\n                        // NOT_STARTED or FAILED: ok to start (or retry) the migration\n                    }\n                }\n            }\n\n            // Create coordinator\n            MigrationCoordinator coordinator = new MigrationCoordinator(pulsar().getLocalMetadataStore(), targetUrl);\n\n            // Start migration in background thread\n            pulsar().getExecutor().submit(() -> {\n                try {\n                    log.info().attr(\"targetUrl\", targetUrl).log(\"Starting metadata migration\");\n                    coordinator.startMigration();\n                    log.info(\"Metadata migration completed successfully\");\n                } catch (Exception e) {\n                    log.error().exception(e).log(\"Metadata migration failed\");","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/MetadataMigrationBase.java#L94-L130","documentation":"A 409 CONFLICT thrown when starting a metadata migration whose stored state phase is already COMPLETED. The migration is idempotent-by-guard: once finished, the flag in the source store permanently records COMPLETED and further starts are rejected instead of re-running.","triggerScenarios":"Re-invoking the migration start endpoint after a prior migration finished successfully; replaying an idempotent automation script; stale client state unaware the migration already ran.","commonSituations":"Post-migration verification scripts accidentally calling start again; disaster-recovery runbooks that include the start step without checking state; expected behavior after a successful migration.","solutions":["No action needed — the migration is done; verify the broker is reading from the target store.","Use the migration status endpoint to confirm phase COMPLETED instead of starting again.","If a re-migration is truly required, follow the documented reset procedure (which implies reconfiguring stores/flag), not the start endpoint."],"exampleFix":"// before: unconditional start after DR runbook step\nstartMigration(target); // 409\n// after: skip when already completed\nif (getMigrationStatus().getPhase() != Phase.COMPLETED) {\n    startMigration(target);\n}","handlingStrategy":"validation","validationCode":"if (getMigrationStatus().getPhase() == Phase.COMPLETED) {\n  return; // migration already finished, skip start\n}","typeGuard":null,"tryCatchPattern":"try { startMigration(target); }\ncatch (PulsarAdminException e) {\n  if (e.getStatusCode() == 409 && e.getMessage().contains(\"completed\")) {\n    // treat as success / no-op\n  } else throw e;\n}","preventionTips":["Treat COMPLETED as success in runbooks and scripts","Check status before start in idempotent automation","Never re-run startMigration after a successful migration; use documented reset procedures for re-migration"],"tags":["metadata-migration","conflict","idempotency"],"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"}