/** * Copyright 2016 Yahoo Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yahoo.pulsar.broker.admin; import static com.google.common.base.Preconditions.checkNotNull; import static com.yahoo.pulsar.common.util.Codec.decode; import java.io.IOException; import java.io.OutputStream; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.Encoded; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.container.AsyncResponse; import javax.ws.rs.container.Suspended; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.StreamingOutput; import org.apache.bookkeeper.mledger.AsyncCallbacks.ManagedLedgerInfoCallback; import org.apache.bookkeeper.mledger.Entry; import org.apache.bookkeeper.mledger.ManagedLedgerConfig; import org.apache.bookkeeper.mledger.ManagedLedgerException; import org.apache.bookkeeper.mledger.ManagedLedgerInfo; import org.apache.bookkeeper.mledger.impl.ManagedLedgerFactoryImpl; import org.apache.bookkeeper.mledger.impl.ManagedLedgerOfflineBacklog; import org.apache.bookkeeper.mledger.impl.PositionImpl; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.data.Stat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.yahoo.pulsar.broker.PulsarService; import com.yahoo.pulsar.broker.service.BrokerServiceException.NotAllowedException; import com.yahoo.pulsar.broker.service.BrokerServiceException.SubscriptionBusyException; import com.yahoo.pulsar.broker.service.BrokerServiceException.SubscriptionInvalidCursorPosition; import com.yahoo.pulsar.broker.service.BrokerServiceException.TopicBusyException; import com.yahoo.pulsar.broker.service.persistent.PersistentReplicator; import com.yahoo.pulsar.broker.service.persistent.PersistentSubscription; import com.yahoo.pulsar.broker.service.persistent.PersistentTopic; import com.yahoo.pulsar.broker.web.RestException; import com.yahoo.pulsar.client.admin.PulsarAdminException.NotFoundException; import com.yahoo.pulsar.client.admin.PulsarAdminException.PreconditionFailedException; import com.yahoo.pulsar.client.api.PulsarClientException; import com.yahoo.pulsar.common.api.Commands; import com.yahoo.pulsar.common.api.proto.PulsarApi.KeyValue; import com.yahoo.pulsar.common.api.proto.PulsarApi.MessageMetadata; import com.yahoo.pulsar.common.compression.CompressionCodec; import com.yahoo.pulsar.common.compression.CompressionCodecProvider; import com.yahoo.pulsar.common.naming.DestinationDomain; import com.yahoo.pulsar.common.naming.DestinationName; import com.yahoo.pulsar.common.partition.PartitionedTopicMetadata; import com.yahoo.pulsar.common.policies.data.AuthAction; import com.yahoo.pulsar.common.policies.data.AuthPolicies; import com.yahoo.pulsar.common.policies.data.PartitionedTopicStats; import com.yahoo.pulsar.common.policies.data.PersistentOfflineTopicStats; import com.yahoo.pulsar.common.policies.data.PersistentTopicInternalStats; import com.yahoo.pulsar.common.policies.data.PersistentTopicStats; import com.yahoo.pulsar.common.policies.data.Policies; import com.yahoo.pulsar.zookeeper.ZooKeeperCache.Deserializer; import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; /** */ @Path("/persistent") @Produces(MediaType.APPLICATION_JSON) @Api(value = "/persistent", description = "Persistent topic admin apis", tags = "persistent topic") public class PersistentTopics extends AdminResource { private static final Logger log = LoggerFactory.getLogger(PersistentTopics.class); private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSZ").withZone(ZoneId.systemDefault()); private static final String PARTITIONED_TOPIC_PATH_ZNODE = "partitioned-topics"; private static final int PARTITIONED_TOPIC_WAIT_SYNC_TIME_MS = 1000; private static final int OFFLINE_TOPIC_STAT_TTL_MINS = 10; @GET @Path("/{property}/{cluster}/{namespace}") @ApiOperation(value = "Get the list of destinations under a namespace.", response = String.class, responseContainer = "List") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Namespace doesn't exist") }) public List<String> getList(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) { validateAdminAccessOnProperty(property); // Validate that namespace exists, throws 404 if it doesn't exist try { policiesCache().get(path("policies", property, cluster, namespace)); } catch (KeeperException.NoNodeException e) { log.warn("[{}] Failed to get topic list {}/{}/{}: Namespace does not exist", clientAppId(), property, cluster, namespace); throw new RestException(Status.NOT_FOUND, "Namespace does not exist"); } catch (Exception e) { log.error("[{}] Failed to get topic list {}/{}/{}", clientAppId(), property, cluster, namespace, e); throw new RestException(e); } List<String> destinations = Lists.newArrayList(); try { String path = String.format("/managed-ledgers/%s/%s/%s/%s", property, cluster, namespace, domain()); for (String destination : managedLedgerListCache().get(path)) { if (domain().equals(DestinationDomain.persistent.toString())) { destinations.add(DestinationName .get(domain(), property, cluster, namespace, decode(destination)).toString()); } } } catch (KeeperException.NoNodeException e) { // NoNode means there are no destination in this domain for this namespace } catch (Exception e) { log.error("[{}] Failed to get destination list for namespace {}/{}/{}", clientAppId(), property, cluster, namespace, e); throw new RestException(e); } destinations.sort(null); return destinations; } @GET @Path("/{property}/{cluster}/{namespace}/partitioned") @ApiOperation(value = "Get the list of partitioned topics under a namespace.", response = String.class, responseContainer = "List") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Namespace doesn't exist") }) public List<String> getPartitionedTopicList(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) { validateAdminAccessOnProperty(property); // Validate that namespace exists, throws 404 if it doesn't exist try { policiesCache().get(path("policies", property, cluster, namespace)); } catch (KeeperException.NoNodeException e) { log.warn("[{}] Failed to get partitioned topic list {}/{}/{}: Namespace does not exist", clientAppId(), property, cluster, namespace); throw new RestException(Status.NOT_FOUND, "Namespace does not exist"); } catch (Exception e) { log.error("[{}] Failed to get partitioned topic list for namespace {}/{}/{}", clientAppId(), property, cluster, namespace, e); throw new RestException(e); } List<String> partitionedTopics = Lists.newArrayList(); try { String partitionedTopicPath = path(PARTITIONED_TOPIC_PATH_ZNODE, property, cluster, namespace, domain()); List<String> destinations = globalZk().getChildren(partitionedTopicPath, false); partitionedTopics = destinations.stream().map(s -> String.format("persistent://%s/%s/%s/%s", property, cluster, namespace, decode(s))).collect( Collectors.toList()); } catch (KeeperException.NoNodeException e) { // NoNode means there are no partitioned topics in this domain for this namespace } catch (Exception e) { log.error("[{}] Failed to get partitioned topic list for namespace {}/{}/{}", clientAppId(), property, cluster, namespace, e); throw new RestException(e); } partitionedTopics.sort(null); return partitionedTopics; } @GET @Path("/{property}/{cluster}/{namespace}/{destination}/permissions") @ApiOperation(value = "Get permissions on a destination.", notes = "Retrieve the effective permissions for a destination. These permissions are defined by the permissions set at the" + "namespace level combined (union) with any eventual specific permission set on the destination.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Namespace doesn't exist") }) public Map<String, Set<AuthAction>> getPermissionsOnDestination(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination) { // This operation should be reading from zookeeper and it should be allowed without having admin privileges destination = decode(destination); validateAdminAccessOnProperty(property); String destinationUri = DestinationName.get(domain(), property, cluster, namespace, destination).toString(); try { Policies policies = policiesCache().get(path("policies", property, cluster, namespace)) .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace does not exist")); Map<String, Set<AuthAction>> permissions = Maps.newTreeMap(); AuthPolicies auth = policies.auth_policies; // First add namespace level permissions for (String role : auth.namespace_auth.keySet()) { permissions.put(role, auth.namespace_auth.get(role)); } // Then add destination level permissions if (auth.destination_auth.containsKey(destinationUri)) { for (Map.Entry<String, Set<AuthAction>> entry : auth.destination_auth.get(destinationUri).entrySet()) { String role = entry.getKey(); Set<AuthAction> destinationPermissions = entry.getValue(); if (!permissions.containsKey(role)) { permissions.put(role, destinationPermissions); } else { // Do the union between namespace and destination level Set<AuthAction> union = Sets.union(permissions.get(role), destinationPermissions); permissions.put(role, union); } } } return permissions; } catch (Exception e) { log.error("[{}] Failed to get permissions for destination {}", clientAppId(), destinationUri, e); throw new RestException(e); } } protected void validateAdminOperationOnDestination(DestinationName fqdn, boolean authoritative) { validateAdminAccessOnProperty(fqdn.getProperty()); validateDestinationOwnership(fqdn, authoritative); } @POST @Path("/{property}/{cluster}/{namespace}/{destination}/permissions/{role}") @ApiOperation(value = "Grant a new permission to a role on a single destination.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Namespace doesn't exist"), @ApiResponse(code = 409, message = "Concurrent modification") }) public void grantPermissionsOnDestination(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @PathParam("role") String role, Set<AuthAction> actions) { destination = decode(destination); // This operation should be reading from zookeeper and it should be allowed without having admin privileges validateAdminAccessOnProperty(property); validatePoliciesReadOnlyAccess(); String destinationUri = DestinationName.get(domain(), property, cluster, namespace, destination).toString(); try { Stat nodeStat = new Stat(); byte[] content = globalZk().getData(path("policies", property, cluster, namespace), null, nodeStat); Policies policies = jsonMapper().readValue(content, Policies.class); if (!policies.auth_policies.destination_auth.containsKey(destinationUri)) { policies.auth_policies.destination_auth.put(destinationUri, new TreeMap<String, Set<AuthAction>>()); } policies.auth_policies.destination_auth.get(destinationUri).put(role, actions); // Write the new policies to zookeeper globalZk().setData(path("policies", property, cluster, namespace), jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion()); // invalidate the local cache to force update policiesCache().invalidate(path("policies", property, cluster, namespace)); log.info("[{}] Successfully granted access for role {}: {} - destination {}", clientAppId(), role, actions, destinationUri); } catch (KeeperException.NoNodeException e) { log.warn("[{}] Failed to grant permissions on destination {}: Namespace does not exist", clientAppId(), destinationUri); throw new RestException(Status.NOT_FOUND, "Namespace does not exist"); } catch (Exception e) { log.error("[{}] Failed to grant permissions for destination {}", clientAppId(), destinationUri, e); throw new RestException(e); } } @DELETE @Path("/{property}/{cluster}/{namespace}/{destination}/permissions/{role}") @ApiOperation(value = "Revoke permissions on a destination.", notes = "Revoke permissions to a role on a single destination. If the permission was not set at the destination" + "level, but rather at the namespace level, this operation will return an error (HTTP status code 412).") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Namespace doesn't exist"), @ApiResponse(code = 412, message = "Permissions are not set at the destination level") }) public void revokePermissionsOnDestination(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @PathParam("role") String role) { destination = decode(destination); // This operation should be reading from zookeeper and it should be allowed without having admin privileges validateAdminAccessOnProperty(property); validatePoliciesReadOnlyAccess(); String destinationUri = DestinationName.get(domain(), property, cluster, namespace, destination).toString(); Stat nodeStat = new Stat(); Policies policies; try { byte[] content = globalZk().getData(path("policies", property, cluster, namespace), null, nodeStat); policies = jsonMapper().readValue(content, Policies.class); } catch (KeeperException.NoNodeException e) { log.warn("[{}] Failed to revoke permissions on destination {}: Namespace does not exist", clientAppId(), destinationUri); throw new RestException(Status.NOT_FOUND, "Namespace does not exist"); } catch (Exception e) { log.error("[{}] Failed to revoke permissions for destination {}", clientAppId(), destinationUri, e); throw new RestException(e); } if (!policies.auth_policies.destination_auth.containsKey(destinationUri) || !policies.auth_policies.destination_auth.get(destinationUri).containsKey(role)) { log.warn("[{}] Failed to revoke permission from role {} on destination: Not set at destination level", clientAppId(), role, destinationUri); throw new RestException(Status.PRECONDITION_FAILED, "Permissions are not set at the destination level"); } policies.auth_policies.destination_auth.get(destinationUri).remove(role); try { // Write the new policies to zookeeper String namespacePath = path("policies", property, cluster, namespace); globalZk().setData(namespacePath, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion()); // invalidate the local cache to force update policiesCache().invalidate(namespacePath); globalZkCache().invalidate(namespacePath); log.info("[{}] Successfully revoke access for role {} - destination {}", clientAppId(), role, destinationUri); } catch (Exception e) { log.error("[{}] Failed to revoke permissions for destination {}", clientAppId(), destinationUri, e); throw new RestException(e); } } @PUT @Path("/{property}/{cluster}/{namespace}/{destination}/partitions") @ApiOperation(value = "Create a partitioned topic.", notes = "It needs to be called before creating a producer on a partitioned topic.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 409, message = "Partitioned topic already exist") }) public void createPartitionedTopic(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, int numPartitions, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); validateAdminAccessOnProperty(dn.getProperty()); if (numPartitions <= 1) { throw new RestException(Status.NOT_ACCEPTABLE, "Number of partitions should be more than 1"); } try { String path = path(PARTITIONED_TOPIC_PATH_ZNODE, property, cluster, namespace, domain(), dn.getEncodedLocalName()); byte[] data = jsonMapper().writeValueAsBytes(new PartitionedTopicMetadata(numPartitions)); zkCreateOptimistic(path, data); // we wait for the data to be synced in all quorums and the observers Thread.sleep(PARTITIONED_TOPIC_WAIT_SYNC_TIME_MS); log.info("[{}] Successfully created partitioned topic {}", clientAppId(), dn); } catch (KeeperException.NodeExistsException e) { log.warn("[{}] Failed to create already existing partitioned topic {}", clientAppId(), dn); throw new RestException(Status.CONFLICT, "Partitioned topic already exist"); } catch (Exception e) { log.error("[{}] Failed to create partitioned topic {}", clientAppId(), dn, e); throw new RestException(e); } } @GET @Path("/{property}/{cluster}/{namespace}/{destination}/partitions") @ApiOperation(value = "Get partitioned topic metadata.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") }) public PartitionedTopicMetadata getPartitionedMetadata(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); return getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); } @DELETE @Path("/{property}/{cluster}/{namespace}/{destination}/partitions") @ApiOperation(value = "Delete a partitioned topic.", notes = "It will also delete all the partitions of the topic if it exists.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Partitioned topic does not exist") }) public void deletePartitionedTopic(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); validateAdminAccessOnProperty(dn.getProperty()); PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); int numPartitions = partitionMetadata.partitions; if (numPartitions > 0) { final CompletableFuture<Void> future = new CompletableFuture<>(); final AtomicInteger count = new AtomicInteger(numPartitions); try { for (int i = 0; i < numPartitions; i++) { DestinationName dn_partition = dn.getPartition(i); pulsar().getAdminClient().persistentTopics().deleteAsync(dn_partition.toString()) .whenComplete((r, ex) -> { if (ex != null) { if (ex instanceof NotFoundException) { // if the sub-topic is not found, the client might not have called create // producer or it might have been deleted earlier, so we ignore the 404 error. // For all other exception, we fail the delete partition method even if a single // partition is failed to be deleted if (log.isDebugEnabled()) { log.debug("[{}] Partition not found: {}", clientAppId(), dn_partition); } } else { future.completeExceptionally(ex); log.error("[{}] Failed to delete partition {}", clientAppId(), dn_partition, ex); return; } } else { log.info("[{}] Deleted partition {}", clientAppId(), dn_partition); } if (count.decrementAndGet() == 0) { future.complete(null); } }); } future.get(); } catch (Exception e) { Throwable t = e.getCause(); if (t instanceof PreconditionFailedException) { throw new RestException(Status.PRECONDITION_FAILED, "Topic has active producers/subscriptions"); } else { throw new RestException(t); } } } // Only tries to delete the znode for partitioned topic when all its partitions are successfully deleted String path = path(PARTITIONED_TOPIC_PATH_ZNODE, property, cluster, namespace, domain(), dn.getEncodedLocalName()); try { globalZk().delete(path, -1); globalZkCache().invalidate(path); // we wait for the data to be synced in all quorums and the observers Thread.sleep(PARTITIONED_TOPIC_WAIT_SYNC_TIME_MS); log.info("[{}] Deleted partitioned topic {}", clientAppId(), dn); } catch (KeeperException.NoNodeException nne) { throw new RestException(Status.NOT_FOUND, "Partitioned topic does not exist"); } catch (Exception e) { log.error("[{}] Failed to delete partitioned topic {}", clientAppId(), dn, e); throw new RestException(e); } } @DELETE @Path("/{property}/{cluster}/{namespace}/{destination}") @ApiOperation(value = "Delete a topic.", notes = "The topic cannot be deleted if there's any active subscription or producer connected to the it.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic does not exist"), @ApiResponse(code = 412, message = "Topic has active producers/subscriptions") }) public void deleteTopic(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); validateAdminOperationOnDestination(dn, authoritative); PersistentTopic topic = getTopicReference(dn); if (dn.isGlobal()) { // Delete is disallowed on global topic log.error("[{}] Delete topic is forbidden on global namespace {}", clientAppId(), dn); throw new RestException(Status.FORBIDDEN, "Delete forbidden on global namespace"); } try { topic.delete().get(); log.info("[{}] Successfully removed topic {}", clientAppId(), dn); } catch (Exception e) { Throwable t = e.getCause(); log.error("[{}] Failed to get delete topic {}", clientAppId(), dn, t); if (t instanceof TopicBusyException) { throw new RestException(Status.PRECONDITION_FAILED, "Topic has active producers/subscriptions"); } else { throw new RestException(t); } } } @GET @Path("/{property}/{cluster}/{namespace}/{destination}/subscriptions") @ApiOperation(value = "Get the list of persistent subscriptions for a given topic.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic does not exist") }) public List<String> getSubscriptions(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); List<String> subscriptions = Lists.newArrayList(); PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); if (partitionMetadata.partitions > 0) { try { // get the subscriptions only from the 1st partition since all the other partitions will have the same // subscriptions subscriptions.addAll( pulsar().getAdminClient().persistentTopics().getSubscriptions(dn.getPartition(0).toString())); } catch (Exception e) { throw new RestException(e); } } else { validateAdminOperationOnDestination(dn, authoritative); PersistentTopic topic = getTopicReference(dn); try { topic.getSubscriptions().forEach((subName, sub) -> subscriptions.add(subName)); } catch (Exception e) { log.error("[{}] Failed to get list of subscriptions for {}", clientAppId(), dn); throw new RestException(e); } } return subscriptions; } @GET @Path("{property}/{cluster}/{namespace}/{destination}/stats") @ApiOperation(value = "Get the stats for the topic.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic does not exist") }) public PersistentTopicStats getStats(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); validateAdminOperationOnDestination(dn, authoritative); PersistentTopic topic = getTopicReference(dn); return topic.getStats(); } @GET @Path("{property}/{cluster}/{namespace}/{destination}/internalStats") @ApiOperation(value = "Get the internal stats for the topic.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic does not exist") }) public PersistentTopicInternalStats getInternalStats(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); validateAdminOperationOnDestination(dn, authoritative); PersistentTopic topic = getTopicReference(dn); return topic.getInternalStats(); } @GET @Path("{property}/{cluster}/{namespace}/{destination}/internal-info") @ApiOperation(value = "Get the internal stats for the topic.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic does not exist") }) public void getManagedLedgerInfo(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @Suspended AsyncResponse asyncResponse) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); validateAdminAccessOnProperty(dn.getProperty()); String managedLedger = dn.getPersistenceNamingEncoding(); pulsar().getManagedLedgerFactory().asyncGetManagedLedgerInfo(managedLedger, new ManagedLedgerInfoCallback() { @Override public void getInfoComplete(ManagedLedgerInfo info, Object ctx) { asyncResponse.resume((StreamingOutput) output -> { jsonMapper().writer().writeValue(output, info); }); } @Override public void getInfoFailed(ManagedLedgerException exception, Object ctx) { asyncResponse.resume(exception); } }, null); } @GET @Path("{property}/{cluster}/{namespace}/{destination}/partitioned-stats") @ApiOperation(value = "Get the stats for the partitioned topic.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic does not exist") }) public PartitionedTopicStats getPartitionedStats(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); if (partitionMetadata.partitions == 0) { throw new RestException(Status.NOT_FOUND, "Partitioned Topic not found"); } PartitionedTopicStats stats = new PartitionedTopicStats(partitionMetadata); try { for (int i = 0; i < partitionMetadata.partitions; i++) { PersistentTopicStats partitionStats = pulsar().getAdminClient().persistentTopics() .getStats(dn.getPartition(i).toString()); stats.add(partitionStats); stats.partitions.put(dn.getPartition(i).toString(), partitionStats); } } catch (Exception e) { throw new RestException(e); } return stats; } @DELETE @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}") @ApiOperation(value = "Delete a subscription.", notes = "There should not be any active consumers on the subscription.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic does not exist"), @ApiResponse(code = 412, message = "Subscription has active consumers") }) public void deleteSubscription(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @PathParam("subName") String subName, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); if (partitionMetadata.partitions > 0) { try { for (int i = 0; i < partitionMetadata.partitions; i++) { pulsar().getAdminClient().persistentTopics().deleteSubscription(dn.getPartition(i).toString(), subName); } } catch (Exception e) { throw new RestException(e); } } else { validateAdminOperationOnDestination(dn, authoritative); PersistentTopic topic = getTopicReference(dn); try { PersistentSubscription sub = topic.getPersistentSubscription(subName); checkNotNull(sub); sub.delete().get(); log.info("[{}][{}] Deleted subscription {}", clientAppId(), dn, subName); } catch (Exception e) { Throwable t = e.getCause(); log.error("[{}] Failed to delete subscription {} {}", clientAppId(), dn, subName, e); if (e instanceof NullPointerException) { throw new RestException(Status.NOT_FOUND, "Subscription not found"); } else if (t instanceof SubscriptionBusyException) { throw new RestException(Status.PRECONDITION_FAILED, "Subscription has active connected consumers"); } else { throw new RestException(t); } } } } @POST @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/skip_all") @ApiOperation(value = "Skip all messages on a topic subscription.", notes = "Completely clears the backlog on the subscription.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic or subscription does not exist") }) public void skipAllMessages(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @PathParam("subName") String subName, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); if (partitionMetadata.partitions > 0) { try { for (int i = 0; i < partitionMetadata.partitions; i++) { pulsar().getAdminClient().persistentTopics().skipAllMessages(dn.getPartition(i).toString(), subName); } } catch (Exception e) { throw new RestException(e); } } else { validateAdminOperationOnDestination(dn, authoritative); PersistentTopic topic = getTopicReference(dn); try { if (subName.startsWith(topic.replicatorPrefix)) { String remoteCluster = PersistentReplicator.getRemoteCluster(subName); PersistentReplicator repl = topic.getPersistentReplicator(remoteCluster); checkNotNull(repl); repl.clearBacklog().get(); } else { PersistentSubscription sub = topic.getPersistentSubscription(subName); checkNotNull(sub); sub.clearBacklog().get(); } log.info("[{}] Cleared backlog on {} {}", clientAppId(), dn, subName); } catch (NullPointerException npe) { throw new RestException(Status.NOT_FOUND, "Subscription not found"); } catch (Exception exception) { log.error("[{}] Failed to skip all messages {} {}", clientAppId(), dn, subName, exception); throw new RestException(exception); } } } @POST @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/skip/{numMessages}") @ApiOperation(value = "Skip messages on a topic subscription.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic or subscription does not exist") }) public void skipMessages(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @PathParam("subName") String subName, @PathParam("numMessages") int numMessages, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); if (partitionMetadata.partitions > 0) { throw new RestException(Status.METHOD_NOT_ALLOWED, "Skip messages on a partitioned topic is not allowed"); } validateAdminOperationOnDestination(dn, authoritative); PersistentTopic topic = getTopicReference(dn); try { if (subName.startsWith(topic.replicatorPrefix)) { String remoteCluster = PersistentReplicator.getRemoteCluster(subName); PersistentReplicator repl = topic.getPersistentReplicator(remoteCluster); checkNotNull(repl); repl.skipMessages(numMessages).get(); } else { PersistentSubscription sub = topic.getPersistentSubscription(subName); checkNotNull(sub); sub.skipMessages(numMessages).get(); } log.info("[{}] Skipped {} messages on {} {}", clientAppId(), numMessages, dn, subName); } catch (NullPointerException npe) { throw new RestException(Status.NOT_FOUND, "Subscription not found"); } catch (Exception exception) { log.error("[{}] Failed to skip {} messages {} {}", clientAppId(), numMessages, dn, subName, exception); throw new RestException(exception); } } @POST @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/expireMessages/{expireTimeInSeconds}") @ApiOperation(value = "Expire messages on a topic subscription.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic or subscription does not exist") }) public void expireTopicMessages(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @PathParam("subName") String subName, @PathParam("expireTimeInSeconds") int expireTimeInSeconds, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); expireMessages(property, cluster, namespace, destination, subName, expireTimeInSeconds, authoritative); } @POST @Path("/{property}/{cluster}/{namespace}/{destination}/all_subscription/expireMessages/{expireTimeInSeconds}") @ApiOperation(value = "Expire messages on all subscriptions of topic.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic or subscription does not exist") }) public void expireMessagesForAllSubscriptions(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destinationName, @PathParam("expireTimeInSeconds") int expireTimeInSeconds, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { final String destination = decode(destinationName); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); if (partitionMetadata.partitions > 0) { try { // expire messages for each partition destination for (int i = 0; i < partitionMetadata.partitions; i++) { pulsar().getAdminClient().persistentTopics() .expireMessagesForAllSubscriptions(dn.getPartition(i).toString(), expireTimeInSeconds); } } catch (Exception e) { log.error("[{}] Failed to expire messages up to {} on {} {}", clientAppId(), expireTimeInSeconds, dn, e); throw new RestException(e); } } else { // validate ownership and redirect if current broker is not owner validateAdminOperationOnDestination(dn, authoritative); PersistentTopic topic = getTopicReference(dn); topic.getReplicators().forEach((subName, replicator) -> { expireMessages(property, cluster, namespace, destination, subName, expireTimeInSeconds, authoritative); }); topic.getSubscriptions().forEach((subName, subscriber) -> { expireMessages(property, cluster, namespace, destination, subName, expireTimeInSeconds, authoritative); }); } } @POST @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/resetcursor/{timestamp}") @ApiOperation(value = "Reset subscription to message position closest to absolute timestamp (in ms).", notes = "There should not be any active consumers on the subscription.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic does not exist"), @ApiResponse(code = 405, message = "Not supported for global topics"), @ApiResponse(code = 412, message = "Subscription has active consumers") }) public void resetCursor(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @PathParam("subName") String subName, @PathParam("timestamp") long timestamp, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); if (partitionMetadata.partitions > 0) { int numParts = partitionMetadata.partitions; int numPartException = 0; Exception partitionException = null; try { for (int i = 0; i < numParts; i++) { pulsar().getAdminClient().persistentTopics().resetCursor(dn.getPartition(i).toString(), subName, timestamp); } } catch (PreconditionFailedException pfe) { // throw the last exception if all partitions get this error // any other exception on partition is reported back to user ++numPartException; partitionException = pfe; } catch (Exception e) { log.warn("[{}] [{}] Failed to reset cursor on subscription {} to time {}", clientAppId(), dn, subName, timestamp, e); throw new RestException(e); } // report an error to user if unable to reset for all partitions if (numPartException == numParts) { log.warn("[{}] [{}] Failed to reset cursor on subscription {} to time {}", clientAppId(), dn, subName, timestamp, partitionException); throw new RestException(Status.PRECONDITION_FAILED, partitionException.getMessage()); } else if (numPartException > 0 && log.isDebugEnabled()) { log.debug("[{}][{}] partial errors for reset cursor on subscription {} to time {} - ", clientAppId(), destination, subName, timestamp, partitionException); } } else { validateAdminOperationOnDestination(dn, authoritative); log.info("[{}][{}] received reset cursor on subscription {} to time {}", clientAppId(), destination, subName, timestamp); PersistentTopic topic = getTopicReference(dn); try { PersistentSubscription sub = topic.getPersistentSubscription(subName); checkNotNull(sub); sub.resetCursor(timestamp).get(); log.info("[{}][{}] reset cursor on subscription {} to time {}", clientAppId(), dn, subName, timestamp); } catch (Exception e) { Throwable t = e.getCause(); log.warn("[{}] [{}] Failed to reset cursor on subscription {} to time {}", clientAppId(), dn, subName, timestamp, e); if (e instanceof NullPointerException) { throw new RestException(Status.NOT_FOUND, "Subscription not found"); } else if (e instanceof NotAllowedException) { throw new RestException(Status.METHOD_NOT_ALLOWED, e.getMessage()); } else if (t instanceof SubscriptionBusyException) { throw new RestException(Status.PRECONDITION_FAILED, "Subscription has active connected consumers"); } else if (t instanceof SubscriptionInvalidCursorPosition) { throw new RestException(Status.PRECONDITION_FAILED, "Unable to find position for timestamp specified -" + t.getMessage()); } else { throw new RestException(e); } } } } @GET @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/position/{messagePosition}") @ApiOperation(value = "Peek nth message on a topic subscription.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Topic, subscription or the message position does not exist") }) public Response peekNthMessage(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @PathParam("subName") String subName, @PathParam("messagePosition") int messagePosition, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); if (partitionMetadata.partitions > 0) { throw new RestException(Status.METHOD_NOT_ALLOWED, "Peek messages on a partitioned topic is not allowed"); } validateAdminOperationOnDestination(dn, authoritative); PersistentTopic topic = getTopicReference(dn); PersistentReplicator repl = null; PersistentSubscription sub = null; Entry entry = null; if (subName.startsWith(topic.replicatorPrefix)) { repl = getReplicatorReference(subName, topic); } else { sub = getSubscriptionReference(subName, topic); } try { if (subName.startsWith(topic.replicatorPrefix)) { entry = repl.peekNthMessage(messagePosition).get(); } else { entry = sub.peekNthMessage(messagePosition).get(); } checkNotNull(entry); PositionImpl pos = (PositionImpl) entry.getPosition(); ByteBuf metadataAndPayload = entry.getDataBuffer(); // moves the readerIndex to the payload MessageMetadata metadata = Commands.parseMessageMetadata(metadataAndPayload); ResponseBuilder responseBuilder = Response.ok(); responseBuilder.header("X-Pulsar-Message-ID", pos.toString()); for (KeyValue keyValue : metadata.getPropertiesList()) { responseBuilder.header("X-Pulsar-PROPERTY-" + keyValue.getKey(), keyValue.getValue()); } if (metadata.hasPublishTime()) { responseBuilder.header("X-Pulsar-publish-time", DATE_FORMAT.format(Instant.ofEpochMilli(metadata.getPublishTime()))); } // Decode if needed CompressionCodec codec = CompressionCodecProvider.getCompressionCodec(metadata.getCompression()); ByteBuf uncompressedPayload = codec.decode(metadataAndPayload, metadata.getUncompressedSize()); // Copy into a heap buffer for output stream compatibility ByteBuf data = PooledByteBufAllocator.DEFAULT.heapBuffer(uncompressedPayload.readableBytes(), uncompressedPayload.readableBytes()); data.writeBytes(uncompressedPayload); uncompressedPayload.release(); StreamingOutput stream = new StreamingOutput() { @Override public void write(OutputStream output) throws IOException, WebApplicationException { output.write(data.array(), data.arrayOffset(), data.readableBytes()); data.release(); } }; return responseBuilder.entity(stream).build(); } catch (NullPointerException npe) { throw new RestException(Status.NOT_FOUND, "Message not found"); } catch (Exception exception) { log.error("[{}] Failed to get message at position {} from {} {}", clientAppId(), messagePosition, dn, subName, exception); throw new RestException(exception); } finally { if (entry != null) { entry.release(); } } } @GET @Path("{property}/{cluster}/{namespace}/{destination}/backlog") @ApiOperation(value = "Get estimated backlog for offline topic.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Namespace does not exist") }) public PersistentOfflineTopicStats getBacklog(@PathParam("property") String property, @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) { destination = decode(destination); validateAdminAccessOnProperty(property); // Validate that namespace exists, throw 404 if it doesn't exist // note that we do not want to load the topic and hence skip validateAdminOperationOnDestination() try { policiesCache().get(path("policies", property, cluster, namespace)); } catch (KeeperException.NoNodeException e) { log.warn("[{}] Failed to get topic backlog {}/{}/{}: Namespace does not exist", clientAppId(), property, cluster, namespace); throw new RestException(Status.NOT_FOUND, "Namespace does not exist"); } catch (Exception e) { log.error("[{}] Failed to get topic backlog {}/{}/{}", clientAppId(), property, cluster, namespace, e); throw new RestException(e); } DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); PersistentOfflineTopicStats offlineTopicStats = null; try { offlineTopicStats = pulsar().getBrokerService().getOfflineTopicStat(dn); if (offlineTopicStats != null) { // offline topic stat has a cost - so use cached value until TTL long elapsedMs = System.currentTimeMillis() - offlineTopicStats.statGeneratedAt.getTime(); if (TimeUnit.MINUTES.convert(elapsedMs, TimeUnit.MILLISECONDS) < OFFLINE_TOPIC_STAT_TTL_MINS) { return offlineTopicStats; } } final ManagedLedgerConfig config = pulsar().getBrokerService().getManagedLedgerConfig(dn).get(); ManagedLedgerOfflineBacklog offlineTopicBacklog = new ManagedLedgerOfflineBacklog(config.getDigestType(), config.getPassword(), pulsar().getAdvertisedAddress(), false); offlineTopicStats = offlineTopicBacklog .estimateUnloadedTopicBacklog((ManagedLedgerFactoryImpl) pulsar().getManagedLedgerFactory(), dn); pulsar().getBrokerService().cacheOfflineTopicStats(dn, offlineTopicStats); } catch (Exception exception) { throw new RestException(exception); } return offlineTopicStats; } public void expireMessages(String property, String cluster, String namespace, String destination, String subName, int expireTimeInSeconds, boolean authoritative) { DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative); if (partitionMetadata.partitions > 0) { // expire messages for each partition destination try { for (int i = 0; i < partitionMetadata.partitions; i++) { pulsar().getAdminClient().persistentTopics().expireMessages(dn.getPartition(i).toString(), subName, expireTimeInSeconds); } } catch (Exception e) { throw new RestException(e); } } else { // validate ownership and redirect if current broker is not owner validateAdminOperationOnDestination(dn, authoritative); PersistentTopic topic = getTopicReference(dn); try { if (subName.startsWith(topic.replicatorPrefix)) { String remoteCluster = PersistentReplicator.getRemoteCluster(subName); PersistentReplicator repl = topic.getPersistentReplicator(remoteCluster); checkNotNull(repl); repl.expireMessages(expireTimeInSeconds); } else { PersistentSubscription sub = topic.getPersistentSubscription(subName); checkNotNull(sub); sub.expireMessages(expireTimeInSeconds); } log.info("[{}] Message expire started up to {} on {} {}", clientAppId(), expireTimeInSeconds, dn, subName); } catch (NullPointerException npe) { throw new RestException(Status.NOT_FOUND, "Subscription not found"); } catch (Exception exception) { log.error("[{}] Failed to expire messages up to {} on {} with subscription {} {}", clientAppId(), expireTimeInSeconds, dn, subName, exception); throw new RestException(exception); } } } public PartitionedTopicMetadata getPartitionedTopicMetadata(String property, String cluster, String namespace, String destination, boolean authoritative) { DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination); validateClusterOwnership(dn.getCluster()); try { checkConnect(dn); } catch (WebApplicationException e) { validateAdminAccessOnProperty(dn.getProperty()); } catch (Exception e) { // unknown error marked as internal server error log.warn("Unexpected error while authorizing lookup. destination={}, role={}. Error: {}", destination, clientAppId(), e.getMessage(), e); throw new RestException(e); } String path = path(PARTITIONED_TOPIC_PATH_ZNODE, property, cluster, namespace, domain(), dn.getEncodedLocalName()); PartitionedTopicMetadata partitionMetadata = fetchPartitionedTopicMetadata(pulsar(), path); if (log.isDebugEnabled()) { log.debug("[{}] Total number of partitions for topic {} is {}", clientAppId(), dn, partitionMetadata.partitions); } return partitionMetadata; } public static CompletableFuture<PartitionedTopicMetadata> getPartitionedTopicMetadata(PulsarService pulsar, String clientAppId, DestinationName dn) { CompletableFuture<PartitionedTopicMetadata> metadataFuture = new CompletableFuture<>(); try { // (1) authorize client try { checkAuthorization(pulsar, dn, clientAppId); } catch (RestException e) { try { validateAdminAccessOnProperty(pulsar, clientAppId, dn.getProperty()); } catch (RestException authException) { log.warn("Failed to authorize {} on cluster {}", clientAppId, dn.toString()); throw new PulsarClientException(String.format("Authorization failed %s on cluster %s with error %s", clientAppId, dn.toString(), authException.getMessage())); } } catch (Exception ex) { // throw without wrapping to PulsarClientException that considers: unknown error marked as internal // server error log.warn("Failed to authorize {} on cluster {} with unexpected exception {}", clientAppId, dn.toString(), ex.getMessage(), ex); throw ex; } String path = path(PARTITIONED_TOPIC_PATH_ZNODE, dn.getProperty(), dn.getCluster(), dn.getNamespacePortion(), "persistent", dn.getEncodedLocalName()); fetchPartitionedTopicMetadataAsync(pulsar, path).thenAccept(metadata -> { if (log.isDebugEnabled()) { log.debug("[{}] Total number of partitions for topic {} is {}", clientAppId, dn, metadata.partitions); } metadataFuture.complete(metadata); }).exceptionally(ex -> { metadataFuture.completeExceptionally(ex); return null; }); } catch (Exception ex) { metadataFuture.completeExceptionally(ex); } return metadataFuture; } private static PartitionedTopicMetadata fetchPartitionedTopicMetadata(PulsarService pulsar, String path) { try { return fetchPartitionedTopicMetadataAsync(pulsar, path).get(); } catch (Exception e) { if (e.getCause() instanceof RestException) { throw (RestException) e; } throw new RestException(e); } } private static CompletableFuture<PartitionedTopicMetadata> fetchPartitionedTopicMetadataAsync(PulsarService pulsar, String path) { CompletableFuture<PartitionedTopicMetadata> metadataFuture = new CompletableFuture<>(); try { // gets the number of partitions from the zk cache pulsar.getGlobalZkCache().getDataAsync(path, new Deserializer<PartitionedTopicMetadata>() { @Override public PartitionedTopicMetadata deserialize(String key, byte[] content) throws Exception { return jsonMapper().readValue(content, PartitionedTopicMetadata.class); } }).thenAccept(metadata -> { // if the partitioned topic is not found in zk, then the topic is not partitioned if (metadata.isPresent()) { metadataFuture.complete(metadata.get()); } else { metadataFuture.complete(new PartitionedTopicMetadata()); } }).exceptionally(ex -> { metadataFuture.completeExceptionally(ex); return null; }); } catch (Exception e) { metadataFuture.completeExceptionally(e); } return metadataFuture; } /** * Get the Topic object reference from the Pulsar broker */ private PersistentTopic getTopicReference(DestinationName dn) { try { PersistentTopic topic = (PersistentTopic) pulsar().getBrokerService().getTopicReference(dn.toString()); checkNotNull(topic); return topic; } catch (Exception e) { throw new RestException(Status.NOT_FOUND, "Topic not found"); } } /** * Get the Subscription object reference from the Topic reference */ private PersistentSubscription getSubscriptionReference(String subName, PersistentTopic topic) { try { PersistentSubscription sub = topic.getPersistentSubscription(subName); return checkNotNull(sub); } catch (Exception e) { throw new RestException(Status.NOT_FOUND, "Subscription not found"); } } /** * Get the Replicator object reference from the Topic reference */ private PersistentReplicator getReplicatorReference(String replName, PersistentTopic topic) { try { String remoteCluster = PersistentReplicator.getRemoteCluster(replName); PersistentReplicator repl = topic.getPersistentReplicator(remoteCluster); return checkNotNull(repl); } catch (Exception e) { throw new RestException(Status.NOT_FOUND, "Replicator not found"); } } }