{"record":{"id":"55ad008dec059f79","repo":"apache/pulsar","slug":"metadata-store-is-not-configured-for-migration-pl","errorCode":null,"errorMessage":"Metadata store is not configured for migration. Please ensure you're using a supported source metadata store (e.g., ZooKeeper).","messagePattern":"Metadata store is not configured for migration\\. Please ensure you're using a supported source metadata store \\(e\\.g\\., ZooKeeper\\)\\.","errorType":"http","errorClass":"RestException","httpStatus":400,"severity":"error","filePath":"pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/MetadataMigrationBase.java","lineNumber":98,"sourceCode":"            @ApiResponse(responseCode = \"204\", description = \"Migration started successfully\"),\n            @ApiResponse(responseCode = \"400\", description = \"Invalid target URL\"),\n            @ApiResponse(responseCode = \"409\", description = \"Migration already in progress\"),\n            @ApiResponse(responseCode = \"500\", description = \"Internal server error\")\n    })\n    public void startMigration(\n            @Parameter(description = \"Target metadata store URL\", required = true)\n            @QueryParam(\"target\")\n            String targetUrl) {\n        validateSuperUserAccess();\n\n        if (targetUrl == null || targetUrl.trim().isEmpty()) {\n            throw new RestException(Response.Status.BAD_REQUEST, \"Target URL is required\");\n        }\n\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                    }","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/MetadataMigrationBase.java#L80-L116","documentation":"A 400 BAD_REQUEST thrown when the broker's local metadata store is not a DualMetadataStore, i.e. the broker was not started with a migration-capable (dual/source-wrapped) metadata store configuration. Migration can only be orchestrated through a DualMetadataStore that fronts both source and target stores.","triggerScenarios":"Calling the migration start endpoint on a broker whose configurationMetadataStore/localMetadataStore is a plain store (e.g. default ZooKeeper store, not wrapped in DualMetadataStore via the migration configuration).","commonSituations":"Running migration admin commands against a normal (non-migration) broker deployment; forgetting to enable the dual metadata store wrapper in broker.conf (metadataStoreUrl / metadataStore config for migration mode); hitting the wrong broker cluster.","solutions":["Reconfigure the broker to use the dual metadata store wrapper so pulsar().getLocalMetadataStore() instanceof DualMetadataStore, then restart.","Target the request at a broker that is actually configured for migration.","Verify with broker logs/config which metadata store implementation is active at startup."],"exampleFix":"# before: default store only\nmetadataStoreUrl=zookeeper:zk1:2181\n# after: configure migration/dual store mode per migration docs so the broker\n# instantiates DualMetadataStore wrapping ZooKeeper (source) and target store\n# then restart brokers before calling the migration endpoint","handlingStrategy":"validation","validationCode":"// ensure brokers run the dual metadata store before calling migration APIs\n// (no public API check; verify config at deploy time)\nassert brokerConf.metadataStoreConfiguredForMigration() : \"configure DualMetadataStore first\";","typeGuard":null,"tryCatchPattern":"try { startMigration(target); }\ncatch (PulsarAdminException e) {\n  if (e.getStatusCode() == 400 && e.getMessage().contains(\"not configured for migration\")) {\n    /* reconfigure broker with DualMetadataStore and restart */\n  } else throw e;\n}","preventionTips":["Deploy migration-mode brokers with DualMetadataStore wrapping the source (e.g. ZooKeeper) store","Document which broker clusters are migration-capable","Point migration tooling at migration-capable brokers only"],"tags":["metadata-migration","configuration","bad-request"],"backgroundTag":"metadata-store-not-configured","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"}