apache/pulsar · error · RestException

Topic is already scalable: ${scalableName}

Error message

Topic is already scalable: ${scalableName}

What it means

HTTP 409 CONFLICT raised during migration when the target scalable topic already has scalable-topic metadata in metadata store, i.e. the topic was already migrated. Migration is idempotent-unfriendly by design: it refuses to re-run on an already-scalable topic.

Source

Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ScalableTopics.java:322

     * Orchestrate a regular-to-scalable migration:
     * <ol>
     *   <li>reject if scalable metadata already exists;</li>
     *   <li>resolve the source topic's existence + partition count;</li>
     *   <li>unless {@code force}, reject if any legacy v4 client is still connected;</li>
     *   <li>build the migrated layout (sealed legacy parents + active children);</li>
     *   <li>create the new child segment topics;</li>
     *   <li>atomically write the scalable metadata (the commit point — connected V5 lookup
     *       sessions transition from the synthetic layout to the real DAG via the metadata
     *       watch);</li>
     *   <li>terminate the old topics so no further v4 writes can land — they become the
     *       drainable sealed parent segments.</li>
     * </ol>
     */
    private CompletableFuture<Void> doMigrateToScalableAsync(TopicName scalableName,
                                                             TopicName persistentBase, boolean force) {
        return resources().getScalableTopicMetadataAsync(scalableName).thenCompose(existing -> {
            if (existing.isPresent()) {
                throw new RestException(Response.Status.CONFLICT,
                        "Topic is already scalable: " + scalableName);
            }
            return pulsar().getNamespaceService().checkTopicExistsAsync(persistentBase);
        }).thenCompose(existsInfo -> {
            boolean exists = existsInfo.isExists();
            int partitions = existsInfo.getPartitions();
            existsInfo.recycle();
            if (!exists) {
                throw new RestException(Response.Status.NOT_FOUND,
                        "Topic does not exist: " + persistentBase);
            }
            CompletableFuture<Void> precheck = force
                    ? CompletableFuture.completedFuture(null)
                    : checkNoLegacyConnectionsAsync(persistentBase, partitions);
            return precheck.thenApply(__ -> partitions);
        }).thenCompose(partitions -> {
            ScalableTopicMetadata metadata =
                    ScalableTopicController.createMigratedMetadata(persistentBase, partitions,

View on GitHub (pinned to 820761864e)

Solutions

  1. Check whether the topic is already scalable (GET its scalable topic metadata) and skip migration if present.
  2. Treat 409 as success in idempotent automation and continue the pipeline.
  3. If a previous migration half-failed, verify the segment topics exist and use the scalable topic directly instead of re-migrating.

Example fix

// before
await admin.scalableTopics().migrateToScalable(tenant, ns, topic);
// after
if (await admin.scalableTopics().getScalableTopicMetadataAsync(tenant, ns, topic).isPresent()) {
    return; // already migrated
}
await admin.scalableTopics().migrateToScalable(tenant, ns, topic);
Defensive patterns

Strategy: validation

Validate before calling

const md = await admin.scalableTopics().getScalableTopicMetadataAsync(tenant, ns, topic);
if (md.isPresent()) return; // already scalable, nothing to do

Type guard

const isAlreadyScalable = (metadataOpt) => metadataOpt != null && metadataOpt.isPresent();

Try / catch

try {
  await admin.scalableTopics().migrateToScalable(tenant, ns, topic);
} catch (e) {
  if (e.status === 409 && /already scalable/.test(e.message)) return; // idempotent success
  throw e;
}

Prevention

When it happens

Trigger: Calling POST /admin/v2/scalable-topics/{tenant}/{namespace}/{topic}/migrate on a topic whose scalable metadata already exists — typically a retried migration after a partial/complete previous run.

Common situations: CI pipelines that retry failed migration jobs; scripts that migrate all topics in a namespace without filtering already-migrated ones; concurrent migration attempts from two operators.

Related errors


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