apache/pulsar · error · RestException

Metadata store is not configured for migration. Please…

Error message

Metadata store is not configured for migration. Please ensure you're using a supported source metadata store (e.g., ZooKeeper).

What it means

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.

Solutions

  1. Reconfigure the broker to use the dual metadata store wrapper so pulsar().getLocalMetadataStore() instanceof DualMetadataStore, then restart.
  2. Target the request at a broker that is actually configured for migration.
  3. Verify with broker logs/config which metadata store implementation is active at startup.

Example fix

# before: default store only
metadataStoreUrl=zookeeper:zk1:2181
# after: configure migration/dual store mode per migration docs so the broker
# instantiates DualMetadataStore wrapping ZooKeeper (source) and target store
# then restart brokers before calling the migration endpoint
Defensive patterns

Strategy: validation

Validate before calling

// ensure brokers run the dual metadata store before calling migration APIs
// (no public API check; verify config at deploy time)
assert brokerConf.metadataStoreConfiguredForMigration() : "configure DualMetadataStore first";

Try / catch

try { startMigration(target); }
catch (PulsarAdminException e) {
  if (e.getStatusCode() == 400 && e.getMessage().contains("not configured for migration")) {
    /* reconfigure broker with DualMetadataStore and restart */
  } else throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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

Appendix: source

Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/MetadataMigrationBase.java:98

            @ApiResponse(responseCode = "204", description = "Migration started successfully"),
            @ApiResponse(responseCode = "400", description = "Invalid target URL"),
            @ApiResponse(responseCode = "409", description = "Migration already in progress"),
            @ApiResponse(responseCode = "500", description = "Internal server error")
    })
    public void startMigration(
            @Parameter(description = "Target metadata store URL", required = true)
            @QueryParam("target")
            String targetUrl) {
        validateSuperUserAccess();

        if (targetUrl == null || targetUrl.trim().isEmpty()) {
            throw new RestException(Response.Status.BAD_REQUEST, "Target URL is required");
        }

        try {
            // Check if metadata store is wrapped with DualMetadataStore
            if (!(pulsar().getLocalMetadataStore() instanceof DualMetadataStore dualStore)) {
                throw new RestException(Response.Status.BAD_REQUEST, "Metadata store is not configured for migration. "
                        + "Please ensure you're using a supported source metadata store (e.g., ZooKeeper).");
            }

            // Reject the request if a migration is already in progress or was completed. The migration
            // flag is always kept in the source store, so read it from there: after a completed
            // migration the dual store would route the read to the target store.
            var existingFlag = dualStore.getSourceStore().get(MigrationState.MIGRATION_FLAG_PATH).get();
            if (existingFlag.isPresent()) {
                MigrationState currentState = ObjectMapperFactory.getMapper().reader()
                        .readValue(existingFlag.get().getValue(), MigrationState.class);
                switch (currentState.getPhase()) {
                    case PREPARATION, COPYING -> throw new RestException(Response.Status.CONFLICT,
                            "Migration is already in progress (phase: " + currentState.getPhase() + ")");
                    case COMPLETED -> throw new RestException(Response.Status.CONFLICT,
                            "Migration has already been completed");
                    default -> {
                        // NOT_STARTED or FAILED: ok to start (or retry) the migration
                    }

View on GitHub (pinned to 820761864e)