apache/pulsar · warning · RestException

Concurrent modification

Error message

Concurrent modification

What it means

HTTP 409 returned by the namespace permission-grant admin API when the underlying metadata store reports an optimistic-concurrency failure (MetadataStoreException.BadVersionException) or an IllegalStateException propagated by legacy code. The permissions policy was modified concurrently by another broker/admin operation, so the write was rejected instead of silently overwriting.

Source

Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/NamespacesBase.java:681

                .exceptionally(ex -> {
                    Throwable realCause = FutureUtil.unwrapCompletionException(ex);
                    //The IllegalArgumentException and the IllegalStateException were historically thrown by the
                    // grantPermissionAsync method, so we catch them here to ensure backwards compatibility.
                    if (realCause instanceof MetadataStoreException.NotFoundException
                            || realCause instanceof IllegalArgumentException) {
                        log.warn()
                                .attr("namespace", namespaceName)
                                .exception(ex)
                                .log("Failed to set permissions for namespace : does not exist");
                        throw new RestException(Status.NOT_FOUND, "Topic's namespace does not exist");
                    } else if (realCause instanceof MetadataStoreException.BadVersionException
                            || realCause instanceof IllegalStateException) {
                        log.warn()
                                .attr("namespace", namespaceName)
                                .exceptionMessage(ex.getCause())
                                .exception(ex)
                                .log("Failed to set permissions for namespace");
                        throw new RestException(Status.CONFLICT, "Concurrent modification");
                    } else {
                        log.error()
                                .attr("namespace", namespaceName)
                                .exception(ex)
                                .log("Failed to get permissions for namespace");
                        throw new RestException(realCause);
                    }
                });
    }

    protected CompletableFuture<Void> internalGrantPermissionOnTopicsAsync(List<GrantTopicPermissionOptions> options) {
        return checkNamespace(options.stream().map(o -> TopicName.get(o.getTopic()).getNamespace()))
                .thenCompose(__ -> validateAdminAccessForTenantAsync(
                        TopicName.get(options.get(0).getTopic()).getTenant())
                ).thenCompose(__ -> internalCheckTopicExists(options.stream().map(o -> TopicName.get(o.getTopic()))))
                .thenCompose(__ -> getAuthorizationService().grantPermissionAsync(options))
                .thenAccept(unused -> log.info()
                        .attr("options", options)

View on GitHub (pinned to 820761864e)

Solutions

  1. Retry the grant: re-read current permissions with GET /namespaces/{ns}/permissions, re-apply the desired diff, and resubmit.
  2. Serialize permission changes for a namespace (single admin process, lock, or queue) instead of concurrent updates.
  3. Inspect broker logs (WARN 'Failed to set permissions for namespace') to identify the competing operation.
  4. Upgrade Pulsar if you see IllegalStateException without a real concurrent writer — legacy error propagation path.

Example fix

// before: fire-and-forget concurrent grant
twoAdmins.forEach(a -> a.grantPermission(ns, role, actions));
// after: retry on 409
retryOnConflict(3, () -> grantPermission(ns, role, actions));
Defensive patterns

Strategy: retry

Validate before calling

// no reliable pre-check; minimize the window instead
List<GrantedPermission> current = admin.namespaces().getPermissions(ns);

Try / catch

try {
    admin.namespaces().grantPermissionOnNamespace(ns, role, actions);
} catch (PulsarAdminException.ConflictException e) {
    // re-read and retry with backoff
    retry(3, () -> admin.namespaces().grantPermissionOnNamespace(ns, role, actions));
}

Prevention

When it happens

Trigger: Calling POST/PUT on /namespaces/{tenant}/{namespace}/permissions/{role} (internalGrantPermissionOnNamespaceAsync) while another admin client or broker updates the same namespace's permission policies; the metadata store version check fails, or a wrapped IllegalStateException surfaces from grantPermissionAsync.

Common situations: Two admins or automation scripts granting/revoking permissions on the same namespace at once; configuration-management tools (Terraform/Ansible) racing; repeated rapid permission updates through the REST API hitting the same z-node/etcd key version.

Related errors


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