{"record":{"id":"92dd4b2928aae034","repo":"apache/pulsar","slug":"topic-is-already-scalable-scalablename","errorCode":null,"errorMessage":"Topic is already scalable: ${scalableName}","messagePattern":"Topic is already scalable: (.+?)","errorType":"http","errorClass":"RestException","httpStatus":409,"severity":"error","filePath":"pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ScalableTopics.java","lineNumber":322,"sourceCode":"     * Orchestrate a regular-to-scalable migration:\n     * <ol>\n     *   <li>reject if scalable metadata already exists;</li>\n     *   <li>resolve the source topic's existence + partition count;</li>\n     *   <li>unless {@code force}, reject if any legacy v4 client is still connected;</li>\n     *   <li>build the migrated layout (sealed legacy parents + active children);</li>\n     *   <li>create the new child segment topics;</li>\n     *   <li>atomically write the scalable metadata (the commit point — connected V5 lookup\n     *       sessions transition from the synthetic layout to the real DAG via the metadata\n     *       watch);</li>\n     *   <li>terminate the old topics so no further v4 writes can land — they become the\n     *       drainable sealed parent segments.</li>\n     * </ol>\n     */\n    private CompletableFuture<Void> doMigrateToScalableAsync(TopicName scalableName,\n                                                             TopicName persistentBase, boolean force) {\n        return resources().getScalableTopicMetadataAsync(scalableName).thenCompose(existing -> {\n            if (existing.isPresent()) {\n                throw new RestException(Response.Status.CONFLICT,\n                        \"Topic is already scalable: \" + scalableName);\n            }\n            return pulsar().getNamespaceService().checkTopicExistsAsync(persistentBase);\n        }).thenCompose(existsInfo -> {\n            boolean exists = existsInfo.isExists();\n            int partitions = existsInfo.getPartitions();\n            existsInfo.recycle();\n            if (!exists) {\n                throw new RestException(Response.Status.NOT_FOUND,\n                        \"Topic does not exist: \" + persistentBase);\n            }\n            CompletableFuture<Void> precheck = force\n                    ? CompletableFuture.completedFuture(null)\n                    : checkNoLegacyConnectionsAsync(persistentBase, partitions);\n            return precheck.thenApply(__ -> partitions);\n        }).thenCompose(partitions -> {\n            ScalableTopicMetadata metadata =\n                    ScalableTopicController.createMigratedMetadata(persistentBase, partitions,","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ScalableTopics.java#L304-L340","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait admin.scalableTopics().migrateToScalable(tenant, ns, topic);\n// after\nif (await admin.scalableTopics().getScalableTopicMetadataAsync(tenant, ns, topic).isPresent()) {\n    return; // already migrated\n}\nawait admin.scalableTopics().migrateToScalable(tenant, ns, topic);","handlingStrategy":"validation","validationCode":"const md = await admin.scalableTopics().getScalableTopicMetadataAsync(tenant, ns, topic);\nif (md.isPresent()) return; // already scalable, nothing to do","typeGuard":"const isAlreadyScalable = (metadataOpt) => metadataOpt != null && metadataOpt.isPresent();","tryCatchPattern":"try {\n  await admin.scalableTopics().migrateToScalable(tenant, ns, topic);\n} catch (e) {\n  if (e.status === 409 && /already scalable/.test(e.message)) return; // idempotent success\n  throw e;\n}","preventionTips":["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."],"tags":["rest","conflict","migration","scalable-topics","http-409"],"backgroundTag":"resource-already-exists","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}