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
- Check whether the topic is already scalable (GET its scalable topic metadata) and skip migration if present.
- Treat 409 as success in idempotent automation and continue the pipeline.
- 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
- Make migration jobs idempotent: check existing scalable metadata first.
- Serialize migrations per topic (single operator/lock) to avoid concurrent 409s.
- Track migrated topics in your own state store instead of relying on retries.
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
- ${legacy} legacy v4 client connection(s) still attached to $
- Topic does not exist: ${persistentBase}
- numInitialSegments must be >= 1
- Scalable topic not found: ${tn}
- Label name cannot be null or empty
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/92dd4b2928aae034.
Report an issue: GitHub.