From 9fd8ac3ed914dd1bb3ab7e34bb229fb5eee28651 Mon Sep 17 00:00:00 2001 From: lushiji Date: Mon, 3 Nov 2025 08:48:07 +0800 Subject: [PATCH 1/2] [fix] Formatting code(mvn spotless:apply) --- pom.xml | 144 +- pulsar-admin-mcp-contrib/pom.xml | 109 +- .../org/apache/pulsar/admin/mcp/Main.java | 22 +- .../admin/mcp/client/PulsarClientManager.java | 182 +-- .../admin/mcp/config/PulsarMCPCliOptions.java | 103 +- .../admin/mcp/tools/BasePulsarTools.java | 333 ++-- .../pulsar/admin/mcp/tools/ClusterTools.java | 1356 ++++++++-------- .../pulsar/admin/mcp/tools/MessageTools.java | 1165 +++++++------- .../admin/mcp/tools/MonitoringTools.java | 1255 +++++++-------- .../admin/mcp/tools/NamespaceTools.java | 980 ++++++------ .../pulsar/admin/mcp/tools/SchemaTools.java | 580 +++---- .../admin/mcp/tools/SubscriptionTools.java | 924 +++++------ .../pulsar/admin/mcp/tools/TenantTools.java | 694 +++++---- .../pulsar/admin/mcp/tools/TopicTools.java | 1364 +++++++++-------- .../mcp/transport/AbstractMCPServer.java | 366 ++--- .../admin/mcp/transport/HttpMCPServer.java | 212 +-- .../admin/mcp/transport/StdioMCPServer.java | 123 +- .../pulsar/admin/mcp/transport/Transport.java | 12 +- .../mcp/transport/TransportLauncher.java | 81 +- .../admin/mcp/transport/TransportManager.java | 23 +- .../mcp/client/PulsarClientManagerTest.java | 277 ++-- .../mcp/config/PulsarMCPCliOptionsTest.java | 335 ++-- .../admin/mcp/tools/BasePulsarToolsTest.java | 691 +++++---- .../mcp/transport/HttpMCPServerTest.java | 285 ++-- .../mcp/transport/StdioMCPServerTest.java | 322 ++-- .../mcp/transport/TransportManagerTest.java | 245 ++- pulsar-auth-contrib/pom.xml | 2 +- pulsar-bookkeeper-contrib/pom.xml | 2 +- pulsar-client-common-contrib/pom.xml | 2 +- pulsar-connector-contrib/pom.xml | 2 +- pulsar-function-contrib/pom.xml | 2 +- pulsar-interceptor-contrib/pom.xml | 2 +- pulsar-loadbalance-contrib/pom.xml | 2 +- pulsar-metrics-contrib/pom.xml | 2 +- pulsar-rpc-contrib/pom.xml | 19 +- .../client/DefaultRequestCallBack.java | 72 +- .../rpc/contrib/client/PulsarRpcClient.java | 257 ++-- .../client/PulsarRpcClientBuilder.java | 137 +- .../client/PulsarRpcClientBuilderImpl.java | 123 +- .../contrib/client/PulsarRpcClientImpl.java | 332 ++-- .../rpc/contrib/client/ReplyListener.java | 92 +- .../rpc/contrib/client/RequestCallBack.java | 146 +- .../rpc/contrib/client/RequestSender.java | 44 +- .../rpc/contrib/client/package-info.java | 39 +- .../pulsar/rpc/contrib/common/Constants.java | 14 +- .../common/MessageDispatcherFactory.java | 180 ++- .../common/PulsarRpcClientException.java | 39 +- .../common/PulsarRpcServerException.java | 39 +- .../rpc/contrib/common/package-info.java | 21 +- .../rpc/contrib/server/PulsarRpcServer.java | 51 +- .../server/PulsarRpcServerBuilder.java | 102 +- .../server/PulsarRpcServerBuilderImpl.java | 154 +- .../contrib/server/PulsarRpcServerImpl.java | 108 +- .../server/ReplyProducerPoolFactory.java | 90 +- .../rpc/contrib/server/ReplySender.java | 151 +- .../rpc/contrib/server/RequestListener.java | 108 +- .../rpc/contrib/server/package-info.java | 35 +- .../rpc/contrib/PulsarRpcClientTest.java | 349 +++-- .../rpc/contrib/PulsarRpcServerTest.java | 116 +- .../pulsar/rpc/contrib/SimpleRpcCallTest.java | 1113 ++++++++------ .../rpc/contrib/base/PulsarRpcBase.java | 94 +- .../base/SingletonPulsarContainer.java | 71 +- .../pulsar/txn/SingletonPulsarContainer.java | 74 +- .../apache/pulsar/txn/TransactionDemo.java | 13 +- 64 files changed, 8497 insertions(+), 7885 deletions(-) diff --git a/pom.xml b/pom.xml index 3321a33..2367ab1 100644 --- a/pom.xml +++ b/pom.xml @@ -21,14 +21,26 @@ apache 29 - 1.0.0-SNAPSHOT - - 2024 pulsar-java-contrib + 1.0.0-SNAPSHOT pom Pulsar Java Contrib + + pulsar-client-common-contrib + pulsar-loadbalance-contrib + pulsar-interceptor-contrib + pulsar-connector-contrib + pulsar-function-contrib + pulsar-bookkeeper-contrib + pulsar-transaction-contrib + pulsar-metrics-contrib + pulsar-auth-contrib + pulsar-rpc-contrib + pulsar-admin-mcp-contrib + + 1.18.32 2.0.13 @@ -52,20 +64,6 @@ 5.12.0 - - pulsar-client-common-contrib - pulsar-loadbalance-contrib - pulsar-interceptor-contrib - pulsar-connector-contrib - pulsar-function-contrib - pulsar-bookkeeper-contrib - pulsar-transaction-contrib - pulsar-metrics-contrib - pulsar-auth-contrib - pulsar-rpc-contrib - pulsar-admin-mcp-contrib - - @@ -102,6 +100,12 @@ + + org.awaitility + awaitility + ${awaitility.version} + test + org.projectlombok lombok @@ -122,34 +126,9 @@ ${org.testing.version} test - - org.awaitility - awaitility - ${awaitility.version} - test - - - org.apache.maven.plugins - maven-wrapper-plugin - ${maven-wrapper-plugin.version} - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - - - org.projectlombok - lombok - ${lombok.version} - - - - com.diffplug.spotless @@ -195,36 +174,6 @@ - - - org.apache.maven.plugins - maven-checkstyle-plugin - ${maven-checkstyle-plugin.version} - - - com.puppycrawl.tools - checkstyle - ${puppycrawl.checkstyle.version} - - - - ./src/main/resources/checkstyle.xml - ./src/main/resources/suppressions.xml - true - UTF-8 - **/*.proto - - - - - validate-checkstyle - validate - - check - - - - @@ -267,6 +216,57 @@ + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + ./src/main/resources/checkstyle.xml + ./src/main/resources/suppressions.xml + true + UTF-8 + **/*.proto + + + + + com.puppycrawl.tools + checkstyle + ${puppycrawl.checkstyle.version} + + + + + validate-checkstyle + + check + + validate + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + org.apache.maven.plugins + maven-wrapper-plugin + ${maven-wrapper-plugin.version} + + + 2024 diff --git a/pulsar-admin-mcp-contrib/pom.xml b/pulsar-admin-mcp-contrib/pom.xml index d0ff264..35cb110 100644 --- a/pulsar-admin-mcp-contrib/pom.xml +++ b/pulsar-admin-mcp-contrib/pom.xml @@ -14,9 +14,7 @@ limitations under the License. --> - + 4.0.0 @@ -58,19 +56,47 @@ + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + io.modelcontextprotocol.sdk + mcp + 0.12.0 + + + org.apache.commons + commons-lang3 + 3.18.0 + + + org.apache.pulsar + pulsar-client + org.apache.pulsar pulsar-client-admin test + + org.apache.pulsar + pulsar-client-admin + org.eclipse.jetty - jetty-server + jetty-http ${jetty.version} org.eclipse.jetty - jetty-servlet + jetty-io ${jetty.version} @@ -80,12 +106,12 @@ org.eclipse.jetty - jetty-webapp + jetty-server ${jetty.version} org.eclipse.jetty - jetty-io + jetty-servlet ${jetty.version} @@ -95,37 +121,9 @@ org.eclipse.jetty - jetty-http + jetty-webapp ${jetty.version} - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - - io.modelcontextprotocol.sdk - mcp - 0.12.0 - - - org.apache.pulsar - pulsar-client - - - org.apache.pulsar - pulsar-client-admin - - - org.apache.commons - commons-lang3 - 3.18.0 - org.mockito mockito-core @@ -139,8 +137,7 @@ org.springframework.boot - spring-boot-starter-test - test + spring-boot-starter org.springframework.boot @@ -150,7 +147,8 @@ org.springframework.boot - spring-boot-starter-validation + spring-boot-starter-test + test org.springframework.boot @@ -158,14 +156,9 @@ - - org.testcontainers - pulsar - test - org.springframework.boot - spring-boot-starter + spring-boot-starter-validation org.springframework.boot @@ -173,10 +166,23 @@ + + org.testcontainers + pulsar + test + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + org.springframework.boot spring-boot-maven-plugin @@ -186,22 +192,15 @@ - repackage + + repackage + - - org.apache.maven.plugins - maven-compiler-plugin - - ${maven.compiler.source} - ${maven.compiler.target} - - 2024 - diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/Main.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/Main.java index f9775d3..c59ccfc 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/Main.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/Main.java @@ -19,17 +19,17 @@ import org.slf4j.LoggerFactory; public class Main { - private static final Logger logger = LoggerFactory.getLogger(Main.class); + private static final Logger logger = LoggerFactory.getLogger(Main.class); - public static void main(String[] args) { - try { - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); - logger.info("Starting Pulsar Admin MCP Server with options: {}", options); - TransportLauncher.start(options); - } catch (Exception e) { - logger.error("Fatal error starting Pulsar Admin MCP Server: {}", e.getMessage(), e); - System.err.println("Fatal error: " + e.getMessage()); - System.exit(-1); - } + public static void main(String[] args) { + try { + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); + logger.info("Starting Pulsar Admin MCP Server with options: {}", options); + TransportLauncher.start(options); + } catch (Exception e) { + logger.error("Fatal error starting Pulsar Admin MCP Server: {}", e.getMessage(), e); + System.err.println("Fatal error: " + e.getMessage()); + System.exit(-1); } + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/client/PulsarClientManager.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/client/PulsarClientManager.java index 84e464a..f7d3e03 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/client/PulsarClientManager.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/client/PulsarClientManager.java @@ -23,113 +23,115 @@ @Component public class PulsarClientManager implements AutoCloseable { - private PulsarAdmin pulsarAdmin; - private PulsarClient pulsarClient; + private PulsarAdmin pulsarAdmin; + private PulsarClient pulsarClient; - private final AtomicBoolean adminInitialized = new AtomicBoolean(); - private final AtomicBoolean clientInitialized = new AtomicBoolean(); + private final AtomicBoolean adminInitialized = new AtomicBoolean(); + private final AtomicBoolean clientInitialized = new AtomicBoolean(); - public void initialize() { - getAdmin(); - getClient(); + public void initialize() { + getAdmin(); + getClient(); + } + + public synchronized PulsarAdmin getAdmin() { + if (!adminInitialized.get()) { + initializePulsarAdmin(); } + return pulsarAdmin; + } - public synchronized PulsarAdmin getAdmin() { - if (!adminInitialized.get()) { - initializePulsarAdmin(); - } - return pulsarAdmin; + public synchronized PulsarClient getClient() { + if (!clientInitialized.get()) { + initializePulsarClient(); } + return pulsarClient; + } - public synchronized PulsarClient getClient() { - if (!clientInitialized.get()) { - initializePulsarClient(); - } - return pulsarClient; + private void initializePulsarAdmin() { + + if (!adminInitialized.compareAndSet(false, true)) { + return; } - private void initializePulsarAdmin() { + boolean success = false; + try { + String adminUrl = System.getenv().getOrDefault("PULSAR_ADMIN_URL", "http://localhost:8080"); - if (!adminInitialized.compareAndSet(false, true)) { - return; - } + PulsarAdminBuilder adminBuilder = + PulsarAdmin.builder() + .serviceHttpUrl(adminUrl) + .connectionTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS); - boolean success = false; - try { - String adminUrl = System.getenv().getOrDefault("PULSAR_ADMIN_URL", "http://localhost:8080"); - - PulsarAdminBuilder adminBuilder = PulsarAdmin.builder() - .serviceHttpUrl(adminUrl) - .connectionTimeout(30, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS); - - pulsarAdmin = adminBuilder.build(); - - pulsarAdmin.clusters().getClusters(); - success = true; - - } catch (Exception e) { - if (pulsarAdmin != null) { - try { - pulsarAdmin.close(); - } catch (Exception ignore) { - - } - pulsarAdmin = null; - } - adminInitialized.set(false); - throw new RuntimeException("Failed to initialize PulsarAdmin", e); - } finally { - if (!success) { - adminInitialized.set(false); - } - } - } + pulsarAdmin = adminBuilder.build(); - private void initializePulsarClient() { - if (!clientInitialized.compareAndSet(false, true)) { - return; - } - boolean success = false; + pulsarAdmin.clusters().getClusters(); + success = true; + + } catch (Exception e) { + if (pulsarAdmin != null) { try { - String serviceUrl = System.getenv().getOrDefault("PULSAR_SERVICE_URL", "pulsar://localhost:6650"); - - var clientBuilder = PulsarClient.builder() - .serviceUrl(serviceUrl) - .operationTimeout(30, TimeUnit.SECONDS) - .connectionTimeout(30, TimeUnit.SECONDS) - .keepAliveInterval(30, TimeUnit.SECONDS); - - this.pulsarClient = clientBuilder.build(); - success = true; - - } catch (Exception e) { - if (pulsarClient != null) { - try { - pulsarClient.close(); - } catch (Exception ignore) { - } - pulsarClient = null; - } - clientInitialized.set(false); - throw new RuntimeException("Failed to initialize PulsarClient", e); - } finally { - if (!success) { - clientInitialized.set(false); - } + pulsarAdmin.close(); + } catch (Exception ignore) { + } + pulsarAdmin = null; + } + adminInitialized.set(false); + throw new RuntimeException("Failed to initialize PulsarAdmin", e); + } finally { + if (!success) { + adminInitialized.set(false); + } } + } - @Override - public void close() throws Exception { - if (pulsarClient != null) { - pulsarClient.close(); - } - if (pulsarAdmin != null) { - pulsarAdmin.close(); + private void initializePulsarClient() { + if (!clientInitialized.compareAndSet(false, true)) { + return; + } + boolean success = false; + try { + String serviceUrl = + System.getenv().getOrDefault("PULSAR_SERVICE_URL", "pulsar://localhost:6650"); + + var clientBuilder = + PulsarClient.builder() + .serviceUrl(serviceUrl) + .operationTimeout(30, TimeUnit.SECONDS) + .connectionTimeout(30, TimeUnit.SECONDS) + .keepAliveInterval(30, TimeUnit.SECONDS); + + this.pulsarClient = clientBuilder.build(); + success = true; + + } catch (Exception e) { + if (pulsarClient != null) { + try { + pulsarClient.close(); + } catch (Exception ignore) { } - adminInitialized.set(false); + pulsarClient = null; + } + clientInitialized.set(false); + throw new RuntimeException("Failed to initialize PulsarClient", e); + } finally { + if (!success) { clientInitialized.set(false); + } } + } + @Override + public void close() throws Exception { + if (pulsarClient != null) { + pulsarClient.close(); + } + if (pulsarAdmin != null) { + pulsarAdmin.close(); + } + adminInitialized.set(false); + clientInitialized.set(false); + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/config/PulsarMCPCliOptions.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/config/PulsarMCPCliOptions.java index 163f4e6..e742349 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/config/PulsarMCPCliOptions.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/config/PulsarMCPCliOptions.java @@ -18,67 +18,64 @@ @Getter public class PulsarMCPCliOptions { - @Getter - public enum TransportType { + @Getter + public enum TransportType { + STDIO("stdio", "Standard input/output (Claude Desktop)"), + HTTP("http", "HTTP Streaming Events (Web application)"); + private final String value; + private final String description; - STDIO("stdio", "Standard input/output (Claude Desktop)"), - HTTP("http", "HTTP Streaming Events (Web application)"); - private final String value; - private final String description; - - TransportType(String value, String description) { - this.value = value; - this.description = description; - } + TransportType(String value, String description) { + this.value = value; + this.description = description; + } - public static TransportType fromString(String value) { - for (TransportType t : values()) { - if (t.value.equalsIgnoreCase(value)) { - return t; - } - } - throw new IllegalArgumentException( - value + " is not a valid TransportType. Valid Options: stdio,http"); + public static TransportType fromString(String value) { + for (TransportType t : values()) { + if (t.value.equalsIgnoreCase(value)) { + return t; } + } + throw new IllegalArgumentException( + value + " is not a valid TransportType. Valid Options: stdio,http"); } + } - private TransportType transport = TransportType.STDIO; - private int httpPort = 8889; + private TransportType transport = TransportType.STDIO; + private int httpPort = 8889; - public static PulsarMCPCliOptions parseArgs(String[] args) { - PulsarMCPCliOptions opts = new PulsarMCPCliOptions(); + public static PulsarMCPCliOptions parseArgs(String[] args) { + PulsarMCPCliOptions opts = new PulsarMCPCliOptions(); - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - switch (arg) { - case "-t", "--transport" -> { - if (i + 1 >= args.length) { - throw new IllegalArgumentException("Missing value for --transport"); - } - opts.transport = TransportType.fromString(args[++i]); - } - case "--port" -> { - if (i + 1 >= args.length) { - throw new IllegalArgumentException("Missing value for --port"); - } - try { - opts.httpPort = Integer.parseInt(args[++i]); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid port number for --port"); - } - } - default -> { - throw new IllegalArgumentException("Unknown argument: " + arg); - } - } + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + switch (arg) { + case "-t", "--transport" -> { + if (i + 1 >= args.length) { + throw new IllegalArgumentException("Missing value for --transport"); + } + opts.transport = TransportType.fromString(args[++i]); + } + case "--port" -> { + if (i + 1 >= args.length) { + throw new IllegalArgumentException("Missing value for --port"); + } + try { + opts.httpPort = Integer.parseInt(args[++i]); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid port number for --port"); + } } - return opts; + default -> { + throw new IllegalArgumentException("Unknown argument: " + arg); + } + } } + return opts; + } - @Override - public String toString() { - return "PulsarMCPCliOptions{transport=" + transport - + ",httpPort=" + httpPort + '}'; - } + @Override + public String toString() { + return "PulsarMCPCliOptions{transport=" + transport + ",httpPort=" + httpPort + '}'; + } } - diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/BasePulsarTools.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/BasePulsarTools.java index d09911b..19a575b 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/BasePulsarTools.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/BasePulsarTools.java @@ -23,181 +23,168 @@ public abstract class BasePulsarTools { - protected static final Logger LOGGER = LoggerFactory.getLogger(BasePulsarTools.class); - protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - protected final PulsarAdmin pulsarAdmin; - - public BasePulsarTools(PulsarAdmin pulsarAdmin) { - if (pulsarAdmin == null) { - throw new IllegalArgumentException("pulsarAdmin cannot be null"); - } - this.pulsarAdmin = pulsarAdmin; - } - - protected McpSchema.CallToolResult createSuccessResult(String message, Object data){ - StringBuilder result = new StringBuilder(); - result.append(message).append("\n"); - - if (data != null){ - try { - String jsonData = OBJECT_MAPPER.writerWithDefaultPrettyPrinter() - .writeValueAsString(data); - result.append(jsonData) - .append("\n"); - } catch (Exception e) { - result.append("Result").append(data.toString()).append("\n"); - } - } - - return new McpSchema.CallToolResult( - List.of(new McpSchema.TextContent(result.toString())), - false - ); - } - - protected McpSchema.CallToolResult createErrorResult(String message){ - String errorText = "Error: " + message; - return new McpSchema.CallToolResult( - List.of(new McpSchema.TextContent(errorText)), - true - ); - } - - protected McpSchema.CallToolResult createErrorResult(String message, List suggestions){ - StringBuilder result = new StringBuilder(); - result.append(message).append("\n"); - - if (suggestions != null && !suggestions.isEmpty()) { - suggestions.forEach(s -> result.append(s).append("\n")); - } - return new McpSchema.CallToolResult( - List.of(new McpSchema.TextContent(result.toString())), - true - ); - } - - protected String getStringParam(Map map, String key){ - Object value = map.get(key); - return value == null ? "" : value.toString(); - } - - protected String getRequiredStringParam(Map map, String key) throws IllegalArgumentException{ - String value = getStringParam(map, key); - if (value == null || value.trim().isEmpty()) { - throw new IllegalArgumentException("Required parameter '" + key + "' is missing"); - } - return value.trim(); - } - - protected Integer getIntParam(Map map, String key, Integer defaultValue) { - Object value = map.get(key); - if (value == null) { - return defaultValue; - } - - try { - if (value instanceof Number) { - return ((Number) value).intValue(); - } else { - return Integer.parseInt(value.toString()); - } - } catch (NumberFormatException e) { - return defaultValue; - } - } - - protected Boolean getBooleanParam(Map map, String key, Boolean defaultValue) { - Object value = map.get(key); - if (value == null) { - return defaultValue; - } - if (value instanceof Boolean) { - return (Boolean) value; - } else { - return Boolean.parseBoolean(value.toString()); - } - } - - protected Long getLongParam(Map arguments, String timestamp, Long defaultValue) { - Object value = arguments.get(timestamp); - if (value == null) { - return defaultValue; - } - - try { - if (value instanceof Number) { - return ((Number) value).longValue(); - } else { - return Long.parseLong(value.toString()); - } - } catch (NumberFormatException e) { - return defaultValue; - } - } - - protected static McpSchema.Tool createTool ( - String name, - String description, - String inputSchema) { - return McpSchema.Tool.builder() - .name(name) - .description(description) - .inputSchema(inputSchema) - .build(); - } - - - protected String buildFullTopicName(Map arguments) { - String topicName = getStringParam(arguments, "topic"); - if (topicName != null && !topicName.isBlank()) { - if (topicName.startsWith("persistent://") || topicName.startsWith("non-persistent://")) { - return topicName.trim(); - } - } - - String tenant = (String) arguments.getOrDefault("tenant", "public"); - String namespace = (String) arguments.getOrDefault("namespace", "default"); - Boolean persistent = (Boolean) arguments.getOrDefault("persistent", true); - - String prefix = persistent ? "persistent://" : "non-persistent://"; - return prefix + tenant + "/" + namespace + "/" + topicName; + protected static final Logger LOGGER = LoggerFactory.getLogger(BasePulsarTools.class); + protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + protected final PulsarAdmin pulsarAdmin; + + public BasePulsarTools(PulsarAdmin pulsarAdmin) { + if (pulsarAdmin == null) { + throw new IllegalArgumentException("pulsarAdmin cannot be null"); + } + this.pulsarAdmin = pulsarAdmin; + } + + protected McpSchema.CallToolResult createSuccessResult(String message, Object data) { + StringBuilder result = new StringBuilder(); + result.append(message).append("\n"); + + if (data != null) { + try { + String jsonData = OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(data); + result.append(jsonData).append("\n"); + } catch (Exception e) { + result.append("Result").append(data.toString()).append("\n"); + } + } + + return new McpSchema.CallToolResult( + List.of(new McpSchema.TextContent(result.toString())), false); + } + + protected McpSchema.CallToolResult createErrorResult(String message) { + String errorText = "Error: " + message; + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(errorText)), true); + } + + protected McpSchema.CallToolResult createErrorResult(String message, List suggestions) { + StringBuilder result = new StringBuilder(); + result.append(message).append("\n"); + + if (suggestions != null && !suggestions.isEmpty()) { + suggestions.forEach(s -> result.append(s).append("\n")); + } + return new McpSchema.CallToolResult( + List.of(new McpSchema.TextContent(result.toString())), true); + } + + protected String getStringParam(Map map, String key) { + Object value = map.get(key); + return value == null ? "" : value.toString(); + } + + protected String getRequiredStringParam(Map map, String key) + throws IllegalArgumentException { + String value = getStringParam(map, key); + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException("Required parameter '" + key + "' is missing"); + } + return value.trim(); + } + + protected Integer getIntParam(Map map, String key, Integer defaultValue) { + Object value = map.get(key); + if (value == null) { + return defaultValue; + } + + try { + if (value instanceof Number) { + return ((Number) value).intValue(); + } else { + return Integer.parseInt(value.toString()); + } + } catch (NumberFormatException e) { + return defaultValue; + } + } + + protected Boolean getBooleanParam(Map map, String key, Boolean defaultValue) { + Object value = map.get(key); + if (value == null) { + return defaultValue; + } + if (value instanceof Boolean) { + return (Boolean) value; + } else { + return Boolean.parseBoolean(value.toString()); + } + } + + protected Long getLongParam(Map arguments, String timestamp, Long defaultValue) { + Object value = arguments.get(timestamp); + if (value == null) { + return defaultValue; + } + + try { + if (value instanceof Number) { + return ((Number) value).longValue(); + } else { + return Long.parseLong(value.toString()); + } + } catch (NumberFormatException e) { + return defaultValue; + } + } + + protected static McpSchema.Tool createTool(String name, String description, String inputSchema) { + return McpSchema.Tool.builder() + .name(name) + .description(description) + .inputSchema(inputSchema) + .build(); + } + + protected String buildFullTopicName(Map arguments) { + String topicName = getStringParam(arguments, "topic"); + if (topicName != null && !topicName.isBlank()) { + if (topicName.startsWith("persistent://") || topicName.startsWith("non-persistent://")) { + return topicName.trim(); + } + } + + String tenant = (String) arguments.getOrDefault("tenant", "public"); + String namespace = (String) arguments.getOrDefault("namespace", "default"); + Boolean persistent = (Boolean) arguments.getOrDefault("persistent", true); + + String prefix = persistent ? "persistent://" : "non-persistent://"; + return prefix + tenant + "/" + namespace + "/" + topicName; + } + + protected String resolveNamespace(Map arguments) { + String tenant = getStringParam(arguments, "tenant"); + String namespace = getStringParam(arguments, "namespace"); + + if (namespace != null && namespace.contains("/")) { + return namespace; + } + + if (tenant == null) { + tenant = "public"; + } + + if (namespace == null) { + namespace = "default"; + } + + return tenant + "/" + namespace; + } + + protected void addTopicBreakdown(Map result, String fullTopicName) { + if (fullTopicName.startsWith("persistent://")) { + fullTopicName = fullTopicName.substring("persistent://".length()); + } else if (fullTopicName.startsWith("non-persistent://")) { + fullTopicName = fullTopicName.substring("non-persistent://".length()); } - protected String resolveNamespace(Map arguments) { - String tenant = getStringParam(arguments, "tenant"); - String namespace = getStringParam(arguments, "namespace"); - - if (namespace != null && namespace.contains("/")) { - return namespace; - } - - if (tenant == null) { - tenant = "public"; - } - - if (namespace == null) { - namespace = "default"; - } - - return tenant + "/" + namespace; - } - - protected void addTopicBreakdown(Map result, String fullTopicName) { - if (fullTopicName.startsWith("persistent://")) { - fullTopicName = fullTopicName.substring("persistent://".length()); - } else if (fullTopicName.startsWith("non-persistent://")) { - fullTopicName = fullTopicName.substring("non-persistent://".length()); - } - - String[] parts = fullTopicName.split("/", 3); - if (parts.length != 3) { - return; - } - - result.put("tenant", parts[0]); - result.put("namespace", parts[1]); - result.put("topicName", parts[2]); + String[] parts = fullTopicName.split("/", 3); + if (parts.length != 3) { + return; } + result.put("tenant", parts[0]); + result.put("namespace", parts[1]); + result.put("topicName", parts[2]); + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/ClusterTools.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/ClusterTools.java index 62c6a15..1a20e52 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/ClusterTools.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/ClusterTools.java @@ -31,82 +31,86 @@ public class ClusterTools extends BasePulsarTools { - public ClusterTools(PulsarAdmin pulsarAdmin) { - super(pulsarAdmin); - } - - public void registerTools(McpSyncServer mcpServer){ - registerListClusters(mcpServer); - registerGetClusterInfo(mcpServer); - registerCreateCluster(mcpServer); - registerDeleteCluster(mcpServer); - registerUpdateClusterConfig(mcpServer); - registerGetClusterStats(mcpServer); - registerListBrokers(mcpServer); - registerGetBrokerStats(mcpServer); - registerGetClusterFailureDomain(mcpServer); - registerSetClusterFailureDomain(mcpServer); - } - - private void registerListClusters(McpSyncServer mcpServer){ - McpSchema.Tool tool = createTool( - "list-clusters", - "List all Pulsar clusters and their status", - """ + public ClusterTools(PulsarAdmin pulsarAdmin) { + super(pulsarAdmin); + } + + public void registerTools(McpSyncServer mcpServer) { + registerListClusters(mcpServer); + registerGetClusterInfo(mcpServer); + registerCreateCluster(mcpServer); + registerDeleteCluster(mcpServer); + registerUpdateClusterConfig(mcpServer); + registerGetClusterStats(mcpServer); + registerListBrokers(mcpServer); + registerGetBrokerStats(mcpServer); + registerGetClusterFailureDomain(mcpServer); + registerSetClusterFailureDomain(mcpServer); + } + + private void registerListClusters(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "list-clusters", + "List all Pulsar clusters and their status", + """ { "type": "object", "properties": {}, "required": [] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - var clusters = pulsarAdmin.clusters().getClusters(); - - Map clusterDetails = new HashMap<>(); - for (String clusterName :clusters) { - try { - ClusterData clusterData = pulsarAdmin.clusters().getCluster(clusterName); - Map details = new HashMap<>(); - details.put("serviceUrl", clusterData.getServiceUrl()); - details.put("serviceUrlTls", clusterData.getServiceUrlTls()); - details.put("brokerServiceUrl", clusterData.getBrokerServiceUrl()); - details.put("brokerServiceUrlTls", clusterData.getBrokerServiceUrlTls()); - details.put("status", "active"); - clusterDetails.put(clusterName, details); - } catch (Exception e) { - Map details = new HashMap<>(); - details.put("status", "error"); - details.put("error", e.getMessage()); - clusterDetails.put(clusterName, details); - } - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + var clusters = pulsarAdmin.clusters().getClusters(); + + Map clusterDetails = new HashMap<>(); + for (String clusterName : clusters) { + try { + ClusterData clusterData = pulsarAdmin.clusters().getCluster(clusterName); + Map details = new HashMap<>(); + details.put("serviceUrl", clusterData.getServiceUrl()); + details.put("serviceUrlTls", clusterData.getServiceUrlTls()); + details.put("brokerServiceUrl", clusterData.getBrokerServiceUrl()); + details.put("brokerServiceUrlTls", clusterData.getBrokerServiceUrlTls()); + details.put("status", "active"); + clusterDetails.put(clusterName, details); + } catch (Exception e) { + Map details = new HashMap<>(); + details.put("status", "error"); + details.put("error", e.getMessage()); + clusterDetails.put(clusterName, details); + } + } - Map result = new HashMap<>(); - result.put("clusters", clusters); - result.put("count", clusters.size()); - result.put("clusterDetails", clusterDetails); + Map result = new HashMap<>(); + result.put("clusters", clusters); + result.put("count", clusters.size()); + result.put("clusterDetails", clusterDetails); - return createSuccessResult("Cluster details", result); + return createSuccessResult("Cluster details", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to list clusters", e); - return createErrorResult("Failed to list clusters" + e.getMessage()); - } - }).build()); - } - - private void registerGetClusterInfo(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-cluster-info", - "Get details information about a specific cluster", - """ + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to list clusters", e); + return createErrorResult("Failed to list clusters" + e.getMessage()); + } + }) + .build()); + } + + private void registerGetClusterInfo(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-cluster-info", + "Get details information about a specific cluster", + """ { "type": "object", "properties": { @@ -117,46 +121,49 @@ private void registerGetClusterInfo(McpSyncServer mcpServer) { }, "required": ["clusterName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String clusterName = getRequiredStringParam(request.arguments(), "clusterName"); - - ClusterData clusterData = pulsarAdmin.clusters().getCluster(clusterName); - - Map result = new HashMap<>(); - result.put("clusterName", clusterName); - result.put("serviceUrl", clusterData.getServiceUrl()); - result.put("serviceUrlTls", clusterData.getServiceUrlTls()); - result.put("brokerServiceUrl", clusterData.getBrokerServiceUrl()); - result.put("brokerServiceUrlTls", clusterData.getBrokerServiceUrlTls()); - result.put("peerClusterNames", clusterData.getPeerClusterNames()); - result.put("proxyProtocol", clusterData.getProxyProtocol()); - result.put("authenticationPlugin", clusterData.getAuthenticationPlugin()); - result.put("authenticationParameters", clusterData.getAuthenticationParameters()); - result.put("proxyServiceUrl", clusterData.getProxyServiceUrl()); - - return createSuccessResult("Cluster info retrieved", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get cluster info", e); - return createErrorResult("Failed to get cluster info: " + e.getMessage()); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String clusterName = getRequiredStringParam(request.arguments(), "clusterName"); + + ClusterData clusterData = pulsarAdmin.clusters().getCluster(clusterName); + + Map result = new HashMap<>(); + result.put("clusterName", clusterName); + result.put("serviceUrl", clusterData.getServiceUrl()); + result.put("serviceUrlTls", clusterData.getServiceUrlTls()); + result.put("brokerServiceUrl", clusterData.getBrokerServiceUrl()); + result.put("brokerServiceUrlTls", clusterData.getBrokerServiceUrlTls()); + result.put("peerClusterNames", clusterData.getPeerClusterNames()); + result.put("proxyProtocol", clusterData.getProxyProtocol()); + result.put("authenticationPlugin", clusterData.getAuthenticationPlugin()); + result.put( + "authenticationParameters", clusterData.getAuthenticationParameters()); + result.put("proxyServiceUrl", clusterData.getProxyServiceUrl()); + + return createSuccessResult("Cluster info retrieved", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get cluster info", e); + return createErrorResult("Failed to get cluster info: " + e.getMessage()); + } }) - .build()); - } - - private void registerCreateCluster(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "create-cluster", - "Create a new Pulsar cluster", - """ + .build()); + } + + private void registerCreateCluster(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "create-cluster", + "Create a new Pulsar cluster", + """ { "type": "object", "properties": { @@ -195,81 +202,85 @@ private void registerCreateCluster(McpSyncServer mcpServer) { }, "required": ["clusterName", "serviceUrl"] } - """ - ); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String clusterName = getRequiredStringParam(request.arguments(), "clusterName"); + String serviceUrl = getRequiredStringParam(request.arguments(), "serviceUrl"); + String serviceUrlTls = getStringParam(request.arguments(), "serviceUrlTls"); + String brokerServiceUrl = + getStringParam(request.arguments(), "brokerServiceUrl"); + String brokerServiceUrlTls = + getStringParam(request.arguments(), "brokerServiceUrlTls"); + String proxyServiceUrl = getStringParam(request.arguments(), "proxyServiceUrl"); + String authenticationPlugin = + getStringParam(request.arguments(), "authenticationPlugin"); + String authenticationParameters = + getStringParam(request.arguments(), "authenticationParameters"); - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { try { - String clusterName = getRequiredStringParam(request.arguments(), "clusterName"); - String serviceUrl = getRequiredStringParam(request.arguments(), "serviceUrl"); - String serviceUrlTls = getStringParam(request.arguments(), "serviceUrlTls"); - String brokerServiceUrl = getStringParam(request.arguments(), "brokerServiceUrl"); - String brokerServiceUrlTls = getStringParam(request.arguments(), "brokerServiceUrlTls"); - String proxyServiceUrl = getStringParam(request.arguments(), "proxyServiceUrl"); - String authenticationPlugin = getStringParam(request.arguments(), "authenticationPlugin"); - String authenticationParameters = getStringParam( - request.arguments(), - "authenticationParameters" - ); - - try { - pulsarAdmin.clusters().getCluster(clusterName); - return createErrorResult("Cluster already exists: " + clusterName, - List.of("Choose a different cluster name")); - } catch (PulsarAdminException.NotFoundException ignore) { - - } catch (PulsarAdminException e) { - return createErrorResult("Failed to verify cluster existence: " + e.getMessage()); - } + pulsarAdmin.clusters().getCluster(clusterName); + return createErrorResult( + "Cluster already exists: " + clusterName, + List.of("Choose a different cluster name")); + } catch (PulsarAdminException.NotFoundException ignore) { - var clusterDataBuilder = ClusterData.builder() - .serviceUrl(serviceUrl); + } catch (PulsarAdminException e) { + return createErrorResult( + "Failed to verify cluster existence: " + e.getMessage()); + } - if (serviceUrlTls != null) { - clusterDataBuilder.serviceUrlTls(serviceUrlTls); - } - if (brokerServiceUrl != null) { - clusterDataBuilder.brokerServiceUrl(brokerServiceUrl); - } - if (brokerServiceUrlTls != null) { - clusterDataBuilder.brokerServiceUrlTls(brokerServiceUrlTls); - } - if (proxyServiceUrl != null) { - clusterDataBuilder.proxyServiceUrl(proxyServiceUrl); - } - if (authenticationPlugin != null) { - clusterDataBuilder.authenticationPlugin(authenticationPlugin); - } - if (authenticationParameters != null) { - clusterDataBuilder.authenticationParameters(authenticationParameters); - } + var clusterDataBuilder = ClusterData.builder().serviceUrl(serviceUrl); + + if (serviceUrlTls != null) { + clusterDataBuilder.serviceUrlTls(serviceUrlTls); + } + if (brokerServiceUrl != null) { + clusterDataBuilder.brokerServiceUrl(brokerServiceUrl); + } + if (brokerServiceUrlTls != null) { + clusterDataBuilder.brokerServiceUrlTls(brokerServiceUrlTls); + } + if (proxyServiceUrl != null) { + clusterDataBuilder.proxyServiceUrl(proxyServiceUrl); + } + if (authenticationPlugin != null) { + clusterDataBuilder.authenticationPlugin(authenticationPlugin); + } + if (authenticationParameters != null) { + clusterDataBuilder.authenticationParameters(authenticationParameters); + } - pulsarAdmin.clusters().createCluster(clusterName, clusterDataBuilder.build()); + pulsarAdmin.clusters().createCluster(clusterName, clusterDataBuilder.build()); - Map result = new HashMap<>(); - result.put("clusterName", clusterName); - result.put("serviceUrl", serviceUrl); - result.put("created", true); + Map result = new HashMap<>(); + result.put("clusterName", clusterName); + result.put("serviceUrl", serviceUrl); + result.put("created", true); - return createSuccessResult("Cluster created successfully", result); + return createSuccessResult("Cluster created successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to create cluster", e); - return createErrorResult("Failed to create cluster: " + e.getMessage()); - } + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to create cluster", e); + return createErrorResult("Failed to create cluster: " + e.getMessage()); + } }) - .build()); - } - - private void registerUpdateClusterConfig(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "update-cluster-config", - "Update configuration of an existing Pulsar cluster", - """ + .build()); + } + + private void registerUpdateClusterConfig(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "update-cluster-config", + "Update configuration of an existing Pulsar cluster", + """ { "type": "object", "properties": { @@ -308,88 +319,95 @@ private void registerUpdateClusterConfig(McpSyncServer mcpServer) { }, "required": ["clusterName", "serviceUrl"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String clusterName = getRequiredStringParam(request.arguments(), "clusterName"); + String serviceUrl = getStringParam(request.arguments(), "serviceUrl"); + String serviceUrlTls = getStringParam(request.arguments(), "serviceUrlTls"); + String brokerServiceUrl = + getStringParam(request.arguments(), "brokerServiceUrl"); + String brokerServiceUrlTls = + getStringParam(request.arguments(), "brokerServiceUrlTls"); + String proxyServiceUrl = getStringParam(request.arguments(), "proxyServiceUrl"); + String authenticationPlugin = + getStringParam(request.arguments(), "authenticationPlugin"); + String authenticationParameters = + getStringParam(request.arguments(), "authenticationParameters"); + + ClusterData current; try { - String clusterName = getRequiredStringParam(request.arguments(), "clusterName"); - String serviceUrl = getStringParam(request.arguments(), "serviceUrl"); - String serviceUrlTls = getStringParam(request.arguments(), "serviceUrlTls"); - String brokerServiceUrl = getStringParam(request.arguments(), "brokerServiceUrl"); - String brokerServiceUrlTls = getStringParam(request.arguments(), "brokerServiceUrlTls"); - String proxyServiceUrl = getStringParam(request.arguments(), "proxyServiceUrl"); - String authenticationPlugin = getStringParam(request.arguments(), "authenticationPlugin"); - String authenticationParameters = getStringParam( - request.arguments(), - "authenticationParameters"); - - ClusterData current; - try { - current = pulsarAdmin.clusters().getCluster(clusterName); - } catch (PulsarAdminException.NotFoundException e) { - return createErrorResult("Cluster not found: " + clusterName); - } - - String finalServiceUrl = (serviceUrl != null && !serviceUrl.isBlank()) - ? serviceUrl.trim() - : current.getServiceUrl(); - - var b = ClusterData.builder() - .serviceUrl(finalServiceUrl) - .serviceUrlTls((serviceUrlTls != null - && !serviceUrlTls.isBlank()) - ? serviceUrlTls - : current.getServiceUrlTls()) - .brokerServiceUrl((brokerServiceUrl != null - && !brokerServiceUrl.isBlank()) - ? brokerServiceUrl - : current.getBrokerServiceUrl()) - .brokerServiceUrlTls((brokerServiceUrlTls != null - && !brokerServiceUrlTls.isBlank()) - ? brokerServiceUrlTls - : current.getBrokerServiceUrlTls()) - .proxyServiceUrl((proxyServiceUrl != null - && !proxyServiceUrl.isBlank()) - ? proxyServiceUrl - : current.getProxyServiceUrl()); - - if (authenticationPlugin != null && !authenticationPlugin.isBlank()) { - b.authenticationPlugin(authenticationPlugin); - } else if (current.getAuthenticationPlugin() != null) { - b.authenticationPlugin(current.getAuthenticationPlugin()); - } - if (authenticationParameters != null && !authenticationParameters.isBlank()) { - b.authenticationParameters(authenticationParameters); - } else if (current.getAuthenticationParameters() != null) { - b.authenticationParameters(current.getAuthenticationParameters()); - } - - pulsarAdmin.clusters().updateCluster(clusterName, b.build()); - Map result = new HashMap<>(); - result.put("clusterName", clusterName); - result.put("serviceUrl", serviceUrl); - result.put("updated", true); - - return createSuccessResult("Cluster configuration updated successfully", result); + current = pulsarAdmin.clusters().getCluster(clusterName); + } catch (PulsarAdminException.NotFoundException e) { + return createErrorResult("Cluster not found: " + clusterName); + } - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid input: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to update cluster config", e); - return createErrorResult("Failed to update cluster config: " + e.getMessage()); + String finalServiceUrl = + (serviceUrl != null && !serviceUrl.isBlank()) + ? serviceUrl.trim() + : current.getServiceUrl(); + + var b = + ClusterData.builder() + .serviceUrl(finalServiceUrl) + .serviceUrlTls( + (serviceUrlTls != null && !serviceUrlTls.isBlank()) + ? serviceUrlTls + : current.getServiceUrlTls()) + .brokerServiceUrl( + (brokerServiceUrl != null && !brokerServiceUrl.isBlank()) + ? brokerServiceUrl + : current.getBrokerServiceUrl()) + .brokerServiceUrlTls( + (brokerServiceUrlTls != null && !brokerServiceUrlTls.isBlank()) + ? brokerServiceUrlTls + : current.getBrokerServiceUrlTls()) + .proxyServiceUrl( + (proxyServiceUrl != null && !proxyServiceUrl.isBlank()) + ? proxyServiceUrl + : current.getProxyServiceUrl()); + + if (authenticationPlugin != null && !authenticationPlugin.isBlank()) { + b.authenticationPlugin(authenticationPlugin); + } else if (current.getAuthenticationPlugin() != null) { + b.authenticationPlugin(current.getAuthenticationPlugin()); + } + if (authenticationParameters != null && !authenticationParameters.isBlank()) { + b.authenticationParameters(authenticationParameters); + } else if (current.getAuthenticationParameters() != null) { + b.authenticationParameters(current.getAuthenticationParameters()); } + + pulsarAdmin.clusters().updateCluster(clusterName, b.build()); + Map result = new HashMap<>(); + result.put("clusterName", clusterName); + result.put("serviceUrl", serviceUrl); + result.put("updated", true); + + return createSuccessResult( + "Cluster configuration updated successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid input: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to update cluster config", e); + return createErrorResult("Failed to update cluster config: " + e.getMessage()); + } }) - .build()); - } - - private void registerDeleteCluster(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "delete-cluster", - "Delete a Pulsar cluster by name", - """ + .build()); + } + + private void registerDeleteCluster(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "delete-cluster", + "Delete a Pulsar cluster by name", + """ { "type": "object", "properties": { @@ -405,76 +423,80 @@ private void registerDeleteCluster(McpSyncServer mcpServer) { }, "required": ["clusterName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String clusterName = getRequiredStringParam(request.arguments(), "clusterName").trim(); - boolean force = getBooleanParam(request.arguments(), "force", false); - - if (!force) { - List tenants = pulsarAdmin.tenants().getTenants(); - List referencingTenants = new ArrayList<>(); - for (String tenant : tenants) { - var info = pulsarAdmin.tenants().getTenantInfo(tenant); - var allowed = info != null ? info.getAllowedClusters() : null; - if (allowed != null && allowed.contains(clusterName)) { - referencingTenants.add(tenant); - } - } - - List referencingNamespaces = new ArrayList<>(); - for (String tenant : tenants) { - var nss = pulsarAdmin.namespaces().getNamespaces(tenant); - for (String ns : nss) { - var repl = pulsarAdmin.namespaces().getNamespaceReplicationClusters(ns); - if (repl != null && repl.contains(clusterName)) { - referencingNamespaces.add(ns); - } - } - } - - if (!referencingTenants.isEmpty() || !referencingNamespaces.isEmpty()) { - StringBuilder sb = new StringBuilder(); - sb.append("Cluster '").append(clusterName) - .append("' is still referenced. Use 'force: true' to delete anyway."); - if (!referencingTenants.isEmpty()) { - sb.append(" Referenced by tenants: ").append(referencingTenants); - } - if (!referencingNamespaces.isEmpty()) { - sb.append(" Referenced by namespaces: ").append(referencingNamespaces); - } - return createErrorResult(sb.toString()); - } - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String clusterName = + getRequiredStringParam(request.arguments(), "clusterName").trim(); + boolean force = getBooleanParam(request.arguments(), "force", false); + + if (!force) { + List tenants = pulsarAdmin.tenants().getTenants(); + List referencingTenants = new ArrayList<>(); + for (String tenant : tenants) { + var info = pulsarAdmin.tenants().getTenantInfo(tenant); + var allowed = info != null ? info.getAllowedClusters() : null; + if (allowed != null && allowed.contains(clusterName)) { + referencingTenants.add(tenant); + } + } + + List referencingNamespaces = new ArrayList<>(); + for (String tenant : tenants) { + var nss = pulsarAdmin.namespaces().getNamespaces(tenant); + for (String ns : nss) { + var repl = pulsarAdmin.namespaces().getNamespaceReplicationClusters(ns); + if (repl != null && repl.contains(clusterName)) { + referencingNamespaces.add(ns); + } + } + } + + if (!referencingTenants.isEmpty() || !referencingNamespaces.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("Cluster '") + .append(clusterName) + .append("' is still referenced. Use 'force: true' to delete anyway."); + if (!referencingTenants.isEmpty()) { + sb.append(" Referenced by tenants: ").append(referencingTenants); + } + if (!referencingNamespaces.isEmpty()) { + sb.append(" Referenced by namespaces: ").append(referencingNamespaces); + } + return createErrorResult(sb.toString()); + } + } - pulsarAdmin.clusters().deleteCluster(clusterName); + pulsarAdmin.clusters().deleteCluster(clusterName); - Map result = new HashMap<>(); - result.put("clusterName", clusterName); - result.put("deleted", true); - result.put("forced", force); + Map result = new HashMap<>(); + result.put("clusterName", clusterName); + result.put("deleted", true); + result.put("forced", force); - return createSuccessResult("Cluster deleted successfully", result); + return createSuccessResult("Cluster deleted successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to delete cluster", e); - return createErrorResult("Failed to delete cluster: " + e.getMessage()); - } + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to delete cluster", e); + return createErrorResult("Failed to delete cluster: " + e.getMessage()); + } }) - .build()); - } - - private void registerGetClusterStats(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-cluster-stats", - "Get statistics for a given Pulsar cluster", - """ + .build()); + } + + private void registerGetClusterStats(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-cluster-stats", + "Get statistics for a given Pulsar cluster", + """ { "type": "object", "properties": { @@ -485,42 +507,44 @@ private void registerGetClusterStats(McpSyncServer mcpServer) { }, "required": ["clusterName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String clusterName = getRequiredStringParam(request.arguments(), "clusterName"); - if (clusterName == null || clusterName.isBlank()) { - return createErrorResult("Missing required parameter: clusterName"); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String clusterName = getRequiredStringParam(request.arguments(), "clusterName"); + if (clusterName == null || clusterName.isBlank()) { + return createErrorResult("Missing required parameter: clusterName"); + } - var brokers = pulsarAdmin.brokers().getActiveBrokers(clusterName); + var brokers = pulsarAdmin.brokers().getActiveBrokers(clusterName); - Map stats = new HashMap<>(); - stats.put("clusterName", clusterName); - stats.put("activeBrokers", brokers); - stats.put("brokerCount", brokers.size()); + Map stats = new HashMap<>(); + stats.put("clusterName", clusterName); + stats.put("activeBrokers", brokers); + stats.put("brokerCount", brokers.size()); - return createSuccessResult("Cluster stats retrieved successfully", stats); + return createSuccessResult("Cluster stats retrieved successfully", stats); - } catch (IllegalArgumentException e){ - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get cluster stats", e); - return createErrorResult("Failed to get cluster stats: " + e.getMessage()); - } + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get cluster stats", e); + return createErrorResult("Failed to get cluster stats: " + e.getMessage()); + } }) - .build()); - } - - private void registerListBrokers(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "list-brokers", - "List all active brokers in a given Pulsar cluster", - """ + .build()); + } + + private void registerListBrokers(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "list-brokers", + "List all active brokers in a given Pulsar cluster", + """ { "type": "object", "properties": { @@ -531,65 +555,72 @@ private void registerListBrokers(McpSyncServer mcpServer) { }, "required": ["clusterName"] } - """ - ); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String clusterName = + getRequiredStringParam(request.arguments(), "clusterName").trim(); + if (clusterName.isEmpty()) { + return createErrorResult("clusterName cannot be blank"); + } - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { try { - String clusterName = getRequiredStringParam(request.arguments(), "clusterName").trim(); - if (clusterName.isEmpty()) { - return createErrorResult("clusterName cannot be blank"); - } + pulsarAdmin.clusters().getCluster(clusterName); + } catch (PulsarAdminException.NotFoundException e) { + return createErrorResult("Cluster '" + clusterName + "' not found"); + } - try { - pulsarAdmin.clusters().getCluster(clusterName); - } catch (PulsarAdminException.NotFoundException e) { - return createErrorResult("Cluster '" + clusterName + "' not found"); - } + List active = + new ArrayList<>(pulsarAdmin.brokers().getActiveBrokers(clusterName)); + active.sort(String::compareTo); - List active = new ArrayList<>(pulsarAdmin.brokers().getActiveBrokers(clusterName)); - active.sort(String::compareTo); - - String leader = null; - try { - leader = String.valueOf(pulsarAdmin.brokers().getLeaderBroker()); - } catch (Exception ignore) {} - - var dynamicConfigNames = pulsarAdmin.brokers().getDynamicConfigurationNames(); - List dynamicNamesSorted = dynamicConfigNames == null - ? List.of() - : dynamicConfigNames.stream().sorted().toList(); - - Map result = new HashMap<>(); - result.put("clusterName", clusterName); - result.put("activeBrokers", active); - result.put("brokerCount", active.size()); - result.put("leaderBroker", leader); - result.put("dynamicConfigNames", dynamicNamesSorted); - result.put("available", !active.isEmpty()); - result.put("timestamp", System.currentTimeMillis()); - - String msg = "List of active brokers retrieved successfully" - + (leader != null ? "" : " (leader not available)"); - return createSuccessResult(msg, result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to list brokers", e); - return createErrorResult("Failed to list brokers: " + e.getMessage()); + String leader = null; + try { + leader = String.valueOf(pulsarAdmin.brokers().getLeaderBroker()); + } catch (Exception ignore) { } + + var dynamicConfigNames = pulsarAdmin.brokers().getDynamicConfigurationNames(); + List dynamicNamesSorted = + dynamicConfigNames == null + ? List.of() + : dynamicConfigNames.stream().sorted().toList(); + + Map result = new HashMap<>(); + result.put("clusterName", clusterName); + result.put("activeBrokers", active); + result.put("brokerCount", active.size()); + result.put("leaderBroker", leader); + result.put("dynamicConfigNames", dynamicNamesSorted); + result.put("available", !active.isEmpty()); + result.put("timestamp", System.currentTimeMillis()); + + String msg = + "List of active brokers retrieved successfully" + + (leader != null ? "" : " (leader not available)"); + return createSuccessResult(msg, result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to list brokers", e); + return createErrorResult("Failed to list brokers: " + e.getMessage()); + } }) - .build()); - } - - private void registerGetBrokerStats(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-broker-stats", - "Get statistics for a specific Pulsar broker", - """ + .build()); + } + + private void registerGetBrokerStats(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-broker-stats", + "Get statistics for a specific Pulsar broker", + """ { "type": "object", "properties": { @@ -600,43 +631,44 @@ private void registerGetBrokerStats(McpSyncServer mcpServer) { }, "required": ["brokerUrl"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String brokerUrl = getRequiredStringParam(request.arguments(), "brokerUrl"); - - if (brokerUrl == null || brokerUrl.isBlank()) { - return createErrorResult("Missing required parameter: brokerUrl"); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String brokerUrl = getRequiredStringParam(request.arguments(), "brokerUrl"); + + if (brokerUrl == null || brokerUrl.isBlank()) { + return createErrorResult("Missing required parameter: brokerUrl"); + } - var brokerStats = pulsarAdmin.brokerStats().getTopics(); + var brokerStats = pulsarAdmin.brokerStats().getTopics(); - Map result = new HashMap<>(); - result.put("brokerUrl", brokerUrl); - result.put("stats", brokerStats); + Map result = new HashMap<>(); + result.put("brokerUrl", brokerUrl); + result.put("stats", brokerStats); - return createSuccessResult("Broker stats retrieved successfully", result); + return createSuccessResult("Broker stats retrieved successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get broker stats", e); - return createErrorResult("Failed to get broker stats: " + e.getMessage()); - } + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get broker stats", e); + return createErrorResult("Failed to get broker stats: " + e.getMessage()); + } }) - .build() - ); - } - - private void registerGetClusterFailureDomain(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-cluster-failure-domain", - "Get failure domain(s) for a specific Pulsar cluster", - """ + .build()); + } + + private void registerGetClusterFailureDomain(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-cluster-failure-domain", + "Get failure domain(s) for a specific Pulsar cluster", + """ { "type": "object", "properties": { @@ -656,130 +688,138 @@ private void registerGetClusterFailureDomain(McpSyncServer mcpServer) { }, "required": ["clusterName"] } - """ - ); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String clusterName = + getRequiredStringParam(request.arguments(), "clusterName").trim(); + String domainName = getStringParam(request.arguments(), "domainName"); + boolean includeEmpty = + getBooleanParam(request.arguments(), "includeEmpty", true); + + if (clusterName.isEmpty()) { + return createErrorResult("clusterName cannot be blank"); + } + if (domainName != null) { + domainName = domainName.trim(); + } - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { try { - String clusterName = getRequiredStringParam(request.arguments(), "clusterName").trim(); - String domainName = getStringParam(request.arguments(), "domainName"); - boolean includeEmpty = getBooleanParam(request.arguments(), "includeEmpty", true); - - if (clusterName.isEmpty()) { - return createErrorResult("clusterName cannot be blank"); - } - if (domainName != null) { - domainName = domainName.trim(); - } - - try { - pulsarAdmin.clusters().getCluster(clusterName); - } catch (PulsarAdminException.NotFoundException e) { - return createErrorResult("Cluster '" + clusterName + "' not found"); - } - - Map result = new HashMap<>(); - result.put("clusterName", clusterName); - - if (domainName != null && !domainName.isEmpty()) { - try { - FailureDomain fd = - pulsarAdmin.clusters().getFailureDomain(clusterName, domainName); - - Set brokers = (fd != null && fd.getBrokers() != null) - ? new HashSet<>(fd.getBrokers()) - : new HashSet<>(); - - if (!includeEmpty && brokers.isEmpty()) { - result.put("domains", List.of()); - result.put("domainCount", 0); - result.put("available", false); - return createSuccessResult( - "Domain exists but filtered by includeEmpty=false", result); - } - - Map item = new HashMap<>(); - item.put("domainName", domainName); - item.put("brokers", brokers.stream().sorted().toList()); - item.put("brokerCount", brokers.size()); - - result.put("domains", List.of(item)); - result.put("domainCount", 1); - result.put("available", true); - - return createSuccessResult( - "Cluster failure domain retrieved successfully", result); - } catch (PulsarAdminException.NotFoundException e) { - return createErrorResult( - "Domain '" - + domainName - + "' not found in cluster '" - + clusterName + "'"); - } - } else { - Map raw = - pulsarAdmin.clusters().getFailureDomains(clusterName); - if (raw == null) { - raw = Map.of(); - } - - List> domains = new ArrayList<>(); - int brokerTotal = 0; - List emptyDomains = new ArrayList<>(); - - for (Map.Entry e : raw.entrySet()) { - String dn = e.getKey(); - Set brokers = (e.getValue() != null && e.getValue().getBrokers() != null) - ? new HashSet<>(e.getValue().getBrokers()) - : new HashSet<>(); - - if (!includeEmpty && brokers.isEmpty()) { - emptyDomains.add(dn); - continue; - } - brokerTotal += brokers.size(); - - Map item = new HashMap<>(); - item.put("domainName", dn); - item.put("brokers", brokers.stream().sorted().toList()); - item.put("brokerCount", brokers.size()); - domains.add(item); - } - - domains.sort(Comparator.comparing(m -> (String) m.get("domainName"))); - - result.put("domains", domains); - result.put("domainCount", domains.size()); - result.put("brokerTotal", brokerTotal); - if (!emptyDomains.isEmpty()) { - result.put("filteredEmptyDomains", emptyDomains.stream().sorted().toList()); - } - result.put("available", !domains.isEmpty()); - - String msg = "Cluster failure domains retrieved successfully" - + (includeEmpty ? "" : " (empty domains filtered)"); - return createSuccessResult(msg, result); - } + pulsarAdmin.clusters().getCluster(clusterName); + } catch (PulsarAdminException.NotFoundException e) { + return createErrorResult("Cluster '" + clusterName + "' not found"); + } - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (PulsarAdminException e) { - return createErrorResult("Pulsar admin error: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get failure domains", e); - return createErrorResult("Failed to get failure domains: " + e.getMessage()); + Map result = new HashMap<>(); + result.put("clusterName", clusterName); + + if (domainName != null && !domainName.isEmpty()) { + try { + FailureDomain fd = + pulsarAdmin.clusters().getFailureDomain(clusterName, domainName); + + Set brokers = + (fd != null && fd.getBrokers() != null) + ? new HashSet<>(fd.getBrokers()) + : new HashSet<>(); + + if (!includeEmpty && brokers.isEmpty()) { + result.put("domains", List.of()); + result.put("domainCount", 0); + result.put("available", false); + return createSuccessResult( + "Domain exists but filtered by includeEmpty=false", result); + } + + Map item = new HashMap<>(); + item.put("domainName", domainName); + item.put("brokers", brokers.stream().sorted().toList()); + item.put("brokerCount", brokers.size()); + + result.put("domains", List.of(item)); + result.put("domainCount", 1); + result.put("available", true); + + return createSuccessResult( + "Cluster failure domain retrieved successfully", result); + } catch (PulsarAdminException.NotFoundException e) { + return createErrorResult( + "Domain '" + + domainName + + "' not found in cluster '" + + clusterName + + "'"); + } + } else { + Map raw = + pulsarAdmin.clusters().getFailureDomains(clusterName); + if (raw == null) { + raw = Map.of(); + } + + List> domains = new ArrayList<>(); + int brokerTotal = 0; + List emptyDomains = new ArrayList<>(); + + for (Map.Entry e : raw.entrySet()) { + String dn = e.getKey(); + Set brokers = + (e.getValue() != null && e.getValue().getBrokers() != null) + ? new HashSet<>(e.getValue().getBrokers()) + : new HashSet<>(); + + if (!includeEmpty && brokers.isEmpty()) { + emptyDomains.add(dn); + continue; + } + brokerTotal += brokers.size(); + + Map item = new HashMap<>(); + item.put("domainName", dn); + item.put("brokers", brokers.stream().sorted().toList()); + item.put("brokerCount", brokers.size()); + domains.add(item); + } + + domains.sort(Comparator.comparing(m -> (String) m.get("domainName"))); + + result.put("domains", domains); + result.put("domainCount", domains.size()); + result.put("brokerTotal", brokerTotal); + if (!emptyDomains.isEmpty()) { + result.put("filteredEmptyDomains", emptyDomains.stream().sorted().toList()); + } + result.put("available", !domains.isEmpty()); + + String msg = + "Cluster failure domains retrieved successfully" + + (includeEmpty ? "" : " (empty domains filtered)"); + return createSuccessResult(msg, result); } + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (PulsarAdminException e) { + return createErrorResult("Pulsar admin error: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get failure domains", e); + return createErrorResult("Failed to get failure domains: " + e.getMessage()); + } }) - .build()); - } - - private void registerSetClusterFailureDomain(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "set-cluster-failure-domain", - "Set or update a failure domain in a Pulsar cluster", - """ + .build()); + } + + private void registerSetClusterFailureDomain(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "set-cluster-failure-domain", + "Set or update a failure domain in a Pulsar cluster", + """ { "type": "object", "properties": { @@ -813,111 +853,123 @@ private void registerSetClusterFailureDomain(McpSyncServer mcpServer) { }, "required": ["clusterName", "domainName", "brokers"] } - """ - ); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String clusterName = + getRequiredStringParam(request.arguments(), "clusterName").trim(); + String domainName = + getRequiredStringParam(request.arguments(), "domainName").trim(); + Integer minDomains = getIntParam(request.arguments(), "minDomains", 1); + boolean validate = + getBooleanParam(request.arguments(), "validateBrokers", true); + + Object brokersObj = request.arguments().get("brokers"); + if (!(brokersObj instanceof List rawList)) { + return createErrorResult( + "Parameter 'brokers' must be a non-empty string list"); + } + List brokers = new ArrayList<>(rawList.size()); + for (int i = 0; i < rawList.size(); i++) { + Object v = rawList.get(i); + if (!(v instanceof String s) || (s = s.trim()).isEmpty()) { + return createErrorResult( + "All brokers must be non-empty strings " + + "(invalid at index " + + i + + ")"); + } + brokers.add(s); + } + if (brokers.isEmpty()) { + return createErrorResult("brokers list cannot be empty"); + } + if (minDomains < 1) { + return createErrorResult("minDomains must be at least 1"); + } - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { try { - String clusterName = getRequiredStringParam(request.arguments(), "clusterName").trim(); - String domainName = getRequiredStringParam(request.arguments(), "domainName").trim(); - Integer minDomains = getIntParam(request.arguments(), "minDomains", 1); - boolean validate = getBooleanParam(request.arguments(), - "validateBrokers", true); - - Object brokersObj = request.arguments().get("brokers"); - if (!(brokersObj instanceof List rawList)) { - return createErrorResult("Parameter 'brokers' must be a non-empty string list"); - } - List brokers = new ArrayList<>(rawList.size()); - for (int i = 0; i < rawList.size(); i++) { - Object v = rawList.get(i); - if (!(v instanceof String s) || (s = s.trim()).isEmpty()) { - return createErrorResult("All brokers must be non-empty strings " - + "(invalid at index " + i + ")"); - } - brokers.add(s); - } - if (brokers.isEmpty()) { - return createErrorResult("brokers list cannot be empty"); - } - if (minDomains < 1) { - return createErrorResult("minDomains must be at least 1"); - } - - try { - pulsarAdmin.clusters().getCluster(clusterName); - Set brokerSet = new HashSet<>(brokers); - Map existing = - pulsarAdmin.clusters().getFailureDomains(clusterName); - - if (validate) { - for (Map.Entry e : existing.entrySet()) { - String dn = e.getKey(); - if (dn.equals(domainName)) { - continue; - } - Set used = e.getValue().getBrokers(); - for (String b : brokerSet) { - if (used != null && used.contains(b)) { - return createErrorResult("broker '" - + b + "' already belongs to domain '" - + dn + "'"); - } - } - } + pulsarAdmin.clusters().getCluster(clusterName); + Set brokerSet = new HashSet<>(brokers); + Map existing = + pulsarAdmin.clusters().getFailureDomains(clusterName); + + if (validate) { + for (Map.Entry e : existing.entrySet()) { + String dn = e.getKey(); + if (dn.equals(domainName)) { + continue; + } + Set used = e.getValue().getBrokers(); + for (String b : brokerSet) { + if (used != null && used.contains(b)) { + return createErrorResult( + "broker '" + b + "' already belongs to domain '" + dn + "'"); } + } + } + } + + boolean isUpdate = existing.containsKey(domainName); + + FailureDomain domainObj; + domainObj = FailureDomain.builder().brokers(brokerSet).build(); + + if (isUpdate) { + pulsarAdmin + .clusters() + .updateFailureDomain(clusterName, domainName, domainObj); + } else { + pulsarAdmin + .clusters() + .createFailureDomain(clusterName, domainName, domainObj); + } + + FailureDomain resultDomain = + pulsarAdmin.clusters().getFailureDomain(clusterName, domainName); + + boolean minMet = false; + try { + Map after = + pulsarAdmin.clusters().getFailureDomains(clusterName); + minMet = after != null && after.size() >= minDomains; + } catch (Exception ignore) { + } + + Map result = new HashMap<>(); + result.put("clusterName", clusterName); + result.put("domainName", domainName); + result.put("brokers", new ArrayList<>(brokers)); + result.put("actualBrokers", new ArrayList<>(resultDomain.getBrokers())); + result.put("operation", isUpdate ? "update" : "create"); + result.put("set", true); + result.put("timestamp", System.currentTimeMillis()); + result.put("minDomains", minDomains); + result.put("minDomainsMet", minMet); + + String msg = + "Failure domain " + + (isUpdate ? "updated" : "created") + + " successfully" + + (minMet ? "" : " (warning: minDomains not met)"); + return createSuccessResult(msg, result); - boolean isUpdate = existing.containsKey(domainName); - - FailureDomain domainObj; - domainObj = FailureDomain - .builder().brokers(brokerSet).build(); - - if (isUpdate) { - pulsarAdmin.clusters().updateFailureDomain(clusterName, domainName, domainObj); - } else { - pulsarAdmin.clusters().createFailureDomain(clusterName, domainName, domainObj); - } - - FailureDomain resultDomain = - pulsarAdmin.clusters().getFailureDomain(clusterName, domainName); - - boolean minMet = false; - try { - Map after = - pulsarAdmin.clusters().getFailureDomains(clusterName); - minMet = after != null && after.size() >= minDomains; - } catch (Exception ignore) {} - - Map result = new HashMap<>(); - result.put("clusterName", clusterName); - result.put("domainName", domainName); - result.put("brokers", new ArrayList<>(brokers)); - result.put("actualBrokers", new ArrayList<>(resultDomain.getBrokers())); - result.put("operation", isUpdate ? "update" : "create"); - result.put("set", true); - result.put("timestamp", System.currentTimeMillis()); - result.put("minDomains", minDomains); - result.put("minDomainsMet", minMet); - - String msg = "Failure domain " + (isUpdate ? "updated" : "created") + " successfully" - + (minMet ? "" : " (warning: minDomains not met)"); - return createSuccessResult(msg, result); - - } catch (PulsarAdminException e) { - return createErrorResult("Pulsar admin error: " + e.getMessage()); - } - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to process set failure domain request", e); - return createErrorResult("Failed to process request: " + e.getMessage()); + } catch (PulsarAdminException e) { + return createErrorResult("Pulsar admin error: " + e.getMessage()); } - }) - .build()); - } + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to process set failure domain request", e); + return createErrorResult("Failed to process request: " + e.getMessage()); + } + }) + .build()); + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/MessageTools.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/MessageTools.java index fd63a28..0b3d7ea 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/MessageTools.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/MessageTools.java @@ -41,69 +41,72 @@ public class MessageTools extends BasePulsarTools { - private final PulsarClientManager pulsarClientManager; - private final ConcurrentMap> producerCache = new ConcurrentHashMap<>(); - - public MessageTools(PulsarAdmin pulsarAdmin, PulsarClientManager pulsarClientManager) { - super(pulsarAdmin); - this.pulsarClientManager = pulsarClientManager; + private final PulsarClientManager pulsarClientManager; + private final ConcurrentMap> producerCache = new ConcurrentHashMap<>(); + + public MessageTools(PulsarAdmin pulsarAdmin, PulsarClientManager pulsarClientManager) { + super(pulsarAdmin); + this.pulsarClientManager = pulsarClientManager; + } + + protected PulsarClient getClient() throws Exception { + return pulsarClientManager.getClient(); + } + + private Producer getOrCreateProducer(String fullTopic) throws Exception { + Producer existing = producerCache.get(fullTopic); + if (existing != null) { + return existing; } - - protected PulsarClient getClient() throws Exception { - return pulsarClientManager.getClient(); + PulsarClient client = getClient(); + if (client == null) { + throw new RuntimeException( + "PulsarClient is not available. " + "Please check your Pulsar connection configuration."); } - - private Producer getOrCreateProducer(String fullTopic) throws Exception { - Producer existing = producerCache.get(fullTopic); - if (existing != null) { - return existing; - } - PulsarClient client = getClient(); - if (client == null) { - throw new RuntimeException("PulsarClient is not available. " - + "Please check your Pulsar connection configuration."); - } - if (client.isClosed()) { - throw new RuntimeException("PulsarClient is closed. Please restart the MCP server."); - } - - Producer newProducer = client.newProducer() - .topic(fullTopic) - .enableBatching(true) - .batchingMaxPublishDelay(5, TimeUnit.MILLISECONDS) - .blockIfQueueFull(true) - .compressionType(CompressionType.LZ4) - .sendTimeout(30, TimeUnit.SECONDS) - .create(); - - Producer actual = producerCache.putIfAbsent(fullTopic, newProducer); - if (actual != null) { - try { - newProducer.close(); - } catch (Exception ignored) { - - } - return actual; - } - return newProducer; + if (client.isClosed()) { + throw new RuntimeException("PulsarClient is closed. Please restart the MCP server."); } - public void registerTools(McpSyncServer mcpServer) { - registerPeekMessage(mcpServer); - registerExamineMessages(mcpServer); - registerSkipAllMessages(mcpServer); - registerExpireAllMessages(mcpServer); - registerGetMessageBacklog(mcpServer); - registerSendMessage(mcpServer); - registerGetMessageStats(mcpServer); - registerReceiveMessages(mcpServer); + Producer newProducer = + client + .newProducer() + .topic(fullTopic) + .enableBatching(true) + .batchingMaxPublishDelay(5, TimeUnit.MILLISECONDS) + .blockIfQueueFull(true) + .compressionType(CompressionType.LZ4) + .sendTimeout(30, TimeUnit.SECONDS) + .create(); + + Producer actual = producerCache.putIfAbsent(fullTopic, newProducer); + if (actual != null) { + try { + newProducer.close(); + } catch (Exception ignored) { + + } + return actual; } - - private void registerPeekMessage(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "peek-message", - "Peek messages from a subscription without acknowledging them", - """ + return newProducer; + } + + public void registerTools(McpSyncServer mcpServer) { + registerPeekMessage(mcpServer); + registerExamineMessages(mcpServer); + registerSkipAllMessages(mcpServer); + registerExpireAllMessages(mcpServer); + registerGetMessageBacklog(mcpServer); + registerSendMessage(mcpServer); + registerGetMessageStats(mcpServer); + registerReceiveMessages(mcpServer); + } + + private void registerPeekMessage(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "peek-message", + "Peek messages from a subscription without acknowledging them", + """ { "type": "object", "properties": { @@ -124,59 +127,63 @@ private void registerPeekMessage(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getRequiredStringParam(request.arguments(), "subscriptionName"); - int numMessages = getIntParam(request.arguments(), "numMessages", 1); - - if (numMessages <= 0) { - return createErrorResult("Number of messages must be greater than 0."); - } - - List> messages = pulsarAdmin.topics() - .peekMessages(topic, subscriptionName, numMessages); - - List> messageList = new ArrayList<>(); - for (Message msg : messages) { - Map msgInfo = new HashMap<>(); - msgInfo.put("messageId", msg.getMessageId().toString()); - msgInfo.put("properties", msg.getProperties()); - msgInfo.put("publishTime", msg.getPublishTime()); - msgInfo.put("data", new String(msg.getData())); - messageList.add(msgInfo); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getRequiredStringParam(request.arguments(), "subscriptionName"); + int numMessages = getIntParam(request.arguments(), "numMessages", 1); + + if (numMessages <= 0) { + return createErrorResult("Number of messages must be greater than 0."); + } - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscriptionName); - result.put("messagesCount", messages.size()); - result.put("messages", messageList); + List> messages = + pulsarAdmin.topics().peekMessages(topic, subscriptionName, numMessages); + + List> messageList = new ArrayList<>(); + for (Message msg : messages) { + Map msgInfo = new HashMap<>(); + msgInfo.put("messageId", msg.getMessageId().toString()); + msgInfo.put("properties", msg.getProperties()); + msgInfo.put("publishTime", msg.getPublishTime()); + msgInfo.put("data", new String(msg.getData())); + messageList.add(msgInfo); + } - addTopicBreakdown(result, topic); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("messagesCount", messages.size()); + result.put("messages", messageList); - return createSuccessResult("Peeked " + messageList.size() + " message(s) successfully", result); + addTopicBreakdown(result, topic); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to peek messages", e); - return createErrorResult("Failed to peek messages: " + e.getMessage()); - } - }).build() - ); - } + return createSuccessResult( + "Peeked " + messageList.size() + " message(s) successfully", result); - private void registerExamineMessages(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "examine-messages", - "Examine messages from a topic without consuming them", - """ + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to peek messages", e); + return createErrorResult("Failed to peek messages: " + e.getMessage()); + } + }) + .build()); + } + + private void registerExamineMessages(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "examine-messages", + "Examine messages from a topic without consuming them", + """ { "type": "object", "properties": { @@ -197,74 +204,78 @@ private void registerExamineMessages(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getStringParam(request.arguments(), "subscriptionName"); - int numMessages = getIntParam(request.arguments(), "numMessages", 5); - - if (numMessages <= 0) { - return createErrorResult("Invalid number of messages for examine"); - } - - List> messages = pulsarAdmin.topics().peekMessages( - topic, - subscriptionName, - numMessages); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getStringParam(request.arguments(), "subscriptionName"); + int numMessages = getIntParam(request.arguments(), "numMessages", 5); + + if (numMessages <= 0) { + return createErrorResult("Invalid number of messages for examine"); + } - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscriptionName); - result.put("messageCount", messages.size()); - - List> detailedMessages = messages.stream() - .map(message -> { - Map messageInfo = new HashMap<>(); - messageInfo.put("messageId", message.getMessageId().toString()); - messageInfo.put("properties", message.getProperties()); - messageInfo.put("eventTime", message.getEventTime()); - messageInfo.put("key", message.getKey()); - messageInfo.put("publishTime", message.getPublishTime()); - messageInfo.put("payloadBase64", - Base64.getEncoder().encodeToString(message.getData())); - try { - messageInfo.put("textUtf8", - new String(message.getData(), StandardCharsets.UTF_8)); - } catch (Exception ignore) { - - } - messageInfo.put("producerName", message.getProducerName()); - return messageInfo; + List> messages = + pulsarAdmin.topics().peekMessages(topic, subscriptionName, numMessages); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("messageCount", messages.size()); + + List> detailedMessages = + messages.stream() + .map( + message -> { + Map messageInfo = new HashMap<>(); + messageInfo.put("messageId", message.getMessageId().toString()); + messageInfo.put("properties", message.getProperties()); + messageInfo.put("eventTime", message.getEventTime()); + messageInfo.put("key", message.getKey()); + messageInfo.put("publishTime", message.getPublishTime()); + messageInfo.put( + "payloadBase64", + Base64.getEncoder().encodeToString(message.getData())); + try { + messageInfo.put( + "textUtf8", + new String(message.getData(), StandardCharsets.UTF_8)); + } catch (Exception ignore) { + + } + messageInfo.put("producerName", message.getProducerName()); + return messageInfo; }) - .collect(Collectors.toList()); + .collect(Collectors.toList()); + result.put("messages", detailedMessages); - result.put("messages", detailedMessages); + addTopicBreakdown(result, topic); - addTopicBreakdown(result, topic); + return createSuccessResult("Examined messages successfully", result); - return createSuccessResult("Examined messages successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to examine messages", e); - return createErrorResult("Failed to examine messages: " + e.getMessage()); - } - }).build() - ); - } - - private void registerSkipAllMessages(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "skip-all-messages", - "Skip all messages in a subscription (set cursor to latest)", - """ + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to examine messages", e); + return createErrorResult("Failed to examine messages: " + e.getMessage()); + } + }) + .build()); + } + + private void registerSkipAllMessages(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "skip-all-messages", + "Skip all messages in a subscription (set cursor to latest)", + """ { "type": "object", "properties": { @@ -279,44 +290,46 @@ private void registerSkipAllMessages(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getRequiredStringParam(request.arguments(), "subscriptionName"); - - pulsarAdmin.topics().skipAllMessages(topic, subscriptionName); - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", - subscriptionName); - result.put("skippedAll", true); - - addTopicBreakdown(result, topic); - - return createSuccessResult("Skipped all messages for subscription: " - + subscriptionName, result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Unexpected error while skipping messages", e); - return createErrorResult("Unexpected error: " + e.getMessage()); - } - }).build() - ); - } - - private void registerExpireAllMessages(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "expire-all-messages", - "Expire all messages in a subscription immediately", - """ + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getRequiredStringParam(request.arguments(), "subscriptionName"); + + pulsarAdmin.topics().skipAllMessages(topic, subscriptionName); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("skippedAll", true); + + addTopicBreakdown(result, topic); + + return createSuccessResult( + "Skipped all messages for subscription: " + subscriptionName, result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error while skipping messages", e); + return createErrorResult("Unexpected error: " + e.getMessage()); + } + }) + .build()); + } + + private void registerExpireAllMessages(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "expire-all-messages", + "Expire all messages in a subscription immediately", + """ { "type": "object", "properties": { @@ -331,52 +344,55 @@ private void registerExpireAllMessages(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getRequiredStringParam(request.arguments(), "subscriptionName"); - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getRequiredStringParam(request.arguments(), "subscriptionName"); - - try { - var subs = pulsarAdmin.topics().getSubscriptions(topic); - if (subs == null || !subs.contains(subscriptionName)) { - return createErrorResult("Subscription not found: " + subscriptionName); - } - } catch (Exception ignore) { + var subs = pulsarAdmin.topics().getSubscriptions(topic); + if (subs == null || !subs.contains(subscriptionName)) { + return createErrorResult("Subscription not found: " + subscriptionName); + } + } catch (Exception ignore) { - } + } - pulsarAdmin.topics().expireMessages(topic, subscriptionName, 0); + pulsarAdmin.topics().expireMessages(topic, subscriptionName, 0); - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscriptionName); - result.put("expiredAll", true); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("expiredAll", true); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Expired all messages for subscription: " - + subscriptionName, result); + return createSuccessResult( + "Expired all messages for subscription: " + subscriptionName, result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Unexpected error while expiring messages", e); - return createErrorResult("Unexpected error: " + e.getMessage()); - } - }).build() - ); - } - - private void registerGetMessageBacklog(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-message-backlog", - "Get the current backlog message count for a subscription", - """ + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error while expiring messages", e); + return createErrorResult("Unexpected error: " + e.getMessage()); + } + }) + .build()); + } + + private void registerGetMessageBacklog(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-message-backlog", + "Get the current backlog message count for a subscription", + """ { "type": "object", "properties": { @@ -391,72 +407,75 @@ private void registerGetMessageBacklog(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getRequiredStringParam(request.arguments(), "subscriptionName"); - - long backlog = 0L; - boolean found = false; - - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - if (meta != null && meta.partitions > 0) { - var ps = pulsarAdmin.topics().getPartitionedStats(topic, true); - if (ps != null && ps.getPartitions() != null) { - for (var partEntry : ps.getPartitions().entrySet()) { - TopicStats ts = partEntry.getValue(); - if (ts != null && ts.getSubscriptions() != null) { - var sub = ts.getSubscriptions().get(subscriptionName); - if (sub != null) { - backlog += sub.getMsgBacklog(); - found = true; - } - } - } - } - } else { - TopicStats stats = pulsarAdmin.topics().getStats(topic); - if (stats != null && stats.getSubscriptions() != null) { - var sub = stats.getSubscriptions().get(subscriptionName); - if (sub != null) { - backlog = sub.getMsgBacklog(); - found = true; - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getRequiredStringParam(request.arguments(), "subscriptionName"); + + long backlog = 0L; + boolean found = false; + + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + if (meta != null && meta.partitions > 0) { + var ps = pulsarAdmin.topics().getPartitionedStats(topic, true); + if (ps != null && ps.getPartitions() != null) { + for (var partEntry : ps.getPartitions().entrySet()) { + TopicStats ts = partEntry.getValue(); + if (ts != null && ts.getSubscriptions() != null) { + var sub = ts.getSubscriptions().get(subscriptionName); + if (sub != null) { + backlog += sub.getMsgBacklog(); + found = true; } + } } - - if (!found) { - return createErrorResult("Subscription not found: " + subscriptionName); + } + } else { + TopicStats stats = pulsarAdmin.topics().getStats(topic); + if (stats != null && stats.getSubscriptions() != null) { + var sub = stats.getSubscriptions().get(subscriptionName); + if (sub != null) { + backlog = sub.getMsgBacklog(); + found = true; } + } + } - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscriptionName); - result.put("backlog", backlog); + if (!found) { + return createErrorResult("Subscription not found: " + subscriptionName); + } - addTopicBreakdown(result, topic); - return createSuccessResult("Backlog retrieved successfully", result); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("backlog", backlog); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Unexpected error while getting message backlog", e); - return createErrorResult("Unexpected error: " + e.getMessage()); - } - }).build() - ); - } + addTopicBreakdown(result, topic); + return createSuccessResult("Backlog retrieved successfully", result); - private void registerGetMessageStats(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-message-stats", - "Get message statistics for a topic or subscription", - """ + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error while getting message backlog", e); + return createErrorResult("Unexpected error: " + e.getMessage()); + } + }) + .build()); + } + + private void registerGetMessageStats(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-message-stats", + "Get message statistics for a topic or subscription", + """ { "type": "object", "properties": { @@ -471,98 +490,101 @@ private void registerGetMessageStats(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getStringParam(request.arguments(), "subscriptionName"); - - Map result = new HashMap<>(); - result.put("topic", topic); - - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - if (meta != null && meta.partitions > 0) { - var ps = pulsarAdmin.topics().getPartitionedStats(topic, true); - if (ps == null) { - return createErrorResult("Failed to fetch partitioned stats"); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getStringParam(request.arguments(), "subscriptionName"); + + Map result = new HashMap<>(); + result.put("topic", topic); + + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + if (meta != null && meta.partitions > 0) { + var ps = pulsarAdmin.topics().getPartitionedStats(topic, true); + if (ps == null) { + return createErrorResult("Failed to fetch partitioned stats"); + } + + result.put("msgInCounter", ps.getMsgInCounter()); + result.put("msgOutCounter", ps.getMsgOutCounter()); + result.put("bytesInCounter", ps.getBytesInCounter()); + result.put("bytesOutCounter", ps.getBytesOutCounter()); + + if (subscriptionName != null && !subscriptionName.isBlank()) { + long backlog = 0L; + double rateOut = 0D; + double rateRedeliver = 0D; + boolean found = false; - result.put("msgInCounter", ps.getMsgInCounter()); - result.put("msgOutCounter", ps.getMsgOutCounter()); - result.put("bytesInCounter", ps.getBytesInCounter()); - result.put("bytesOutCounter", ps.getBytesOutCounter()); - - if (subscriptionName != null && !subscriptionName.isBlank()) { - long backlog = 0L; - double rateOut = 0D; - double rateRedeliver = 0D; - boolean found = false; - - if (ps.getPartitions() != null) { - for (var partEntry : ps.getPartitions().entrySet()) { - TopicStats ts = partEntry.getValue(); - if (ts != null && ts.getSubscriptions() != null) { - var sub = ts.getSubscriptions().get(subscriptionName); - if (sub != null) { - backlog += sub.getMsgBacklog(); - rateOut += sub.getMsgRateOut(); - rateRedeliver += sub.getMsgRateRedeliver(); - found = true; - } - } - } - } - - if (!found) { - return createErrorResult("Subscription not found: " + subscriptionName); - } - result.put("subscriptionName", subscriptionName); - result.put("msgBacklog", backlog); - result.put("msgRateOut", rateOut); - result.put("msgRateRedeliver", rateRedeliver); - } - } else { - TopicStats stats = pulsarAdmin.topics().getStats(topic); - result.put("msgInCounter", stats.getMsgInCounter()); - result.put("msgOutCounter", stats.getMsgOutCounter()); - result.put("bytesInCounter", stats.getBytesInCounter()); - result.put("bytesOutCounter", stats.getBytesOutCounter()); - - if (subscriptionName != null && !subscriptionName.isBlank()) { - if (stats.getSubscriptions() == null - || !stats.getSubscriptions().containsKey(subscriptionName)) { - return createErrorResult("Subscription not found: " + subscriptionName); - } - SubscriptionStats subStats = stats.getSubscriptions().get(subscriptionName); - result.put("subscriptionName", subscriptionName); - result.put("msgBacklog", subStats.getMsgBacklog()); - result.put("msgRateOut", subStats.getMsgRateOut()); - result.put("msgRateRedeliver", subStats.getMsgRateRedeliver()); + if (ps.getPartitions() != null) { + for (var partEntry : ps.getPartitions().entrySet()) { + TopicStats ts = partEntry.getValue(); + if (ts != null && ts.getSubscriptions() != null) { + var sub = ts.getSubscriptions().get(subscriptionName); + if (sub != null) { + backlog += sub.getMsgBacklog(); + rateOut += sub.getMsgRateOut(); + rateRedeliver += sub.getMsgRateRedeliver(); + found = true; + } } + } } - addTopicBreakdown(result, topic); - return createSuccessResult("Fetched message stats successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Unexpected error while getting message stats", e); - return createErrorResult("Unexpected error: " + e.getMessage()); + if (!found) { + return createErrorResult("Subscription not found: " + subscriptionName); + } + result.put("subscriptionName", subscriptionName); + result.put("msgBacklog", backlog); + result.put("msgRateOut", rateOut); + result.put("msgRateRedeliver", rateRedeliver); + } + } else { + TopicStats stats = pulsarAdmin.topics().getStats(topic); + result.put("msgInCounter", stats.getMsgInCounter()); + result.put("msgOutCounter", stats.getMsgOutCounter()); + result.put("bytesInCounter", stats.getBytesInCounter()); + result.put("bytesOutCounter", stats.getBytesOutCounter()); + + if (subscriptionName != null && !subscriptionName.isBlank()) { + if (stats.getSubscriptions() == null + || !stats.getSubscriptions().containsKey(subscriptionName)) { + return createErrorResult("Subscription not found: " + subscriptionName); + } + SubscriptionStats subStats = stats.getSubscriptions().get(subscriptionName); + result.put("subscriptionName", subscriptionName); + result.put("msgBacklog", subStats.getMsgBacklog()); + result.put("msgRateOut", subStats.getMsgRateOut()); + result.put("msgRateRedeliver", subStats.getMsgRateRedeliver()); + } } - }).build() - ); - } - private void registerSendMessage(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "send-message", - "Send a message to a specified topic", - """ + addTopicBreakdown(result, topic); + return createSuccessResult("Fetched message stats successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error while getting message stats", e); + return createErrorResult("Unexpected error: " + e.getMessage()); + } + }) + .build()); + } + + private void registerSendMessage(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "send-message", + "Send a message to a specified topic", + """ { "type": "object", "properties": { @@ -585,60 +607,68 @@ private void registerSendMessage(McpSyncServer mcpServer) { }, "required": ["topic", "message"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String fullTopic = buildFullTopicName(request.arguments()); - String message = getRequiredStringParam(request.arguments(), "message"); - String key = getStringParam(request.arguments(), "key"); - - Map propsSafe = new HashMap<>(); - Object propsObj = request.arguments().get("properties"); - if (propsObj instanceof Map raw) { - for (var e : raw.entrySet()) { - if (e.getKey() != null && e.getValue() != null) { - propsSafe.put(String.valueOf(e.getKey()), String.valueOf(e.getValue())); - } - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String fullTopic = buildFullTopicName(request.arguments()); + String message = getRequiredStringParam(request.arguments(), "message"); + String key = getStringParam(request.arguments(), "key"); + + Map propsSafe = new HashMap<>(); + Object propsObj = request.arguments().get("properties"); + if (propsObj instanceof Map raw) { + for (var e : raw.entrySet()) { + if (e.getKey() != null && e.getValue() != null) { + propsSafe.put(String.valueOf(e.getKey()), String.valueOf(e.getValue())); } + } + } - Producer producer = getOrCreateProducer(fullTopic); - - TypedMessageBuilder builder = producer.newMessage() - .value(message.getBytes(StandardCharsets.UTF_8)); - - if (key != null && !key.isEmpty()) { - builder = builder.key(key); - } - if (!propsSafe.isEmpty()) { - builder = builder.properties(propsSafe); - } + Producer producer = getOrCreateProducer(fullTopic); - MessageId msgId = builder.send(); + TypedMessageBuilder builder = + producer.newMessage().value(message.getBytes(StandardCharsets.UTF_8)); - return createSuccessResult("Message sent", Map.of( - "topic", fullTopic, - "messageId", msgId.toString(), - "messageContent", message, - "bytes", message.getBytes(StandardCharsets.UTF_8).length - )); - } catch (IllegalArgumentException iae) { - return createErrorResult("Invalid arguments: " + iae.getMessage()); - } catch (Exception e) { - return createErrorResult("Failed to send message: " + e.getMessage()); + if (key != null && !key.isEmpty()) { + builder = builder.key(key); + } + if (!propsSafe.isEmpty()) { + builder = builder.properties(propsSafe); } - }).build()); - } - private void registerReceiveMessages(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "receive-messages", - "Receive messages from a Pulsar topic", - """ + MessageId msgId = builder.send(); + + return createSuccessResult( + "Message sent", + Map.of( + "topic", + fullTopic, + "messageId", + msgId.toString(), + "messageContent", + message, + "bytes", + message.getBytes(StandardCharsets.UTF_8).length)); + } catch (IllegalArgumentException iae) { + return createErrorResult("Invalid arguments: " + iae.getMessage()); + } catch (Exception e) { + return createErrorResult("Failed to send message: " + e.getMessage()); + } + }) + .build()); + } + + private void registerReceiveMessages(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "receive-messages", + "Receive messages from a Pulsar topic", + """ { "type": "object", "properties": { @@ -676,138 +706,151 @@ private void registerReceiveMessages(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getRequiredStringParam(request.arguments(), "subscriptionName"); + int messageCount = + Math.max(1, getIntParam(request.arguments(), "messageCount", 10)); + + if (messageCount > 1000) { + return createErrorResult("messageCount too large (max 1000)"); + } + int timeoutMs = + Math.max(1000, getIntParam(request.arguments(), "timeoutMs", 5000)); + if (timeoutMs > 120_000) { + return createErrorResult("timeoutMs too large (max 120000)"); + } + boolean ack = getBooleanParam(request.arguments(), "ack", true); + String subTypeStr = getStringParam(request.arguments(), "subscriptionType"); + SubscriptionType subType = SubscriptionType.Shared; + if (subTypeStr != null) { + try { + subType = SubscriptionType.valueOf(subTypeStr.replace('-', '_')); + } catch (IllegalArgumentException ignore) { + + } + } - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getRequiredStringParam(request.arguments(), "subscriptionName"); - int messageCount = Math.max(1, getIntParam(request.arguments(), "messageCount", 10)); - - if (messageCount > 1000) { - return createErrorResult("messageCount too large (max 1000)"); - } - int timeoutMs = Math.max(1000, getIntParam(request.arguments(), "timeoutMs", 5000)); - if (timeoutMs > 120_000) { - return createErrorResult("timeoutMs too large (max 120000)"); - } - boolean ack = getBooleanParam(request.arguments(), "ack", true); - String subTypeStr = getStringParam(request.arguments(), "subscriptionType"); - SubscriptionType subType = SubscriptionType.Shared; - if (subTypeStr != null) { - try { - subType = SubscriptionType.valueOf(subTypeStr.replace('-', '_')); - } catch (IllegalArgumentException ignore) { - - } - } - - try { - List subs = pulsarAdmin.topics().getSubscriptions(topic); - if (subs == null || !subs.contains(subscriptionName)) { - return createErrorResult("Subscription not found: " + subscriptionName); - } - } catch (Exception e) { - LOGGER.warn("Failed to verify subscription existence for {}: {}", topic, e.toString()); - } - - List> out = new ArrayList<>(); + List subs = pulsarAdmin.topics().getSubscriptions(topic); + if (subs == null || !subs.contains(subscriptionName)) { + return createErrorResult("Subscription not found: " + subscriptionName); + } + } catch (Exception e) { + LOGGER.warn( + "Failed to verify subscription existence for {}: {}", + topic, + e.toString()); + } - try { - PulsarClient client = getClient(); - if (client == null) { - return createErrorResult("Pulsar Client is not available"); - } + List> out = new ArrayList<>(); - int rq = Math.min(Math.max(messageCount, 10), 1000); - long deadline = System.nanoTime() + (timeoutMs * 1_000_000L); - - try (Consumer consumer = client.newConsumer() - .topic(topic) - .subscriptionName(subscriptionName) - .subscriptionType(subType) - .receiverQueueSize(rq) - .subscribe()) { - - for (int i = 0; i < messageCount; i++) { - long remainMs = Math.max(0L, (deadline - System.nanoTime()) / 1_000_000L); - if (remainMs == 0) { - break; - } - - Message msg = consumer.receive((int) Math.min(remainMs, Integer.MAX_VALUE), - TimeUnit.MILLISECONDS); - if (msg == null) { - break; - } - - Map m = new HashMap<>(); - m.put("messageId", msg.getMessageId().toString()); - m.put("key", msg.getKey()); - byte[] data = msg.getData(); - m.put("dataUtf8", safeToUtf8(data)); - m.put("dataBase64", java.util.Base64.getEncoder().encodeToString(data)); - m.put("publishTime", msg.getPublishTime()); - m.put("eventTime", msg.getEventTime()); - m.put("properties", msg.getProperties()); - m.put("producerName", msg.getProducerName()); - try { - m.put("redeliveryCount", msg.getRedeliveryCount()); - } catch (Throwable ignore) {} - m.put("dataSize", data != null ? data.length : 0); - - out.add(m); - - if (ack) { - try { - consumer.acknowledge(msg); - } catch (Exception ackEx) { - LOGGER.warn("Acknowledge failed: {}", ackEx.toString()); - } - } - } + try { + PulsarClient client = getClient(); + if (client == null) { + return createErrorResult("Pulsar Client is not available"); + } + + int rq = Math.min(Math.max(messageCount, 10), 1000); + long deadline = System.nanoTime() + (timeoutMs * 1_000_000L); + + try (Consumer consumer = + client + .newConsumer() + .topic(topic) + .subscriptionName(subscriptionName) + .subscriptionType(subType) + .receiverQueueSize(rq) + .subscribe()) { + + for (int i = 0; i < messageCount; i++) { + long remainMs = Math.max(0L, (deadline - System.nanoTime()) / 1_000_000L); + if (remainMs == 0) { + break; + } + + Message msg = + consumer.receive( + (int) Math.min(remainMs, Integer.MAX_VALUE), + TimeUnit.MILLISECONDS); + if (msg == null) { + break; + } + + Map m = new HashMap<>(); + m.put("messageId", msg.getMessageId().toString()); + m.put("key", msg.getKey()); + byte[] data = msg.getData(); + m.put("dataUtf8", safeToUtf8(data)); + m.put("dataBase64", java.util.Base64.getEncoder().encodeToString(data)); + m.put("publishTime", msg.getPublishTime()); + m.put("eventTime", msg.getEventTime()); + m.put("properties", msg.getProperties()); + m.put("producerName", msg.getProducerName()); + try { + m.put("redeliveryCount", msg.getRedeliveryCount()); + } catch (Throwable ignore) { + } + m.put("dataSize", data != null ? data.length : 0); + + out.add(m); + + if (ack) { + try { + consumer.acknowledge(msg); + } catch (Exception ackEx) { + LOGGER.warn("Acknowledge failed: {}", ackEx.toString()); } - } catch (Exception clientException) { - LOGGER.error("Failed to receive messages using PulsarClient", clientException); - return createErrorResult("Failed to receive messages - PulsarClient error: " - + clientException.getMessage()); + } } - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscriptionName); - result.put("requestCount", messageCount); - result.put("receivedCount", out.size()); - result.put("ack", ack); - result.put("subscriptionType", subType.toString()); - result.put("timeoutMs", timeoutMs); - result.put("messages", out); - - addTopicBreakdown(result, topic); - return createSuccessResult("Messages received successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid input parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Unexpected error while receiving messages", e); - return createErrorResult("Unexpected error: " + e.getMessage()); + } + } catch (Exception clientException) { + LOGGER.error( + "Failed to receive messages using PulsarClient", clientException); + return createErrorResult( + "Failed to receive messages - PulsarClient error: " + + clientException.getMessage()); } + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("requestCount", messageCount); + result.put("receivedCount", out.size()); + result.put("ack", ack); + result.put("subscriptionType", subType.toString()); + result.put("timeoutMs", timeoutMs); + result.put("messages", out); + + addTopicBreakdown(result, topic); + return createSuccessResult("Messages received successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid input parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error while receiving messages", e); + return createErrorResult("Unexpected error: " + e.getMessage()); + } }) - .build()); - } + .build()); + } - private static String safeToUtf8(byte[] data) { - if (data == null) { - return null; - } - try { - return new String(data, StandardCharsets.UTF_8); - } catch (Throwable ignore) { - return null; - } + private static String safeToUtf8(byte[] data) { + if (data == null) { + return null; } - + try { + return new String(data, StandardCharsets.UTF_8); + } catch (Throwable ignore) { + return null; + } + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/MonitoringTools.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/MonitoringTools.java index 4477360..8d9f791 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/MonitoringTools.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/MonitoringTools.java @@ -35,27 +35,28 @@ import org.apache.pulsar.common.policies.data.SubscriptionStats; import org.apache.pulsar.common.policies.data.TopicStats; -public class MonitoringTools extends BasePulsarTools{ - public MonitoringTools(PulsarAdmin pulsarAdmin) { - super(pulsarAdmin); - } - - PulsarClient pulsarClient; - - public void registerTools(McpSyncServer mcpServer) { - registerMonitorClusterPerformance(mcpServer); - registerMonitorSubscriptionPerformance(mcpServer); - registerMonitorTopicPerformance(mcpServer); - registerHealthCheck(mcpServer); - registerConnectionDiagnostics(mcpServer); - registerBacklogAnalysis(mcpServer); - } - - private void registerMonitorClusterPerformance(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "monitor-cluster-performance", - "Monitor specific Pulsar cluster performance metrics and health", - """ +public class MonitoringTools extends BasePulsarTools { + public MonitoringTools(PulsarAdmin pulsarAdmin) { + super(pulsarAdmin); + } + + PulsarClient pulsarClient; + + public void registerTools(McpSyncServer mcpServer) { + registerMonitorClusterPerformance(mcpServer); + registerMonitorSubscriptionPerformance(mcpServer); + registerMonitorTopicPerformance(mcpServer); + registerHealthCheck(mcpServer); + registerConnectionDiagnostics(mcpServer); + registerBacklogAnalysis(mcpServer); + } + + private void registerMonitorClusterPerformance(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "monitor-cluster-performance", + "Monitor specific Pulsar cluster performance metrics and health", + """ { "type": "object", "properties": { @@ -83,73 +84,78 @@ private void registerMonitorClusterPerformance(McpSyncServer mcpServer) { }, "required": ["clusterName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String clusterName = getStringParam(request.arguments(), "clusterName"); - boolean includeBrokerStats = getBooleanParam(request.arguments(), "includeBrokerStats", true); - int maxTopics = getIntParam(request.arguments(), "maxTopics", 10); - if (maxTopics < 1) { - maxTopics = 1; - } - if (maxTopics > 50) { - maxTopics = 50; - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String clusterName = getStringParam(request.arguments(), "clusterName"); + boolean includeBrokerStats = + getBooleanParam(request.arguments(), "includeBrokerStats", true); + int maxTopics = getIntParam(request.arguments(), "maxTopics", 10); + if (maxTopics < 1) { + maxTopics = 1; + } + if (maxTopics > 50) { + maxTopics = 50; + } - Map result = new HashMap<>(); - result.put("timestamp", System.currentTimeMillis()); + Map result = new HashMap<>(); + result.put("timestamp", System.currentTimeMillis()); - var clusters = pulsarAdmin.clusters().getClusters(); - if (clusterName == null || !clusters.isEmpty()) { - clusterName = clusters.get(0); - } - if (clusterName == null || clusterName.isBlank()) { - return createErrorResult("clusterName is required and no clusters found"); - } - result.put("clusterName", clusterName); + var clusters = pulsarAdmin.clusters().getClusters(); + if (clusterName == null || !clusters.isEmpty()) { + clusterName = clusters.get(0); + } + if (clusterName == null || clusterName.isBlank()) { + return createErrorResult("clusterName is required and no clusters found"); + } + result.put("clusterName", clusterName); - try { - var brokers = pulsarAdmin.brokers().getActiveBrokers(clusterName); - result.put("activeBrokers", brokers.size()); - result.put("brokerList", brokers); - - if (includeBrokerStats) { - Map brokerStats = new HashMap<>(); - for (String broker : brokers) { - try { - brokerStats.put(broker, Map.of("status", "active")); - } catch (Exception e) { - brokerStats.put(broker, Map.of("status", "error", "error", - e.getMessage())); - } - } - result.put("brokerStats", brokerStats); - } - } catch (Exception e) { - result.put("brokerError", e.getMessage()); + try { + var brokers = pulsarAdmin.brokers().getActiveBrokers(clusterName); + result.put("activeBrokers", brokers.size()); + result.put("brokerList", brokers); + + if (includeBrokerStats) { + Map brokerStats = new HashMap<>(); + for (String broker : brokers) { + try { + brokerStats.put(broker, Map.of("status", "active")); + } catch (Exception e) { + brokerStats.put( + broker, Map.of("status", "error", "error", e.getMessage())); + } } - - result.put("clusterHealth", result.containsKey("brokerError") ? "error" : "healthy"); - return createSuccessResult("Cluster performance monitoring completed", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); + result.put("brokerStats", brokerStats); + } } catch (Exception e) { - return createErrorResult("Failed to monitor cluster performance:" + e.getMessage()); + result.put("brokerError", e.getMessage()); } + + result.put( + "clusterHealth", result.containsKey("brokerError") ? "error" : "healthy"); + return createSuccessResult("Cluster performance monitoring completed", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + return createErrorResult( + "Failed to monitor cluster performance:" + e.getMessage()); + } }) - .build()); - } - - private void registerMonitorTopicPerformance(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "monitor-topic-performance", - "Monitor specific Pulsar topic performance metrics and health", - """ + .build()); + } + + private void registerMonitorTopicPerformance(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "monitor-topic-performance", + "Monitor specific Pulsar topic performance metrics and health", + """ { "type": "object", "properties": { @@ -177,205 +183,221 @@ private void registerMonitorTopicPerformance(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - boolean includeDetails = getBooleanParam(request.arguments(), - "includeDetails", false); - boolean includeInternalStats = getBooleanParam(request.arguments(), - "includeInternalStats", false); - int maxSubscriptions = getIntParam(request.arguments(), - "maxSubscriptions", 10); - - Map result = new HashMap<>(); - result.put("timestamp", System.currentTimeMillis()); - result.put("topic", topic); - - try { - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - double msgRateIn = 0.0, msgRateOut = 0.0, msgThroughputIn = 0.0, msgThroughputOut = 0.0; - long storageSize = 0L; - double averageMsgSize = 0.0; - int avgCount = 0; - - Map subMapAgg = new HashMap<>(); - List publishersAgg = new ArrayList<>(); - - if (meta != null && meta.partitions > 0) { - for (int i = 0; i < meta.partitions; i++) { - var s = pulsarAdmin.topics().getStats(topic + "-partition-" + i); - msgRateIn += s.getMsgRateIn(); - msgRateOut += s.getMsgRateOut(); - msgThroughputIn += s.getMsgThroughputIn(); - msgThroughputOut += s.getMsgThroughputOut(); - storageSize += s.getStorageSize(); - if (s.getAverageMsgSize() > 0) { - averageMsgSize += s.getAverageMsgSize(); - avgCount++; - } - - if (s.getSubscriptions() != null) { - s.getSubscriptions().forEach((k, v) -> - subMapAgg.merge(k, v, (a, b) -> { - return b; - })); - } - if (s.getPublishers() != null) { - publishersAgg.addAll(s.getPublishers()); - } - } - } else { - var s = pulsarAdmin.topics().getStats(topic); - msgRateIn = s.getMsgRateIn(); - msgRateOut = s.getMsgRateOut(); - msgThroughputIn = s.getMsgThroughputIn(); - msgThroughputOut = s.getMsgThroughputOut(); - storageSize = s.getStorageSize(); - averageMsgSize = s.getAverageMsgSize(); - avgCount = (averageMsgSize > 0) ? 1 : 0; - if (s.getSubscriptions() != null) { - subMapAgg.putAll(s.getSubscriptions()); - } - if (s.getPublishers() != null) { - publishersAgg.addAll(s.getPublishers()); - } - } - - result.put("msgRateIn", msgRateIn); - result.put("msgRateOut", msgRateOut); - result.put("msgThroughputIn", msgThroughputIn); - result.put("msgThroughputOut", msgThroughputOut); - result.put("storageSize", storageSize); - result.put("averageMsgSize", avgCount == 0 ? 0.0 : (averageMsgSize / avgCount)); - - int publishersCount = publishersAgg == null ? 0 : publishersAgg.size(); - int subscriptionsCount = subMapAgg == null ? 0 : subMapAgg.size(); - result.put("publishersCount", publishersCount); - result.put("subscriptionsCount", subscriptionsCount); - - int totalConsumers = 0; - long totalBacklog = 0L; - if (subMapAgg != null) { - for (var sub : subMapAgg.values()) { - if (sub.getConsumers() != null) { - totalConsumers += sub.getConsumers().size(); - } - totalBacklog += sub.getMsgBacklog(); - } - } - result.put("totalConsumers", totalConsumers); - result.put("totalBacklog", totalBacklog); - - if (includeDetails && subMapAgg != null) { - Map subscriptionStats = new HashMap<>(); - List subscriptionNames = new ArrayList<>(subMapAgg.keySet()); - if (subscriptionNames.size() > maxSubscriptions) { - subscriptionNames = subscriptionNames.subList(0, maxSubscriptions); - } - for (String subName : subscriptionNames) { - var sub = subMapAgg.get(subName); - Map subDetail = new HashMap<>(); - subDetail.put("msgRateOut", sub.getMsgRateOut()); - subDetail.put("msgThroughputOut", sub.getMsgThroughputOut()); - subDetail.put("msgBacklog", sub.getMsgBacklog()); - subDetail.put("msgRateExpired", sub.getMsgRateExpired()); - subDetail.put("consumersCount", sub.getConsumers() == null - ? 0 : sub.getConsumers().size()); - subDetail.put("type", sub.getType()); - - List> consumerDetails = new ArrayList<>(); - if (sub.getConsumers() != null) { - for (var consumer : sub.getConsumers()) { - Map consumerInfo = new HashMap<>(); - consumerInfo.put("consumerName", consumer.getConsumerName()); - consumerInfo.put("msgRateOut", consumer.getMsgRateOut()); - consumerInfo.put("msgThroughputOut", consumer.getMsgThroughputOut()); - consumerInfo.put("availablePermits", consumer.getAvailablePermits()); - consumerInfo.put("unackedMessages", consumer.getUnackedMessages()); - consumerDetails.add(consumerInfo); - } - } - subDetail.put("consumers", consumerDetails); - subscriptionStats.put(subName, subDetail); - } - result.put("subscriptionStats", subscriptionStats); - - List> publisherDetails = new ArrayList<>(); - if (publishersAgg != null) { - for (var publisher : publishersAgg) { - Map pubInfo = new HashMap<>(); - pubInfo.put("producerName", publisher.getProducerName()); - pubInfo.put("msgRateIn", publisher.getMsgRateIn()); - pubInfo.put("msgThroughputIn", publisher.getMsgThroughputIn()); - pubInfo.put("averageMsgSize", publisher.getAverageMsgSize()); - publisherDetails.add(pubInfo); - } - } - result.put("publisherDetails", publisherDetails); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + boolean includeDetails = + getBooleanParam(request.arguments(), "includeDetails", false); + boolean includeInternalStats = + getBooleanParam(request.arguments(), "includeInternalStats", false); + int maxSubscriptions = getIntParam(request.arguments(), "maxSubscriptions", 10); - if (includeInternalStats) { - try { - var internalStats = (meta != null && meta.partitions > 0) - ? pulsarAdmin.topics().getInternalStats(topic + "-partition-0") - : pulsarAdmin.topics().getInternalStats(topic); - Map internal = new HashMap<>(); - internal.put("numberOfEntries", internalStats.numberOfEntries); - internal.put("totalSize", internalStats.totalSize); - internal.put("currentLedgerEntries", internalStats.currentLedgerEntries); - internal.put("currentLedgerSize", internalStats.currentLedgerSize); - internal.put("ledgerCount", internalStats.ledgers == null - ? 0 : internalStats.ledgers.size()); - internal.put("cursorCount", internalStats.cursors == null - ? 0 : internalStats.cursors.size()); - result.put("internalStats", internal); - } catch (Exception e) { - result.put("internalStatsError", e.getMessage()); - } - } + Map result = new HashMap<>(); + result.put("timestamp", System.currentTimeMillis()); + result.put("topic", topic); - } catch (Exception e) { - result.put("statsError", e.getMessage()); + try { + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + double msgRateIn = 0.0, + msgRateOut = 0.0, + msgThroughputIn = 0.0, + msgThroughputOut = 0.0; + long storageSize = 0L; + double averageMsgSize = 0.0; + int avgCount = 0; + + Map subMapAgg = new HashMap<>(); + List publishersAgg = new ArrayList<>(); + + if (meta != null && meta.partitions > 0) { + for (int i = 0; i < meta.partitions; i++) { + var s = pulsarAdmin.topics().getStats(topic + "-partition-" + i); + msgRateIn += s.getMsgRateIn(); + msgRateOut += s.getMsgRateOut(); + msgThroughputIn += s.getMsgThroughputIn(); + msgThroughputOut += s.getMsgThroughputOut(); + storageSize += s.getStorageSize(); + if (s.getAverageMsgSize() > 0) { + averageMsgSize += s.getAverageMsgSize(); + avgCount++; + } + + if (s.getSubscriptions() != null) { + s.getSubscriptions() + .forEach( + (k, v) -> + subMapAgg.merge( + k, + v, + (a, b) -> { + return b; + })); + } + if (s.getPublishers() != null) { + publishersAgg.addAll(s.getPublishers()); + } } - - String topicHealth = "healthy"; - if (result.containsKey("statsError")) { - topicHealth = "error"; - } else { - Long totalBacklogVal = (Long) result.get("totalBacklog"); - Double msgRateInVal = (Double) result.get("msgRateIn"); - Double msgRateOutVal = (Double) result.get("msgRateOut"); - if (totalBacklogVal != null && totalBacklogVal > 100_000) { - topicHealth = "backlog_high"; - } else if (msgRateInVal != null && msgRateOutVal != null - && msgRateInVal > msgRateOutVal * 1.5) { - topicHealth = "consumption_slow"; + } else { + var s = pulsarAdmin.topics().getStats(topic); + msgRateIn = s.getMsgRateIn(); + msgRateOut = s.getMsgRateOut(); + msgThroughputIn = s.getMsgThroughputIn(); + msgThroughputOut = s.getMsgThroughputOut(); + storageSize = s.getStorageSize(); + averageMsgSize = s.getAverageMsgSize(); + avgCount = (averageMsgSize > 0) ? 1 : 0; + if (s.getSubscriptions() != null) { + subMapAgg.putAll(s.getSubscriptions()); + } + if (s.getPublishers() != null) { + publishersAgg.addAll(s.getPublishers()); + } + } + + result.put("msgRateIn", msgRateIn); + result.put("msgRateOut", msgRateOut); + result.put("msgThroughputIn", msgThroughputIn); + result.put("msgThroughputOut", msgThroughputOut); + result.put("storageSize", storageSize); + result.put( + "averageMsgSize", avgCount == 0 ? 0.0 : (averageMsgSize / avgCount)); + + int publishersCount = publishersAgg == null ? 0 : publishersAgg.size(); + int subscriptionsCount = subMapAgg == null ? 0 : subMapAgg.size(); + result.put("publishersCount", publishersCount); + result.put("subscriptionsCount", subscriptionsCount); + + int totalConsumers = 0; + long totalBacklog = 0L; + if (subMapAgg != null) { + for (var sub : subMapAgg.values()) { + if (sub.getConsumers() != null) { + totalConsumers += sub.getConsumers().size(); + } + totalBacklog += sub.getMsgBacklog(); + } + } + result.put("totalConsumers", totalConsumers); + result.put("totalBacklog", totalBacklog); + + if (includeDetails && subMapAgg != null) { + Map subscriptionStats = new HashMap<>(); + List subscriptionNames = new ArrayList<>(subMapAgg.keySet()); + if (subscriptionNames.size() > maxSubscriptions) { + subscriptionNames = subscriptionNames.subList(0, maxSubscriptions); + } + for (String subName : subscriptionNames) { + var sub = subMapAgg.get(subName); + Map subDetail = new HashMap<>(); + subDetail.put("msgRateOut", sub.getMsgRateOut()); + subDetail.put("msgThroughputOut", sub.getMsgThroughputOut()); + subDetail.put("msgBacklog", sub.getMsgBacklog()); + subDetail.put("msgRateExpired", sub.getMsgRateExpired()); + subDetail.put( + "consumersCount", + sub.getConsumers() == null ? 0 : sub.getConsumers().size()); + subDetail.put("type", sub.getType()); + + List> consumerDetails = new ArrayList<>(); + if (sub.getConsumers() != null) { + for (var consumer : sub.getConsumers()) { + Map consumerInfo = new HashMap<>(); + consumerInfo.put("consumerName", consumer.getConsumerName()); + consumerInfo.put("msgRateOut", consumer.getMsgRateOut()); + consumerInfo.put("msgThroughputOut", consumer.getMsgThroughputOut()); + consumerInfo.put("availablePermits", consumer.getAvailablePermits()); + consumerInfo.put("unackedMessages", consumer.getUnackedMessages()); + consumerDetails.add(consumerInfo); } + } + subDetail.put("consumers", consumerDetails); + subscriptionStats.put(subName, subDetail); } - result.put("topicHealth", topicHealth); + result.put("subscriptionStats", subscriptionStats); + + List> publisherDetails = new ArrayList<>(); + if (publishersAgg != null) { + for (var publisher : publishersAgg) { + Map pubInfo = new HashMap<>(); + pubInfo.put("producerName", publisher.getProducerName()); + pubInfo.put("msgRateIn", publisher.getMsgRateIn()); + pubInfo.put("msgThroughputIn", publisher.getMsgThroughputIn()); + pubInfo.put("averageMsgSize", publisher.getAverageMsgSize()); + publisherDetails.add(pubInfo); + } + } + result.put("publisherDetails", publisherDetails); + } - addTopicBreakdown(result, topic); - return createSuccessResult("Topic performance monitoring completed", result); + if (includeInternalStats) { + try { + var internalStats = + (meta != null && meta.partitions > 0) + ? pulsarAdmin.topics().getInternalStats(topic + "-partition-0") + : pulsarAdmin.topics().getInternalStats(topic); + Map internal = new HashMap<>(); + internal.put("numberOfEntries", internalStats.numberOfEntries); + internal.put("totalSize", internalStats.totalSize); + internal.put("currentLedgerEntries", internalStats.currentLedgerEntries); + internal.put("currentLedgerSize", internalStats.currentLedgerSize); + internal.put( + "ledgerCount", + internalStats.ledgers == null ? 0 : internalStats.ledgers.size()); + internal.put( + "cursorCount", + internalStats.cursors == null ? 0 : internalStats.cursors.size()); + result.put("internalStats", internal); + } catch (Exception e) { + result.put("internalStatsError", e.getMessage()); + } + } - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); } catch (Exception e) { - return createErrorResult("Failed to monitor topic performance: " + e.getMessage()); + result.put("statsError", e.getMessage()); + } + + String topicHealth = "healthy"; + if (result.containsKey("statsError")) { + topicHealth = "error"; + } else { + Long totalBacklogVal = (Long) result.get("totalBacklog"); + Double msgRateInVal = (Double) result.get("msgRateIn"); + Double msgRateOutVal = (Double) result.get("msgRateOut"); + if (totalBacklogVal != null && totalBacklogVal > 100_000) { + topicHealth = "backlog_high"; + } else if (msgRateInVal != null + && msgRateOutVal != null + && msgRateInVal > msgRateOutVal * 1.5) { + topicHealth = "consumption_slow"; + } } + result.put("topicHealth", topicHealth); + + addTopicBreakdown(result, topic); + return createSuccessResult("Topic performance monitoring completed", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + return createErrorResult( + "Failed to monitor topic performance: " + e.getMessage()); + } }) - .build()); - } - - private void registerMonitorSubscriptionPerformance(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "monitor-subscription-performance", - "Monitor performance metrics of a subscription on a topic", - """ + .build()); + } + + private void registerMonitorSubscriptionPerformance(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "monitor-subscription-performance", + "Monitor performance metrics of a subscription on a topic", + """ { "type": "object", "properties": { @@ -390,89 +412,98 @@ private void registerMonitorSubscriptionPerformance(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getRequiredStringParam(request.arguments(), "subscriptionName"); - - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - - long msgBacklog = 0L; - double msgRateOut = 0.0, msgThroughputOut = 0.0; - int consumersCount = 0; - boolean blockedOnUnacked = false; - Object subscriptionType = null; - - if (meta != null && meta.partitions > 0) { - for (int i = 0; i < meta.partitions; i++) { - var stats = pulsarAdmin.topics().getStats(topic + "-partition-" + i); - var sub = (stats.getSubscriptions() == null) - ? null : stats.getSubscriptions().get(subscriptionName); - if (sub == null) { - continue; - } - - msgBacklog += sub.getMsgBacklog(); - msgRateOut += sub.getMsgRateOut(); - msgThroughputOut += sub.getMsgThroughputOut(); - consumersCount += (sub.getConsumers() == null ? 0 : sub.getConsumers().size()); - blockedOnUnacked = blockedOnUnacked || sub.isBlockedSubscriptionOnUnackedMsgs(); - if (subscriptionType == null) { - subscriptionType = sub.getType(); - } - } - if (msgBacklog == 0 && consumersCount == 0 && subscriptionType == null) { - return createErrorResult("Subscription not found: " + subscriptionName); - } - } else { - TopicStats stats = pulsarAdmin.topics().getStats(topic); - var subscriptions = stats.getSubscriptions(); - if (subscriptions == null || !subscriptions.containsKey(subscriptionName)) { - return createErrorResult("Subscription not found: " + subscriptionName); - } - var sub = subscriptions.get(subscriptionName); - msgBacklog = sub.getMsgBacklog(); - msgRateOut = sub.getMsgRateOut(); - msgThroughputOut = sub.getMsgThroughputOut(); - consumersCount = (sub.getConsumers() == null ? 0 : sub.getConsumers().size()); - blockedOnUnacked = sub.isBlockedSubscriptionOnUnackedMsgs(); - subscriptionType = sub.getType(); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getRequiredStringParam(request.arguments(), "subscriptionName"); + + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + + long msgBacklog = 0L; + double msgRateOut = 0.0, msgThroughputOut = 0.0; + int consumersCount = 0; + boolean blockedOnUnacked = false; + Object subscriptionType = null; + + if (meta != null && meta.partitions > 0) { + for (int i = 0; i < meta.partitions; i++) { + var stats = pulsarAdmin.topics().getStats(topic + "-partition-" + i); + var sub = + (stats.getSubscriptions() == null) + ? null + : stats.getSubscriptions().get(subscriptionName); + if (sub == null) { + continue; } - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscriptionName); - result.put("timestamp", System.currentTimeMillis()); - result.put("msgBacklog", msgBacklog); - result.put("msgRateOut", msgRateOut); - result.put("msgThroughputOut", msgThroughputOut); - result.put("consumersCount", consumersCount); - result.put("blockedSubscriptionOnUnackedMsgs", blockedOnUnacked); - result.put("subscriptionType", subscriptionType); - - addTopicBreakdown(result, topic); - return createSuccessResult("Subscription performance retrieved", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to monitor subscription performance", e); - return createErrorResult("Failed to monitor subscription performance: " + e.getMessage()); + msgBacklog += sub.getMsgBacklog(); + msgRateOut += sub.getMsgRateOut(); + msgThroughputOut += sub.getMsgThroughputOut(); + consumersCount += + (sub.getConsumers() == null ? 0 : sub.getConsumers().size()); + blockedOnUnacked = + blockedOnUnacked || sub.isBlockedSubscriptionOnUnackedMsgs(); + if (subscriptionType == null) { + subscriptionType = sub.getType(); + } + } + if (msgBacklog == 0 && consumersCount == 0 && subscriptionType == null) { + return createErrorResult("Subscription not found: " + subscriptionName); + } + } else { + TopicStats stats = pulsarAdmin.topics().getStats(topic); + var subscriptions = stats.getSubscriptions(); + if (subscriptions == null || !subscriptions.containsKey(subscriptionName)) { + return createErrorResult("Subscription not found: " + subscriptionName); + } + var sub = subscriptions.get(subscriptionName); + msgBacklog = sub.getMsgBacklog(); + msgRateOut = sub.getMsgRateOut(); + msgThroughputOut = sub.getMsgThroughputOut(); + consumersCount = (sub.getConsumers() == null ? 0 : sub.getConsumers().size()); + blockedOnUnacked = sub.isBlockedSubscriptionOnUnackedMsgs(); + subscriptionType = sub.getType(); } - }).build()); - } - - private void registerBacklogAnalysis(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "backlog-analysis", - "Analyze message backlog within a Pulsar namespace " - + "and report topics/subscriptions exceeding a given threshold", - """ + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("timestamp", System.currentTimeMillis()); + result.put("msgBacklog", msgBacklog); + result.put("msgRateOut", msgRateOut); + result.put("msgThroughputOut", msgThroughputOut); + result.put("consumersCount", consumersCount); + result.put("blockedSubscriptionOnUnackedMsgs", blockedOnUnacked); + result.put("subscriptionType", subscriptionType); + + addTopicBreakdown(result, topic); + return createSuccessResult("Subscription performance retrieved", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to monitor subscription performance", e); + return createErrorResult( + "Failed to monitor subscription performance: " + e.getMessage()); + } + }) + .build()); + } + + private void registerBacklogAnalysis(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "backlog-analysis", + "Analyze message backlog within a Pulsar namespace " + + "and report topics/subscriptions exceeding a given threshold", + """ { "type": "object", "properties": { @@ -494,77 +525,86 @@ private void registerBacklogAnalysis(McpSyncServer mcpServer) { }, "required": ["namespace"] } - """ - ); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String namespace = getStringParam(request.arguments(), "namespace"); + int threshold = getIntParam(request.arguments(), "threshold", 1000); + boolean includeDetails = + getBooleanParam(request.arguments(), "includeDetails", false); - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String namespace = getStringParam(request.arguments(), "namespace"); - int threshold = getIntParam(request.arguments(), "threshold", 1000); - boolean includeDetails = getBooleanParam(request.arguments(), "includeDetails", false); - - Map result = new HashMap<>(); - result.put("timestamp", System.currentTimeMillis()); - result.put("namespace", namespace); - result.put("threshold", threshold); - - var topics = pulsarAdmin.namespaces().getTopics(namespace); - List alertTopics = new ArrayList<>(); - Map details = new HashMap<>(); - - for (String topic : topics) { - try { - var stats = pulsarAdmin.topics().getStats(topic); - long totalBacklog = stats.getSubscriptions().values() - .stream() - .mapToLong(sub -> sub.getMsgBacklog()) - .sum(); - - if (totalBacklog > threshold) { - alertTopics.add(topic); - } - - if (includeDetails) { - Map subsDetail = new HashMap<>(); - stats.getSubscriptions().forEach((subName, subStats) -> { - subsDetail.put(subName, Map.of( - "backlogMessages", subStats.getMsgBacklog(), - "isHealthy", subStats.getMsgBacklog() <= threshold - )); - }); - details.put(topic, Map.of( - "totalBacklog", totalBacklog, - "subscriptions", subsDetail - )); - } - } catch (Exception e) { - details.put(topic, Map.of( - "error", e.getMessage() - )); - } + Map result = new HashMap<>(); + result.put("timestamp", System.currentTimeMillis()); + result.put("namespace", namespace); + result.put("threshold", threshold); + + var topics = pulsarAdmin.namespaces().getTopics(namespace); + List alertTopics = new ArrayList<>(); + Map details = new HashMap<>(); + + for (String topic : topics) { + try { + var stats = pulsarAdmin.topics().getStats(topic); + long totalBacklog = + stats.getSubscriptions().values().stream() + .mapToLong(sub -> sub.getMsgBacklog()) + .sum(); + + if (totalBacklog > threshold) { + alertTopics.add(topic); } - result.put("alertTopics", alertTopics); if (includeDetails) { - result.put("details", details); + Map subsDetail = new HashMap<>(); + stats + .getSubscriptions() + .forEach( + (subName, subStats) -> { + subsDetail.put( + subName, + Map.of( + "backlogMessages", + subStats.getMsgBacklog(), + "isHealthy", + subStats.getMsgBacklog() <= threshold)); + }); + details.put( + topic, + Map.of( + "totalBacklog", totalBacklog, + "subscriptions", subsDetail)); } + } catch (Exception e) { + details.put(topic, Map.of("error", e.getMessage())); + } + } - return createSuccessResult("Backlog analysis completed", result); - - } catch (Exception e) { - return createErrorResult("Failed to perform backlog analysis: " + e.getMessage()); + result.put("alertTopics", alertTopics); + if (includeDetails) { + result.put("details", details); } + + return createSuccessResult("Backlog analysis completed", result); + + } catch (Exception e) { + return createErrorResult( + "Failed to perform backlog analysis: " + e.getMessage()); + } }) - .build()); - } - - private void registerConnectionDiagnostics(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "connection-diagnostics", - "Run connection diagnostics to Pulsar cluster with different test depths", - """ + .build()); + } + + private void registerConnectionDiagnostics(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "connection-diagnostics", + "Run connection diagnostics to Pulsar cluster with different test depths", + """ { "type": "object", "properties": { @@ -580,136 +620,149 @@ private void registerConnectionDiagnostics(McpSyncServer mcpServer) { }, "required": ["testType"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - Map result = new LinkedHashMap<>(); - String testType = getRequiredStringParam(request.arguments(), "testType").toLowerCase(); - String testTopic = getStringParam(request.arguments(), "testTopic"); - if (testTopic == null || testTopic.isEmpty()) { - testTopic = "persistent://public/default/connection-test"; - } - + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + Map result = new LinkedHashMap<>(); + String testType = + getRequiredStringParam(request.arguments(), "testType").toLowerCase(); + String testTopic = getStringParam(request.arguments(), "testTopic"); + if (testTopic == null || testTopic.isEmpty()) { + testTopic = "persistent://public/default/connection-test"; + } + + try { try { - try { - String leaderBroker = String.valueOf(pulsarAdmin.brokers().getLeaderBroker()); - result.put("adminApiReachable", true); - result.put("leaderBroker", leaderBroker); - } catch (Exception e) { - result.put("adminApiReachable", false); - result.put("adminApiError", e.getMessage()); - return createErrorResult("Admin API unreachable"); - } + String leaderBroker = String.valueOf(pulsarAdmin.brokers().getLeaderBroker()); + result.put("adminApiReachable", true); + result.put("leaderBroker", leaderBroker); + } catch (Exception e) { + result.put("adminApiReachable", false); + result.put("adminApiError", e.getMessage()); + return createErrorResult("Admin API unreachable"); + } - if ("basic".equals(testType)) { - result.put("diagnosticsLevel", "basic"); - return createSuccessResult("Basic connection check completed", result); - } - String subName = "connection-diagnostics-sub-" + System.currentTimeMillis(); - String sentPayload = "connection-test-" + System.currentTimeMillis(); + if ("basic".equals(testType)) { + result.put("diagnosticsLevel", "basic"); + return createSuccessResult("Basic connection check completed", result); + } + String subName = "connection-diagnostics-sub-" + System.currentTimeMillis(); + String sentPayload = "connection-test-" + System.currentTimeMillis(); - long sendStartNs; - long sendEndNs; - long receiveEndNs = 0L; + long sendStartNs; + long sendEndNs; + long receiveEndNs = 0L; - try (Consumer consumer = pulsarClient.newConsumer() + try (Consumer consumer = + pulsarClient + .newConsumer() .topic(testTopic) .subscriptionName(subName) .subscriptionType(SubscriptionType.Exclusive) .subscribe(); - Producer producer = pulsarClient.newProducer() - .topic(testTopic) - .enableBatching(false) - .sendTimeout(5, TimeUnit.SECONDS) - .create()) { - - sendStartNs = System.nanoTime(); - MessageId msgId = producer.newMessage() - .value(sentPayload.getBytes(StandardCharsets.UTF_8)) - .send(); - sendEndNs = System.nanoTime(); - result.put("clientProducerReachable", true); - result.put("testMessageId", msgId.toString()); - - Message msg = consumer.receive(5, TimeUnit.SECONDS); - if (msg != null) { - String received = new String(msg.getData(), StandardCharsets.UTF_8); - if (sentPayload.equals(received)) { - result.put("clientConsumerReachable", true); - result.put("receivedTestMessage", received); - } else { - result.put("clientConsumerReachable", false); - result.put("consumerError", "Received unexpected payload"); - } - consumer.acknowledge(msg); - receiveEndNs = System.nanoTime(); - } else { - result.put("clientConsumerReachable", false); - result.put("consumerError", "No message received in time"); - } - } catch (Exception e) { - result.put("clientProducerReachable", false); - result.put("clientProducerError", e.getMessage()); - return createErrorResult("Producer/Consumer test failed"); - } - - result.put("producerTimeMs", (sendEndNs - sendStartNs) / 1_000_000.0); - - if ("detailed".equals(testType)) { - result.put("diagnosticsLevel", "detailed"); - return createSuccessResult("Detailed connection check completed", result); + Producer producer = + pulsarClient + .newProducer() + .topic(testTopic) + .enableBatching(false) + .sendTimeout(5, TimeUnit.SECONDS) + .create()) { + + sendStartNs = System.nanoTime(); + MessageId msgId = + producer + .newMessage() + .value(sentPayload.getBytes(StandardCharsets.UTF_8)) + .send(); + sendEndNs = System.nanoTime(); + result.put("clientProducerReachable", true); + result.put("testMessageId", msgId.toString()); + + Message msg = consumer.receive(5, TimeUnit.SECONDS); + if (msg != null) { + String received = new String(msg.getData(), StandardCharsets.UTF_8); + if (sentPayload.equals(received)) { + result.put("clientConsumerReachable", true); + result.put("receivedTestMessage", received); + } else { + result.put("clientConsumerReachable", false); + result.put("consumerError", "Received unexpected payload"); } + consumer.acknowledge(msg); + receiveEndNs = System.nanoTime(); + } else { + result.put("clientConsumerReachable", false); + result.put("consumerError", "No message received in time"); + } + } catch (Exception e) { + result.put("clientProducerReachable", false); + result.put("clientProducerError", e.getMessage()); + return createErrorResult("Producer/Consumer test failed"); + } - if ("network".equals(testType) && Boolean.TRUE.equals(result.get("clientConsumerReachable"))) { - double roundTripMs = (receiveEndNs - sendStartNs) / 1_000_000.0; - result.put("roundTripLatencyMs", roundTripMs); - - int testSize = 1024 * 100; - byte[] payload = new byte[testSize]; - Arrays.fill(payload, (byte) 65); - long totalBytes = 0; - long bwStart = System.nanoTime(); - - try (Producer producer = pulsarClient.newProducer() - .topic(testTopic) - .enableBatching(false) - .sendTimeout(5, TimeUnit.SECONDS) - .create()) { - for (int i = 0; i < 5; i++) { - producer.newMessage().value(payload).send(); - totalBytes += testSize; - } - } + result.put("producerTimeMs", (sendEndNs - sendStartNs) / 1_000_000.0); - long bwEnd = System.nanoTime(); - double seconds = (bwEnd - bwStart) / 1_000_000_000.0; - double mbps = (totalBytes / 1024.0 / 1024.0) / seconds; + if ("detailed".equals(testType)) { + result.put("diagnosticsLevel", "detailed"); + return createSuccessResult("Detailed connection check completed", result); + } - result.put("bandwidthMBps", mbps); + if ("network".equals(testType) + && Boolean.TRUE.equals(result.get("clientConsumerReachable"))) { + double roundTripMs = (receiveEndNs - sendStartNs) / 1_000_000.0; + result.put("roundTripLatencyMs", roundTripMs); + + int testSize = 1024 * 100; + byte[] payload = new byte[testSize]; + Arrays.fill(payload, (byte) 65); + long totalBytes = 0; + long bwStart = System.nanoTime(); + + try (Producer producer = + pulsarClient + .newProducer() + .topic(testTopic) + .enableBatching(false) + .sendTimeout(5, TimeUnit.SECONDS) + .create()) { + for (int i = 0; i < 5; i++) { + producer.newMessage().value(payload).send(); + totalBytes += testSize; } + } - result.put("diagnosticsLevel", testType); - return createSuccessResult("Connection diagnostics (" + testType + ") completed", result); + long bwEnd = System.nanoTime(); + double seconds = (bwEnd - bwStart) / 1_000_000_000.0; + double mbps = (totalBytes / 1024.0 / 1024.0) / seconds; - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Error in connection diagnostics", e); - result.put("diagnosticsLevel", testType); - return createErrorResult("Diagnostics failed: " + e.getMessage()); + result.put("bandwidthMBps", mbps); } - }).build() - ); - } - - private void registerHealthCheck(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "health-check", - "Check Pulsar cluster, topic, and subscription health status", - """ + + result.put("diagnosticsLevel", testType); + return createSuccessResult( + "Connection diagnostics (" + testType + ") completed", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Error in connection diagnostics", e); + result.put("diagnosticsLevel", testType); + return createErrorResult("Diagnostics failed: " + e.getMessage()); + } + }) + .build()); + } + + private void registerHealthCheck(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "health-check", + "Check Pulsar cluster, topic, and subscription health status", + """ { "type": "object", "properties": { @@ -723,79 +776,85 @@ private void registerHealthCheck(McpSyncServer mcpServer) { } } } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - Map result = new HashMap<>(); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + Map result = new HashMap<>(); + try { try { - try { - pulsarAdmin.brokers().getLeaderBroker(); - result.put("brokerHealthy", true); - } catch (Exception e) { - result.put("brokerHealthy", false); - return createErrorResult("Broker is not reachable: " + e.getMessage()); - } - - String topic = getStringParam(request.arguments(), "topic"); - String subscriptionName = getStringParam(request.arguments(), "subscriptionName"); - - if (topic != null && !topic.isEmpty()) { - topic = buildFullTopicName(request.arguments()); - TopicStats stats = pulsarAdmin.topics().getStats(topic); - - double throughputMBps = (stats.getMsgThroughputIn() - + stats.getMsgThroughputOut()) / (1024.0 * 1024.0); - double messagesPerSecond = (stats.getMsgRateIn() + stats.getMsgRateOut()); - - result.put("topic", topic); - result.put("throughputMBps", throughputMBps); - result.put("messagesPerSecond", messagesPerSecond); - - long backlog = stats.getSubscriptions().values().stream() - .mapToLong(sub -> sub.getMsgBacklog()) - .sum(); - result.put("backlog", backlog); - - String backlogLevel; - if (backlog == 0) { - backlogLevel = "EMPTY"; - } else if (backlog < 1000) { - backlogLevel = "LOW"; - } else if (backlog < 100000) { - backlogLevel = "MEDIUM"; - } else { - backlogLevel = "HIGH"; - } - result.put("backlogLevel", backlogLevel); - - if (subscriptionName != null && stats.getSubscriptions().containsKey(subscriptionName)) { - SubscriptionStats subStats = stats.getSubscriptions().get(subscriptionName); - result.put("subscriptionName", subscriptionName); - result.put("subscriptionBacklog", subStats.getMsgBacklog()); - result.put("subscriptionMsgRateOut", subStats.getMsgRateOut()); - result.put("subscriptionMsgRateRedeliver", subStats.getMsgRateRedeliver()); - } - - boolean isHealthy = result.get("brokerHealthy").equals(true) - && throughputMBps > 0 - && !"HIGH".equals(backlogLevel); - result.put("isHealthy", isHealthy); + pulsarAdmin.brokers().getLeaderBroker(); + result.put("brokerHealthy", true); + } catch (Exception e) { + result.put("brokerHealthy", false); + return createErrorResult("Broker is not reachable: " + e.getMessage()); + } - addTopicBreakdown(result, topic); - } + String topic = getStringParam(request.arguments(), "topic"); + String subscriptionName = + getStringParam(request.arguments(), "subscriptionName"); + + if (topic != null && !topic.isEmpty()) { + topic = buildFullTopicName(request.arguments()); + TopicStats stats = pulsarAdmin.topics().getStats(topic); + + double throughputMBps = + (stats.getMsgThroughputIn() + stats.getMsgThroughputOut()) + / (1024.0 * 1024.0); + double messagesPerSecond = (stats.getMsgRateIn() + stats.getMsgRateOut()); + + result.put("topic", topic); + result.put("throughputMBps", throughputMBps); + result.put("messagesPerSecond", messagesPerSecond); + + long backlog = + stats.getSubscriptions().values().stream() + .mapToLong(sub -> sub.getMsgBacklog()) + .sum(); + result.put("backlog", backlog); + + String backlogLevel; + if (backlog == 0) { + backlogLevel = "EMPTY"; + } else if (backlog < 1000) { + backlogLevel = "LOW"; + } else if (backlog < 100000) { + backlogLevel = "MEDIUM"; + } else { + backlogLevel = "HIGH"; + } + result.put("backlogLevel", backlogLevel); + + if (subscriptionName != null + && stats.getSubscriptions().containsKey(subscriptionName)) { + SubscriptionStats subStats = stats.getSubscriptions().get(subscriptionName); + result.put("subscriptionName", subscriptionName); + result.put("subscriptionBacklog", subStats.getMsgBacklog()); + result.put("subscriptionMsgRateOut", subStats.getMsgRateOut()); + result.put("subscriptionMsgRateRedeliver", subStats.getMsgRateRedeliver()); + } + + boolean isHealthy = + result.get("brokerHealthy").equals(true) + && throughputMBps > 0 + && !"HIGH".equals(backlogLevel); + result.put("isHealthy", isHealthy); + + addTopicBreakdown(result, topic); + } - return createSuccessResult("Health check completed", result); + return createSuccessResult("Health check completed", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Unexpected error in health check", e); - return createErrorResult("Unexpected error: " + e.getMessage()); - } - }).build() - ); - } + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error in health check", e); + return createErrorResult("Unexpected error: " + e.getMessage()); + } + }) + .build()); + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/NamespaceTools.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/NamespaceTools.java index b8ee7e3..040737a 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/NamespaceTools.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/NamespaceTools.java @@ -26,28 +26,29 @@ public class NamespaceTools extends BasePulsarTools { - public NamespaceTools(PulsarAdmin pulsarAdmin) { - super(pulsarAdmin); - } - - public void registerTools(McpSyncServer mcpServer) { - registerListNamespaces(mcpServer); - registerGetNamespaceInfo(mcpServer); - registerCreateNamespace(mcpServer); - registerDeleteNamespace(mcpServer); - registerSetRetentionPolicy(mcpServer); - registerGetRetentionPolicy(mcpServer); - registerSetBacklogQuota(mcpServer); - registerGetBacklogQuota(mcpServer); - registerClearNamespaceBacklog(mcpServer); - registerNamespaceStats(mcpServer); - } - - private void registerListNamespaces(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "list-namespaces", - "List all namespaces under a given tenant", - """ + public NamespaceTools(PulsarAdmin pulsarAdmin) { + super(pulsarAdmin); + } + + public void registerTools(McpSyncServer mcpServer) { + registerListNamespaces(mcpServer); + registerGetNamespaceInfo(mcpServer); + registerCreateNamespace(mcpServer); + registerDeleteNamespace(mcpServer); + registerSetRetentionPolicy(mcpServer); + registerGetRetentionPolicy(mcpServer); + registerSetBacklogQuota(mcpServer); + registerGetBacklogQuota(mcpServer); + registerClearNamespaceBacklog(mcpServer); + registerNamespaceStats(mcpServer); + } + + private void registerListNamespaces(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "list-namespaces", + "List all namespaces under a given tenant", + """ { "type": "object", "properties": { @@ -58,59 +59,62 @@ private void registerListNamespaces(McpSyncServer mcpServer) { }, "required": [] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String tenant = getStringParam(request.arguments(), "tenant"); - if (tenant != null) { - tenant = tenant.trim(); - } - List namespaces; - if (tenant != null && !tenant.trim().isEmpty()) { - namespaces = pulsarAdmin.namespaces().getNamespaces(tenant); - if (namespaces == null) { - namespaces = List.of(); - } - } else { - List tenants = pulsarAdmin.tenants().getTenants(); - if (tenants == null) { - tenants = List.of(); - } - namespaces = new ArrayList<>(); - for (String namespace : tenants) { - try { - namespaces.addAll(pulsarAdmin.namespaces().getNamespaces(namespace)); - } catch (Exception e) { - LOGGER.warn("Failed to get namespaces for tenant " + namespace, e); - } - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String tenant = getStringParam(request.arguments(), "tenant"); + if (tenant != null) { + tenant = tenant.trim(); + } + List namespaces; + if (tenant != null && !tenant.trim().isEmpty()) { + namespaces = pulsarAdmin.namespaces().getNamespaces(tenant); + if (namespaces == null) { + namespaces = List.of(); + } + } else { + List tenants = pulsarAdmin.tenants().getTenants(); + if (tenants == null) { + tenants = List.of(); + } + namespaces = new ArrayList<>(); + for (String namespace : tenants) { + try { + namespaces.addAll(pulsarAdmin.namespaces().getNamespaces(namespace)); + } catch (Exception e) { + LOGGER.warn("Failed to get namespaces for tenant " + namespace, e); } - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("namespaces", namespaces); - result.put("count", namespaces.size()); - - return createSuccessResult("Namespaces retrieved successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to list namespaces", e); - return createErrorResult("Failed to list namespaces: " + e.getMessage()); + } } - }).build()); - } - - private void registerGetNamespaceInfo(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-namespace-info", - "Get detailed info of a namespace under a tenant", - """ + + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("namespaces", namespaces); + result.put("count", namespaces.size()); + + return createSuccessResult("Namespaces retrieved successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to list namespaces", e); + return createErrorResult("Failed to list namespaces: " + e.getMessage()); + } + }) + .build()); + } + + private void registerGetNamespaceInfo(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-namespace-info", + "Get detailed info of a namespace under a tenant", + """ { "type": "object", "properties": { @@ -127,45 +131,51 @@ private void registerGetNamespaceInfo(McpSyncServer mcpServer) { }, "required": ["tenant", "namespace"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String fullNamespace = resolveNamespace(request.arguments()); - - Map details = new HashMap<>(); - details.put("policies", pulsarAdmin.namespaces().getPolicies(fullNamespace)); - details.put("backlogQuotaMap", pulsarAdmin.namespaces().getBacklogQuotaMap(fullNamespace)); - details.put("retention", pulsarAdmin.namespaces().getRetention(fullNamespace)); - details.put("persistence", pulsarAdmin.namespaces().getPersistence(fullNamespace)); - details.put("maxConsumersPerSubscription", - pulsarAdmin.namespaces(). - getMaxConsumersPerSubscription(fullNamespace)); - details.put("maxConsumersPerTopic", - pulsarAdmin.namespaces(). - getMaxConsumersPerTopic(fullNamespace)); - details.put("maxProducersPerTopic", - pulsarAdmin.namespaces(). - getMaxProducersPerTopic(fullNamespace)); - - return createSuccessResult("Namespace info retrieved successfully", details); - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get namespace info", e); - return createErrorResult("Failed to get namespace info: " + e.getMessage()); - } - }).build()); - } - - private void registerCreateNamespace(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "create-namespace", - "Create a new namespace under a given tenant", - """ + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String fullNamespace = resolveNamespace(request.arguments()); + + Map details = new HashMap<>(); + details.put("policies", pulsarAdmin.namespaces().getPolicies(fullNamespace)); + details.put( + "backlogQuotaMap", + pulsarAdmin.namespaces().getBacklogQuotaMap(fullNamespace)); + details.put("retention", pulsarAdmin.namespaces().getRetention(fullNamespace)); + details.put( + "persistence", pulsarAdmin.namespaces().getPersistence(fullNamespace)); + details.put( + "maxConsumersPerSubscription", + pulsarAdmin.namespaces().getMaxConsumersPerSubscription(fullNamespace)); + details.put( + "maxConsumersPerTopic", + pulsarAdmin.namespaces().getMaxConsumersPerTopic(fullNamespace)); + details.put( + "maxProducersPerTopic", + pulsarAdmin.namespaces().getMaxProducersPerTopic(fullNamespace)); + + return createSuccessResult("Namespace info retrieved successfully", details); + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get namespace info", e); + return createErrorResult("Failed to get namespace info: " + e.getMessage()); + } + }) + .build()); + } + + private void registerCreateNamespace(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "create-namespace", + "Create a new namespace under a given tenant", + """ { "type": "object", "properties": { @@ -182,42 +192,45 @@ private void registerCreateNamespace(McpSyncServer mcpServer) { }, "required": ["namespace"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String fullNamespace = resolveNamespace(request.arguments()); - - pulsarAdmin.namespaces().createNamespace(fullNamespace); - - String[] parts = fullNamespace.split("/"); - String tenant = parts[0]; - String namespace = parts[1]; - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("namespace", namespace); - result.put("created", true); - - return createSuccessResult("Namespace created successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to create namespace", e); - return createErrorResult("Failed to create namespace: " + e.getMessage()); - } - }).build()); - } - - private void registerDeleteNamespace(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "delete-namespace", - "Delete a namespace under a given tenant", - """ + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String fullNamespace = resolveNamespace(request.arguments()); + + pulsarAdmin.namespaces().createNamespace(fullNamespace); + + String[] parts = fullNamespace.split("/"); + String tenant = parts[0]; + String namespace = parts[1]; + + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("namespace", namespace); + result.put("created", true); + + return createSuccessResult("Namespace created successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to create namespace", e); + return createErrorResult("Failed to create namespace: " + e.getMessage()); + } + }) + .build()); + } + + private void registerDeleteNamespace(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "delete-namespace", + "Delete a namespace under a given tenant", + """ { "type": "object", "properties": { @@ -239,48 +252,51 @@ private void registerDeleteNamespace(McpSyncServer mcpServer) { }, "required": ["namespace"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String fullNamespace = resolveNamespace(request.arguments()); - boolean force = getBooleanParam(request.arguments(), "force", false); - - String[] parts = fullNamespace.split("/"); - String tenant = parts.length > 0 ? parts[0] : ""; - String namespace = parts.length > 1 ? parts[1] : ""; - - if (isSystemNamespace(namespace)) { - return createErrorResult("Cannot delete system namespace: " + namespace); - } - - pulsarAdmin.namespaces().deleteNamespace(fullNamespace, force); - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("namespace", namespace); - result.put("force", force); - result.put("deleted", true); - - return createSuccessResult("Namespace deleted successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to delete namespace", e); - return createErrorResult("Failed to delete namespace: " + e.getMessage()); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String fullNamespace = resolveNamespace(request.arguments()); + boolean force = getBooleanParam(request.arguments(), "force", false); + + String[] parts = fullNamespace.split("/"); + String tenant = parts.length > 0 ? parts[0] : ""; + String namespace = parts.length > 1 ? parts[1] : ""; + + if (isSystemNamespace(namespace)) { + return createErrorResult("Cannot delete system namespace: " + namespace); } - }).build()); - } - - private void registerSetRetentionPolicy(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "set-retention-policy", - "Set the retention policy for a specific namespace", - """ + + pulsarAdmin.namespaces().deleteNamespace(fullNamespace, force); + + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("namespace", namespace); + result.put("force", force); + result.put("deleted", true); + + return createSuccessResult("Namespace deleted successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to delete namespace", e); + return createErrorResult("Failed to delete namespace: " + e.getMessage()); + } + }) + .build()); + } + + private void registerSetRetentionPolicy(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "set-retention-policy", + "Set the retention policy for a specific namespace", + """ { "type": "object", "properties": { @@ -307,53 +323,59 @@ private void registerSetRetentionPolicy(McpSyncServer mcpServer) { }, "required": ["tenant", "namespace"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String fullNamespace = resolveNamespace(request.arguments()); - - String[] parts = fullNamespace.split("/", 2); - String tenant = parts.length > 0 ? parts[0] : ""; - String namespace = parts.length > 1 ? parts[1] : ""; - - Integer retentionTime = getIntParam(request.arguments(), "retentionTimeInMinutes", -1); - Integer retentionSize = getIntParam(request.arguments(), "retentionSizeInMB", -1); - if (retentionTime == null) { - retentionTime = -1; - } - if (retentionSize == null) { - retentionSize = -1; - } - - RetentionPolicies policies = new RetentionPolicies(retentionTime, retentionSize); - pulsarAdmin.namespaces().setRetention(fullNamespace, policies); - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("namespace", namespace); - result.put("retentionTime", retentionTime); - result.put("retentionSize", retentionSize); - - return createSuccessResult("Retention policy set successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to set retention policy", e); - return createErrorResult("Failed to set retention policy: " + e.getMessage()); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String fullNamespace = resolveNamespace(request.arguments()); + + String[] parts = fullNamespace.split("/", 2); + String tenant = parts.length > 0 ? parts[0] : ""; + String namespace = parts.length > 1 ? parts[1] : ""; + + Integer retentionTime = + getIntParam(request.arguments(), "retentionTimeInMinutes", -1); + Integer retentionSize = + getIntParam(request.arguments(), "retentionSizeInMB", -1); + if (retentionTime == null) { + retentionTime = -1; } - }).build()); - } - - private void registerGetRetentionPolicy(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-retention-policy", - "Get the retention policy for a specific namespace", - """ + if (retentionSize == null) { + retentionSize = -1; + } + + RetentionPolicies policies = + new RetentionPolicies(retentionTime, retentionSize); + pulsarAdmin.namespaces().setRetention(fullNamespace, policies); + + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("namespace", namespace); + result.put("retentionTime", retentionTime); + result.put("retentionSize", retentionSize); + + return createSuccessResult("Retention policy set successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to set retention policy", e); + return createErrorResult("Failed to set retention policy: " + e.getMessage()); + } + }) + .build()); + } + + private void registerGetRetentionPolicy(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-retention-policy", + "Get the retention policy for a specific namespace", + """ { "type": "object", "properties": { @@ -369,47 +391,51 @@ private void registerGetRetentionPolicy(McpSyncServer mcpServer) { }, "required": ["tenant", "namespace"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String fullNamespace = resolveNamespace(request.arguments()); - - String[] parts = fullNamespace.split("/", 2); - String tenant = parts.length > 0 ? parts[0] : ""; - String namespace = parts.length > 1 ? parts[1] : ""; - - RetentionPolicies policies = pulsarAdmin.namespaces().getRetention(fullNamespace); - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("namespace", namespace); - - if (policies != null) { - result.put("retentionTimeInMinutes", policies.getRetentionTimeInMinutes()); - result.put("retentionSizeInMB", policies.getRetentionSizeInMB()); - } else { - result.put("message", "No retention policy configured for this namespace."); - } - - return createSuccessResult("Retention policy fetched successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get retention policy", e); - return createErrorResult("Failed to get retention policy: " + e.getMessage()); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String fullNamespace = resolveNamespace(request.arguments()); + + String[] parts = fullNamespace.split("/", 2); + String tenant = parts.length > 0 ? parts[0] : ""; + String namespace = parts.length > 1 ? parts[1] : ""; + + RetentionPolicies policies = + pulsarAdmin.namespaces().getRetention(fullNamespace); + + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("namespace", namespace); + + if (policies != null) { + result.put("retentionTimeInMinutes", policies.getRetentionTimeInMinutes()); + result.put("retentionSizeInMB", policies.getRetentionSizeInMB()); + } else { + result.put("message", "No retention policy configured for this namespace."); } - }).build()); - } - - private void registerSetBacklogQuota(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "set-backlog-quota", - "Set backlog quota for a specific namespace", - """ + + return createSuccessResult("Retention policy fetched successfully", result); + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get retention policy", e); + return createErrorResult("Failed to get retention policy: " + e.getMessage()); + } + }) + .build()); + } + + private void registerSetBacklogQuota(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "set-backlog-quota", + "Set backlog quota for a specific namespace", + """ { "type": "object", "properties": { @@ -434,73 +460,75 @@ private void registerSetBacklogQuota(McpSyncServer mcpServer) { }, "required": ["tenant", "namespace", "limitSizeInBytes", "policy"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String fullNamespace = resolveNamespace(request.arguments()); + """); - String[] parts = fullNamespace.split("/", 2); - String tenant = parts.length > 0 ? parts[0] : ""; - String namespace = parts.length > 1 ? parts[1] : ""; + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String fullNamespace = resolveNamespace(request.arguments()); - Integer limitSize = getIntParam(request.arguments(), "limitSizeInBytes", 0); - String policyStr = getRequiredStringParam(request.arguments(), "policy"); + String[] parts = fullNamespace.split("/", 2); + String tenant = parts.length > 0 ? parts[0] : ""; + String namespace = parts.length > 1 ? parts[1] : ""; - if (limitSize == null || limitSize <= 0) { - return createErrorResult("Limit size must be greater than 0."); - } + Integer limitSize = getIntParam(request.arguments(), "limitSizeInBytes", 0); + String policyStr = getRequiredStringParam(request.arguments(), "policy"); - BacklogQuota.RetentionPolicy policy; - switch (policyStr.toLowerCase()) { - case "producer_request_hold": - policy = BacklogQuota.RetentionPolicy.producer_request_hold; - break; - case "producer_exception": - policy = BacklogQuota.RetentionPolicy.producer_exception; - break; - case "consumer_backlog_eviction": - policy = BacklogQuota.RetentionPolicy.consumer_backlog_eviction; - break; - default: - return createErrorResult("Invalid policy:" - + policyStr, - List.of("Valid policies: producer_request_hold, " - + "producer_exception, consumer_backlog_eviction")); - } - - BacklogQuota quota = BacklogQuota.builder() - .limitSize(limitSize) - .retentionPolicy(policy) - .build(); - - pulsarAdmin.namespaces().setBacklogQuota(fullNamespace, quota); - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("namespace", namespace); - result.put("limitSizeInBytes", limitSize); - result.put("policy", policy.name()); - - return createSuccessResult("Backlog quota set successfully", result); + if (limitSize == null || limitSize <= 0) { + return createErrorResult("Limit size must be greater than 0."); + } - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to set backlog quota", e); - return createErrorResult("Failed to set backlog quota: " + e.getMessage()); + BacklogQuota.RetentionPolicy policy; + switch (policyStr.toLowerCase()) { + case "producer_request_hold": + policy = BacklogQuota.RetentionPolicy.producer_request_hold; + break; + case "producer_exception": + policy = BacklogQuota.RetentionPolicy.producer_exception; + break; + case "consumer_backlog_eviction": + policy = BacklogQuota.RetentionPolicy.consumer_backlog_eviction; + break; + default: + return createErrorResult( + "Invalid policy:" + policyStr, + List.of( + "Valid policies: producer_request_hold, " + + "producer_exception, consumer_backlog_eviction")); } - }).build()); - } - - private void registerGetBacklogQuota(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-backlog-quota", - "Get backlog quota for a specific namespace", - """ + + BacklogQuota quota = + BacklogQuota.builder().limitSize(limitSize).retentionPolicy(policy).build(); + + pulsarAdmin.namespaces().setBacklogQuota(fullNamespace, quota); + + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("namespace", namespace); + result.put("limitSizeInBytes", limitSize); + result.put("policy", policy.name()); + + return createSuccessResult("Backlog quota set successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to set backlog quota", e); + return createErrorResult("Failed to set backlog quota: " + e.getMessage()); + } + }) + .build()); + } + + private void registerGetBacklogQuota(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-backlog-quota", + "Get backlog quota for a specific namespace", + """ { "type": "object", "properties": { @@ -516,56 +544,60 @@ private void registerGetBacklogQuota(McpSyncServer mcpServer) { }, "required": ["tenant", "namespace"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String fullNamespace = resolveNamespace(request.arguments()); - - String[] parts = fullNamespace.split("/", 2); - String tenant = parts.length > 0 ? parts[0] : ""; - String namespace = parts.length > 1 ? parts[1] : ""; - - Map quotas = - pulsarAdmin.namespaces().getBacklogQuotaMap(fullNamespace); - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("namespace", namespace); - - if (quotas == null || quotas.isEmpty()) { - result.put("message", "No backlog quota configured"); - result.put("quotas", Map.of()); - } else { - Map quotaInfo = new HashMap<>(); - quotas.forEach((type, quota) -> { - Map info = new HashMap<>(); - info.put("limitSize", quota.getLimitSize()); - info.put("policy", quota.getPolicy().toString()); - quotaInfo.put(type.toString(), info); - }); - result.put("quotas", quotaInfo); - } - - return createSuccessResult("Backlog quota retrieved successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get backlog quota", e); - return createErrorResult("Failed to get backlog quota: " + e.getMessage()); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String fullNamespace = resolveNamespace(request.arguments()); + + String[] parts = fullNamespace.split("/", 2); + String tenant = parts.length > 0 ? parts[0] : ""; + String namespace = parts.length > 1 ? parts[1] : ""; + + Map quotas = + pulsarAdmin.namespaces().getBacklogQuotaMap(fullNamespace); + + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("namespace", namespace); + + if (quotas == null || quotas.isEmpty()) { + result.put("message", "No backlog quota configured"); + result.put("quotas", Map.of()); + } else { + Map quotaInfo = new HashMap<>(); + quotas.forEach( + (type, quota) -> { + Map info = new HashMap<>(); + info.put("limitSize", quota.getLimitSize()); + info.put("policy", quota.getPolicy().toString()); + quotaInfo.put(type.toString(), info); + }); + result.put("quotas", quotaInfo); } - }).build()); - } - - private void registerClearNamespaceBacklog(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "clear-namespace-backlog", - "Clear the backlog for a specific namespace", - """ + + return createSuccessResult("Backlog quota retrieved successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get backlog quota", e); + return createErrorResult("Failed to get backlog quota: " + e.getMessage()); + } + }) + .build()); + } + + private void registerClearNamespaceBacklog(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "clear-namespace-backlog", + "Clear the backlog for a specific namespace", + """ { "type": "object", "properties": { @@ -585,49 +617,52 @@ private void registerClearNamespaceBacklog(McpSyncServer mcpServer) { }, "required": ["tenant", "namespace"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String fullNamespace = resolveNamespace(request.arguments()); - - String[] parts = fullNamespace.split("/", 2); - String tenant = parts.length > 0 ? parts[0] : ""; - String namespace = parts.length > 1 ? parts[1] : ""; - String subscriptionName = getStringParam( - request.arguments(), - "subscriptionName"); - - if (subscriptionName != null && !subscriptionName.trim().isEmpty()) { - pulsarAdmin.namespaces() - .clearNamespaceBacklogForSubscription( - fullNamespace, subscriptionName); - } else { - pulsarAdmin.namespaces().clearNamespaceBacklog(fullNamespace); - } - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("namespace", namespace); - - return createSuccessResult("Namespace backlog cleared successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to clear namespace backlog", e); - return createErrorResult("Failed to clear namespace backlog: " + e.getMessage()); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String fullNamespace = resolveNamespace(request.arguments()); + + String[] parts = fullNamespace.split("/", 2); + String tenant = parts.length > 0 ? parts[0] : ""; + String namespace = parts.length > 1 ? parts[1] : ""; + String subscriptionName = + getStringParam(request.arguments(), "subscriptionName"); + + if (subscriptionName != null && !subscriptionName.trim().isEmpty()) { + pulsarAdmin + .namespaces() + .clearNamespaceBacklogForSubscription(fullNamespace, subscriptionName); + } else { + pulsarAdmin.namespaces().clearNamespaceBacklog(fullNamespace); } - }).build()); - } - - private void registerNamespaceStats(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-namespace-stats", - "Get statistics for a specific namespace", - """ + + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("namespace", namespace); + + return createSuccessResult("Namespace backlog cleared successfully", result); + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to clear namespace backlog", e); + return createErrorResult( + "Failed to clear namespace backlog: " + e.getMessage()); + } + }) + .build()); + } + + private void registerNamespaceStats(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-namespace-stats", + "Get statistics for a specific namespace", + """ { "type": "object", "properties": { @@ -643,50 +678,51 @@ private void registerNamespaceStats(McpSyncServer mcpServer) { }, "required": ["tenant", "namespace"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String fullNamespace = resolveNamespace(request.arguments()); - - String[] parts = fullNamespace.split("/", 2); - String tenant = parts.length > 0 ? parts[0] : ""; - String namespace = parts.length > 1 ? parts[1] : ""; - - List topics = pulsarAdmin.topics().getList(fullNamespace); - if (topics == null) { - topics = List.of(); - } - - long persistentTopics = topics.stream().filter(t -> - t.startsWith("persistent://")).count(); - long nonPersistentTopics = topics.stream().filter(t -> - t.startsWith("non-persistent://")).count(); - - Map result = new HashMap<>(); - result.put("namespace", namespace); - result.put("tenant", tenant); - result.put("persistentTopics", persistentTopics); - result.put("nonPersistentTopics", nonPersistentTopics); - result.put("topics", topics); - - return createSuccessResult("Namespace stats retrieved successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get namespace stats", e); - return createErrorResult("Failed to get namespace stats: " + e.getMessage()); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String fullNamespace = resolveNamespace(request.arguments()); + + String[] parts = fullNamespace.split("/", 2); + String tenant = parts.length > 0 ? parts[0] : ""; + String namespace = parts.length > 1 ? parts[1] : ""; + + List topics = pulsarAdmin.topics().getList(fullNamespace); + if (topics == null) { + topics = List.of(); } - }).build()); - } - - private boolean isSystemNamespace(String namespace) { - return namespace.equals("public/default") - || namespace.equals("public/functions") - || namespace.equals("public/system"); - } -} \ No newline at end of file + long persistentTopics = + topics.stream().filter(t -> t.startsWith("persistent://")).count(); + long nonPersistentTopics = + topics.stream().filter(t -> t.startsWith("non-persistent://")).count(); + + Map result = new HashMap<>(); + result.put("namespace", namespace); + result.put("tenant", tenant); + result.put("persistentTopics", persistentTopics); + result.put("nonPersistentTopics", nonPersistentTopics); + result.put("topics", topics); + + return createSuccessResult("Namespace stats retrieved successfully", result); + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get namespace stats", e); + return createErrorResult("Failed to get namespace stats: " + e.getMessage()); + } + }) + .build()); + } + + private boolean isSystemNamespace(String namespace) { + return namespace.equals("public/default") + || namespace.equals("public/functions") + || namespace.equals("public/system"); + } +} diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/SchemaTools.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/SchemaTools.java index 163a0c9..4644f70 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/SchemaTools.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/SchemaTools.java @@ -27,26 +27,27 @@ import org.apache.pulsar.common.schema.SchemaInfo; import org.apache.pulsar.common.schema.SchemaType; -public class SchemaTools extends BasePulsarTools{ - - public SchemaTools(PulsarAdmin pulsarAdmin) { - super(pulsarAdmin); - } - - public void registerTools(McpSyncServer mcpServer) { - registerGetSchemaInfo(mcpServer); - registerGetSchemaVersion(mcpServer); - registerAllSchemaVersions(mcpServer); - registerDeleteSchema(mcpServer); - registerTestSchemaCompatibility(mcpServer); - registerUploadSchema(mcpServer); - } - - private void registerGetSchemaInfo(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-schema-info", - "Get schema info for a specific topic", - """ +public class SchemaTools extends BasePulsarTools { + + public SchemaTools(PulsarAdmin pulsarAdmin) { + super(pulsarAdmin); + } + + public void registerTools(McpSyncServer mcpServer) { + registerGetSchemaInfo(mcpServer); + registerGetSchemaVersion(mcpServer); + registerAllSchemaVersions(mcpServer); + registerDeleteSchema(mcpServer); + registerTestSchemaCompatibility(mcpServer); + registerUploadSchema(mcpServer); + } + + private void registerGetSchemaInfo(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-schema-info", + "Get schema info for a specific topic", + """ { "type": "object", "properties": { @@ -66,36 +67,40 @@ private void registerGetSchemaInfo(McpSyncServer mcpServer) { }, "required": ["tenant", "namespace", "topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - - SchemaInfo schemaInfo = pulsarAdmin.schemas().getSchemaInfo(topic); - Map result = new HashMap<>(); - result.put("schemaType", schemaInfo.getType().name()); - result.put("schema", Base64.getEncoder().encodeToString(schemaInfo.getSchema())); - result.put("properties", schemaInfo.getProperties()); - - return createSuccessResult("Schema info retrieved successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get schema info", e); - return createErrorResult("Failed to get schema info: " + e.getMessage()); - } - }).build()); - } - - private void registerAllSchemaVersions(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-schema-AllVersions", - "Get schema all versions", - """ + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + + SchemaInfo schemaInfo = pulsarAdmin.schemas().getSchemaInfo(topic); + Map result = new HashMap<>(); + result.put("schemaType", schemaInfo.getType().name()); + result.put( + "schema", Base64.getEncoder().encodeToString(schemaInfo.getSchema())); + result.put("properties", schemaInfo.getProperties()); + + return createSuccessResult("Schema info retrieved successfully", result); + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get schema info", e); + return createErrorResult("Failed to get schema info: " + e.getMessage()); + } + }) + .build()); + } + + private void registerAllSchemaVersions(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-schema-AllVersions", + "Get schema all versions", + """ { "type": "object", "properties": { @@ -106,49 +111,52 @@ private void registerAllSchemaVersions(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - - List versions = pulsarAdmin.schemas().getAllSchemas(topic); - List> versionList = new ArrayList<>(); - - for (int i = 0; i < versions.size(); i++) { - SchemaInfo schemaInfo = versions.get(i); - Map versionMap = new HashMap<>(); - versionMap.put("versionIndex", i); - versionMap.put("type", schemaInfo.getType().name()); - versionMap.put("schema", new String(schemaInfo.getSchema())); - versionMap.put("properties", schemaInfo.getProperties()); - versionList.add(versionMap); - } - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("schemaVersions", versionList); - - addTopicBreakdown(result, topic); - return createSuccessResult("Schema versions retrieved", result); - - } catch (IllegalArgumentException e) { - return createErrorResult("Schema not found for topic"); - } catch (PulsarAdminException e) { - LOGGER.error("Failed to get schema version for topic", e); - return createErrorResult("Failed to get schema version: " + e.getMessage()); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + + List versions = pulsarAdmin.schemas().getAllSchemas(topic); + List> versionList = new ArrayList<>(); + + for (int i = 0; i < versions.size(); i++) { + SchemaInfo schemaInfo = versions.get(i); + Map versionMap = new HashMap<>(); + versionMap.put("versionIndex", i); + versionMap.put("type", schemaInfo.getType().name()); + versionMap.put("schema", new String(schemaInfo.getSchema())); + versionMap.put("properties", schemaInfo.getProperties()); + versionList.add(versionMap); } - }).build()); - } - - private void registerGetSchemaVersion(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-schema-version", - "Get a specific schema version of a topic", - """ + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("schemaVersions", versionList); + + addTopicBreakdown(result, topic); + return createSuccessResult("Schema versions retrieved", result); + + } catch (IllegalArgumentException e) { + return createErrorResult("Schema not found for topic"); + } catch (PulsarAdminException e) { + LOGGER.error("Failed to get schema version for topic", e); + return createErrorResult("Failed to get schema version: " + e.getMessage()); + } + }) + .build()); + } + + private void registerGetSchemaVersion(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-schema-version", + "Get a specific schema version of a topic", + """ { "type": "object", "properties": { @@ -163,53 +171,56 @@ private void registerGetSchemaVersion(McpSyncServer mcpServer) { }, "required": ["topic", "versionIndex"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - int versionIndex = getIntParam(request.arguments(), "versionIndex", 0); - - List schemaInfos = pulsarAdmin - .schemas() - .getAllSchemas(topic); - if (versionIndex < 0 || versionIndex >= schemaInfos.size()) { - return createErrorResult("Invalid versionIndex: " - + versionIndex - + ", available versions: " - + schemaInfos.size()); - } - - SchemaInfo schemaInfo = schemaInfos.get(versionIndex); - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("versionIndex", versionIndex); - result.put("type", schemaInfo.getType().toString()); - result.put("schema", new String(schemaInfo.getSchema())); - result.put("properties", schemaInfo.getProperties()); - result.put("name", schemaInfo.getName()); - - addTopicBreakdown(result, topic); - - return createSuccessResult("Fetched schema version " + versionIndex + " successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (PulsarAdminException e) { - LOGGER.error("Failed to fetch schema version for topic", e); - return createErrorResult("Failed to fetch schema version: " + e.getMessage()); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + int versionIndex = getIntParam(request.arguments(), "versionIndex", 0); + + List schemaInfos = pulsarAdmin.schemas().getAllSchemas(topic); + if (versionIndex < 0 || versionIndex >= schemaInfos.size()) { + return createErrorResult( + "Invalid versionIndex: " + + versionIndex + + ", available versions: " + + schemaInfos.size()); } - }).build()); - } - - private void registerUploadSchema(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "upload-schema", - "Upload a new schema to a topic", - """ + + SchemaInfo schemaInfo = schemaInfos.get(versionIndex); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("versionIndex", versionIndex); + result.put("type", schemaInfo.getType().toString()); + result.put("schema", new String(schemaInfo.getSchema())); + result.put("properties", schemaInfo.getProperties()); + result.put("name", schemaInfo.getName()); + + addTopicBreakdown(result, topic); + + return createSuccessResult( + "Fetched schema version " + versionIndex + " successfully", result); + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (PulsarAdminException e) { + LOGGER.error("Failed to fetch schema version for topic", e); + return createErrorResult("Failed to fetch schema version: " + e.getMessage()); + } + }) + .build()); + } + + private void registerUploadSchema(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "upload-schema", + "Upload a new schema to a topic", + """ { "type": "object", "properties": { @@ -233,70 +244,76 @@ private void registerUploadSchema(McpSyncServer mcpServer) { }, "required": ["topic", "schema", "schemaType"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String schemaStr = getRequiredStringParam(request.arguments(), "schema"); + String schemaTypeStr = + getRequiredStringParam(request.arguments(), "schemaType"); + + SchemaType schemaType; try { - String topic = buildFullTopicName(request.arguments()); - String schemaStr = getRequiredStringParam(request.arguments(), "schema"); - String schemaTypeStr = getRequiredStringParam(request.arguments(), "schemaType"); - - SchemaType schemaType; - try { - schemaType = SchemaType.valueOf(schemaTypeStr.toUpperCase()); - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid schema type: " + schemaTypeStr, - List.of("Valid types: AVRO, JSON, STRING, PROTOBUF, BYTES")); - } + schemaType = SchemaType.valueOf(schemaTypeStr.toUpperCase()); + } catch (IllegalArgumentException e) { + return createErrorResult( + "Invalid schema type: " + schemaTypeStr, + List.of("Valid types: AVRO, JSON, STRING, PROTOBUF, BYTES")); + } - Map props = null; - Object pObj = request.arguments().get("properties"); - if (pObj instanceof Map m) { - props = new HashMap<>(); - for (Map.Entry en : m.entrySet()) { - if (en.getKey() != null && en.getValue() != null) { - props.put(String.valueOf(en.getKey()), String.valueOf(en.getValue())); - } - } + Map props = null; + Object pObj = request.arguments().get("properties"); + if (pObj instanceof Map m) { + props = new HashMap<>(); + for (Map.Entry en : m.entrySet()) { + if (en.getKey() != null && en.getValue() != null) { + props.put(String.valueOf(en.getKey()), String.valueOf(en.getValue())); } - - SchemaInfo schemaInfo = SchemaInfo.builder() - .name(topic) - .type(schemaType) - .schema(schemaStr.getBytes(StandardCharsets.UTF_8)) - .properties(props) - .build(); - - pulsarAdmin.schemas().createSchema(topic, schemaInfo); - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("schema", schemaStr); - result.put("schemaType", schemaTypeStr); - result.put("uploaded", true); - - addTopicBreakdown(result, topic); - - return createSuccessResult("Schema uploaded successfully to topic: " + topic, null); - - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid schemaType: " + e.getMessage()); - } catch (PulsarAdminException e) { - LOGGER.error("Failed to upload schema to topic", e); - return createErrorResult("PulsarAdminException: " + e.getMessage()); + } } - }).build() - ); - } - - private void registerDeleteSchema(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "delete-schema", - "Delete the schema of a topic", - """ + + SchemaInfo schemaInfo = + SchemaInfo.builder() + .name(topic) + .type(schemaType) + .schema(schemaStr.getBytes(StandardCharsets.UTF_8)) + .properties(props) + .build(); + + pulsarAdmin.schemas().createSchema(topic, schemaInfo); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("schema", schemaStr); + result.put("schemaType", schemaTypeStr); + result.put("uploaded", true); + + addTopicBreakdown(result, topic); + + return createSuccessResult( + "Schema uploaded successfully to topic: " + topic, null); + + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid schemaType: " + e.getMessage()); + } catch (PulsarAdminException e) { + LOGGER.error("Failed to upload schema to topic", e); + return createErrorResult("PulsarAdminException: " + e.getMessage()); + } + }) + .build()); + } + + private void registerDeleteSchema(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "delete-schema", + "Delete the schema of a topic", + """ { "type": "object", "properties": { @@ -312,42 +329,45 @@ private void registerDeleteSchema(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - Boolean force = getBooleanParam(request.arguments(), "force", false); - - pulsarAdmin.schemas().deleteSchema(topic, force); - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("deleted", true); - result.put("force", force); - - addTopicBreakdown(result, topic); - - return createSuccessResult("Schema deleted successfully from topic: " + topic, result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (PulsarAdminException e) { - LOGGER.error("Failed to delete schema from topic", e); - return createErrorResult("PulsarAdminException: " + e.getMessage()); - } - }).build() - ); - } - - private void registerTestSchemaCompatibility(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "test-schema-compatibility", - "Test if a schema is compatible with the existing schema of a topic", - """ + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + Boolean force = getBooleanParam(request.arguments(), "force", false); + + pulsarAdmin.schemas().deleteSchema(topic, force); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("deleted", true); + result.put("force", force); + + addTopicBreakdown(result, topic); + + return createSuccessResult( + "Schema deleted successfully from topic: " + topic, result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (PulsarAdminException e) { + LOGGER.error("Failed to delete schema from topic", e); + return createErrorResult("PulsarAdminException: " + e.getMessage()); + } + }) + .build()); + } + + private void registerTestSchemaCompatibility(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "test-schema-compatibility", + "Test if a schema is compatible with the existing schema of a topic", + """ { "type": "object", "properties": { @@ -367,49 +387,57 @@ private void registerTestSchemaCompatibility(McpSyncServer mcpServer) { }, "required": ["topic", "schema", "schemaType"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String schemaStr = getRequiredStringParam(request.arguments(), "schema"); + String schemaTypeStr = + getRequiredStringParam(request.arguments(), "schemaType"); + + SchemaType schemaType; try { - String topic = buildFullTopicName(request.arguments()); - String schemaStr = getRequiredStringParam(request.arguments(), "schema"); - String schemaTypeStr = getRequiredStringParam(request.arguments(), "schemaType"); - - SchemaType schemaType; - try { - schemaType = SchemaType.valueOf(schemaTypeStr.toUpperCase()); - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid schema type: " + schemaTypeStr, - List.of("Valid types: AVRO, JSON, STRING, PROTOBUF, BYTES")); - } - - SchemaInfo schemaInfo = SchemaInfo.builder() - .name(topic) - .type(schemaType) - .schema(schemaStr.getBytes(StandardCharsets.UTF_8)) - .build(); - - boolean isCompatible = pulsarAdmin.schemas() - .testCompatibility(topic, schemaInfo).isCompatibility(); - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("isCompatible", isCompatible); - result.put("schemaType", schemaType); - - addTopicBreakdown(result, topic); - - return createSuccessResult("Compatibility test result: " + isCompatible, result); + schemaType = SchemaType.valueOf(schemaTypeStr.toUpperCase()); } catch (IllegalArgumentException e) { - return createErrorResult("Invalid parameter: " + e.getMessage()); - } catch (PulsarAdminException e) { - LOGGER.error("Failed to test schema compatibility", e); - return createErrorResult("PulsarAdminException: " + e.getMessage()); + return createErrorResult( + "Invalid schema type: " + schemaTypeStr, + List.of("Valid types: AVRO, JSON, STRING, PROTOBUF, BYTES")); } - }).build() - ); - } + + SchemaInfo schemaInfo = + SchemaInfo.builder() + .name(topic) + .type(schemaType) + .schema(schemaStr.getBytes(StandardCharsets.UTF_8)) + .build(); + + boolean isCompatible = + pulsarAdmin + .schemas() + .testCompatibility(topic, schemaInfo) + .isCompatibility(); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("isCompatible", isCompatible); + result.put("schemaType", schemaType); + + addTopicBreakdown(result, topic); + + return createSuccessResult( + "Compatibility test result: " + isCompatible, result); + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid parameter: " + e.getMessage()); + } catch (PulsarAdminException e) { + LOGGER.error("Failed to test schema compatibility", e); + return createErrorResult("PulsarAdminException: " + e.getMessage()); + } + }) + .build()); + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/SubscriptionTools.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/SubscriptionTools.java index b054a22..5fbdaab 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/SubscriptionTools.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/SubscriptionTools.java @@ -26,31 +26,31 @@ import org.apache.pulsar.common.policies.data.SubscriptionStats; import org.apache.pulsar.common.policies.data.TopicStats; -public class SubscriptionTools extends BasePulsarTools{ - - public SubscriptionTools(PulsarAdmin pulsarAdmin) { - super(pulsarAdmin); - } - - public void registerTools(McpSyncServer mcpServer) { - registerListSubscriptions(mcpServer); - registerGetSubscriptionStats(mcpServer); - registerCreateSubscription(mcpServer); - registerDeleteSubscription(mcpServer); - registerSkipMessages(mcpServer); - registerResetSubscriptionCursor(mcpServer); - registerExpireSubscriptionMessages(mcpServer); - registerUnsubscribe(mcpServer); - registerListSubscriptionConsumers(mcpServer); - registerGetSubscriptionCursorPositions(mcpServer); - - } - - private void registerListSubscriptions(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "list-subscriptions", - "List all subscriptions for a specific topic", - """ +public class SubscriptionTools extends BasePulsarTools { + + public SubscriptionTools(PulsarAdmin pulsarAdmin) { + super(pulsarAdmin); + } + + public void registerTools(McpSyncServer mcpServer) { + registerListSubscriptions(mcpServer); + registerGetSubscriptionStats(mcpServer); + registerCreateSubscription(mcpServer); + registerDeleteSubscription(mcpServer); + registerSkipMessages(mcpServer); + registerResetSubscriptionCursor(mcpServer); + registerExpireSubscriptionMessages(mcpServer); + registerUnsubscribe(mcpServer); + registerListSubscriptionConsumers(mcpServer); + registerGetSubscriptionCursorPositions(mcpServer); + } + + private void registerListSubscriptions(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "list-subscriptions", + "List all subscriptions for a specific topic", + """ { "type": "object", "properties": { @@ -61,39 +61,41 @@ private void registerListSubscriptions(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); + """); - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); - List subscriptions = pulsarAdmin.topics().getSubscriptions(topic); + List subscriptions = pulsarAdmin.topics().getSubscriptions(topic); - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptions", subscriptions); - result.put("subscriptionCount", subscriptions.size()); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptions", subscriptions); + result.put("subscriptionCount", subscriptions.size()); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Subscriptions listed successfully", result); + return createSuccessResult("Subscriptions listed successfully", result); - } catch (Exception e) { - LOGGER.error("Failed to list subscriptions", e); - return createErrorResult("Failed to list subscriptions: " + e.getMessage()); - } + } catch (Exception e) { + LOGGER.error("Failed to list subscriptions", e); + return createErrorResult("Failed to list subscriptions: " + e.getMessage()); + } }) - .build()); - } - - private void registerGetSubscriptionStats(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-subscription-stats", - "Get statistics of a subscription for a specific topic", - """ + .build()); + } + + private void registerGetSubscriptionStats(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-subscription-stats", + "Get statistics of a subscription for a specific topic", + """ { "type": "object", "properties": { @@ -108,59 +110,61 @@ private void registerGetSubscriptionStats(McpSyncServer mcpServer) { }, "required": ["topic", "subscription"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscription = getRequiredStringParam(request.arguments(), "subscription"); - - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - if (meta.partitions > 0) { - return createErrorResult("Please specify a concrete partition, e.g. topic-partition-0"); - } - - TopicStats stats = pulsarAdmin.topics().getStats(topic); - SubscriptionStats subStats = stats.getSubscriptions().get(subscription); - if (subStats == null) { - return createErrorResult("Subscription not found: " + subscription); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscription = + getRequiredStringParam(request.arguments(), "subscription"); + + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + if (meta.partitions > 0) { + return createErrorResult( + "Please specify a concrete partition, e.g. topic-partition-0"); + } - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscription", subscription); - result.put("msgBacklog", subStats.getMsgBacklog()); - result.put("msgRateOut", subStats.getMsgRateOut()); - result.put("msgThroughputOut", subStats.getMsgThroughputOut()); - result.put("msgRateRedeliver", - subStats.getMsgRateRedeliver()); - result.put("type", subStats.getType()); - result.put("consumerCount", - subStats.getConsumers() != null - ? subStats.getConsumers().size() - : 0); - result.put("isReplicated", subStats.isReplicated()); - - addTopicBreakdown(result, topic); - - return createSuccessResult("Subscription stats fetched successfully", result); - - } catch (Exception e) { - LOGGER.error("Failed to get subscription stats", e); - return createErrorResult("Failed to get subscription stats: " + e.getMessage()); + TopicStats stats = pulsarAdmin.topics().getStats(topic); + SubscriptionStats subStats = stats.getSubscriptions().get(subscription); + if (subStats == null) { + return createErrorResult("Subscription not found: " + subscription); } + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscription", subscription); + result.put("msgBacklog", subStats.getMsgBacklog()); + result.put("msgRateOut", subStats.getMsgRateOut()); + result.put("msgThroughputOut", subStats.getMsgThroughputOut()); + result.put("msgRateRedeliver", subStats.getMsgRateRedeliver()); + result.put("type", subStats.getType()); + result.put( + "consumerCount", + subStats.getConsumers() != null ? subStats.getConsumers().size() : 0); + result.put("isReplicated", subStats.isReplicated()); + + addTopicBreakdown(result, topic); + + return createSuccessResult("Subscription stats fetched successfully", result); + + } catch (Exception e) { + LOGGER.error("Failed to get subscription stats", e); + return createErrorResult("Failed to get subscription stats: " + e.getMessage()); + } }) - .build()); - } - - private void registerCreateSubscription(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "create-subscription", - "Create a subscription on a topic", - """ + .build()); + } + + private void registerCreateSubscription(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "create-subscription", + "Create a subscription on a topic", + """ { "type": "object", "properties": { @@ -180,49 +184,52 @@ private void registerCreateSubscription(McpSyncServer mcpServer) { }, "required": ["topic", "subscription"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscription = getRequiredStringParam(request.arguments(), "subscription"); - String messageId = getStringParam(request.arguments(), "messageId"); - - String pos = (messageId == null ? "latest" : messageId.trim().toLowerCase()); - if (!pos.equals("latest") && !pos.equals("earliest")) { - return createErrorResult("messageId must be 'latest' or 'earliest'"); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscription = + getRequiredStringParam(request.arguments(), "subscription"); + String messageId = getStringParam(request.arguments(), "messageId"); + + String pos = (messageId == null ? "latest" : messageId.trim().toLowerCase()); + if (!pos.equals("latest") && !pos.equals("earliest")) { + return createErrorResult("messageId must be 'latest' or 'earliest'"); + } - MessageId initial = pos.equals("earliest") ? MessageId.earliest : MessageId.latest; - pulsarAdmin.topics().createSubscription(topic, subscription, initial); + MessageId initial = + pos.equals("earliest") ? MessageId.earliest : MessageId.latest; + pulsarAdmin.topics().createSubscription(topic, subscription, initial); - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscription", subscription); - result.put("messageId", messageId != null ? messageId : "latest"); - result.put("created", true); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscription", subscription); + result.put("messageId", messageId != null ? messageId : "latest"); + result.put("created", true); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Subscription created successfully", result); + return createSuccessResult("Subscription created successfully", result); - } catch (Exception e) { - LOGGER.error("Failed to create subscription", e); - return createErrorResult("Failed to create subscription: " + e.getMessage()); - } + } catch (Exception e) { + LOGGER.error("Failed to create subscription", e); + return createErrorResult("Failed to create subscription: " + e.getMessage()); + } }) - .build() - ); - } - - private void registerDeleteSubscription(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "delete-subscription", - "Delete a subscription from a specific topic", - """ + .build()); + } + + private void registerDeleteSubscription(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "delete-subscription", + "Delete a subscription from a specific topic", + """ { "type": "object", "properties": { @@ -237,39 +244,43 @@ private void registerDeleteSubscription(McpSyncServer mcpServer) { }, "required": ["topic", "subscription"] } - """ - ); + """); - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscription = getRequiredStringParam(request.arguments(), "subscription"); + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscription = + getRequiredStringParam(request.arguments(), "subscription"); - pulsarAdmin.topics().deleteSubscription(topic, subscription); + pulsarAdmin.topics().deleteSubscription(topic, subscription); - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscription", subscription); - result.put("deleted", true); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscription", subscription); + result.put("deleted", true); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Subscription deleted successfully", result); + return createSuccessResult("Subscription deleted successfully", result); - } catch (Exception e) { - LOGGER.error("Failed to delete subscription", e); - return createErrorResult("Failed to delete subscription: " + e.getMessage()); - } - }).build()); - } - - private void registerSkipMessages(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "skip-messages", - "Skip messages for a subscription on a specific topic", - """ + } catch (Exception e) { + LOGGER.error("Failed to delete subscription", e); + return createErrorResult("Failed to delete subscription: " + e.getMessage()); + } + }) + .build()); + } + + private void registerSkipMessages(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "skip-messages", + "Skip messages for a subscription on a specific topic", + """ { "type": "object", "properties": { @@ -289,44 +300,48 @@ private void registerSkipMessages(McpSyncServer mcpServer) { }, "required": ["topic", "subscription"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscription = getRequiredStringParam(request.arguments(), "subscription"); - int numMessages = getIntParam(request.arguments(), "numMessages", 1); - - if (numMessages <= 0) { - return createErrorResult("Number of messages must be greater than 0."); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscription = + getRequiredStringParam(request.arguments(), "subscription"); + int numMessages = getIntParam(request.arguments(), "numMessages", 1); + + if (numMessages <= 0) { + return createErrorResult("Number of messages must be greater than 0."); + } - pulsarAdmin.topics().skipMessages(topic, subscription, numMessages); + pulsarAdmin.topics().skipMessages(topic, subscription, numMessages); - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscription", subscription); - result.put("numMessagesSkipped", numMessages); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscription", subscription); + result.put("numMessagesSkipped", numMessages); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Skipped messages successfully", result); + return createSuccessResult("Skipped messages successfully", result); - } catch (Exception e) { - LOGGER.error("Failed to skip messages", e); - return createErrorResult("Failed to skip messages: " + e.getMessage()); - } - }).build()); - } - - private void registerResetSubscriptionCursor(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "reset-subscription-cursor", - "Reset a subscription cursor to a specific message publish time (timestamp in ms)", - """ + } catch (Exception e) { + LOGGER.error("Failed to skip messages", e); + return createErrorResult("Failed to skip messages: " + e.getMessage()); + } + }) + .build()); + } + + private void registerResetSubscriptionCursor(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "reset-subscription-cursor", + "Reset a subscription cursor to a specific message publish time (timestamp in ms)", + """ { "type": "object", "properties": { @@ -346,45 +361,50 @@ private void registerResetSubscriptionCursor(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getRequiredStringParam(request.arguments(), "subscriptionName"); - Long timestamp = getLongParam(request.arguments(), "timestamp", 0L); - if (timestamp <= 0) { - timestamp = 0L; - } - pulsarAdmin.topics().resetCursor(topic, subscriptionName, timestamp); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getRequiredStringParam(request.arguments(), "subscriptionName"); + Long timestamp = getLongParam(request.arguments(), "timestamp", 0L); + if (timestamp <= 0) { + timestamp = 0L; + } + pulsarAdmin.topics().resetCursor(topic, subscriptionName, timestamp); - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscriptionName); - result.put("timestamp", timestamp); - result.put("reset", true); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("timestamp", timestamp); + result.put("reset", true); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Subscription cursor reset successfully", result); + return createSuccessResult("Subscription cursor reset successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to reset subscription cursor", e); - return createErrorResult("Failed to reset subscription cursor: " + e.getMessage()); - } - }).build()); - } - - private void registerExpireSubscriptionMessages(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "expire-subscription-messages", - "Expire messages for a subscription older than the given seconds", - """ + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to reset subscription cursor", e); + return createErrorResult( + "Failed to reset subscription cursor: " + e.getMessage()); + } + }) + .build()); + } + + private void registerExpireSubscriptionMessages(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "expire-subscription-messages", + "Expire messages for a subscription older than the given seconds", + """ { "type": "object", "properties": { @@ -405,45 +425,50 @@ private void registerExpireSubscriptionMessages(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getRequiredStringParam(request.arguments(), "subscriptionName"); - Integer expireTimeSeconds = getIntParam(request.arguments(), "expireTimeSeconds", 0); - - pulsarAdmin.topics().expireMessages(topic, subscriptionName, expireTimeSeconds); - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscriptionName); - result.put("expireTimeSeconds", expireTimeSeconds); - result.put("expired", true); - - addTopicBreakdown(result, topic); - - return createSuccessResult( - "Expired subscription messages up to message ID successfully" - , result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to expire subscription messages", e); - return createErrorResult("Failed to expire subscription messages: " + e.getMessage()); - } - }).build()); - } - - private void registerUnsubscribe(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "unsubscribe", - "Unsubscribe a subscription from a topic", - """ + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getRequiredStringParam(request.arguments(), "subscriptionName"); + Integer expireTimeSeconds = + getIntParam(request.arguments(), "expireTimeSeconds", 0); + + pulsarAdmin.topics().expireMessages(topic, subscriptionName, expireTimeSeconds); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("expireTimeSeconds", expireTimeSeconds); + result.put("expired", true); + + addTopicBreakdown(result, topic); + + return createSuccessResult( + "Expired subscription messages up to message ID successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to expire subscription messages", e); + return createErrorResult( + "Failed to expire subscription messages: " + e.getMessage()); + } + }) + .build()); + } + + private void registerUnsubscribe(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "unsubscribe", + "Unsubscribe a subscription from a topic", + """ { "type": "object", "properties": { @@ -458,41 +483,45 @@ private void registerUnsubscribe(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getRequiredStringParam(request.arguments(), "subscriptionName"); - - pulsarAdmin.topics().deleteSubscription(topic, subscriptionName); - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscriptionName); - result.put("unsubscribed", true); - - addTopicBreakdown(result, topic); - - return createSuccessResult("Subscription unsubscribed successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to unsubscribe subscription", e); - return createErrorResult("Failed to unsubscribe: " + e.getMessage()); - } - }).build()); - } - - private void registerListSubscriptionConsumers(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "list-subscription-consumers", - "List consumers of a subscription, with per-consumer metrics;", - """ + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getRequiredStringParam(request.arguments(), "subscriptionName"); + + pulsarAdmin.topics().deleteSubscription(topic, subscriptionName); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("unsubscribed", true); + + addTopicBreakdown(result, topic); + + return createSuccessResult("Subscription unsubscribed successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to unsubscribe subscription", e); + return createErrorResult("Failed to unsubscribe: " + e.getMessage()); + } + }) + .build()); + } + + private void registerListSubscriptionConsumers(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "list-subscription-consumers", + "List consumers of a subscription, with per-consumer metrics;", + """ { "type": "object", "properties": { @@ -507,54 +536,39 @@ private void registerListSubscriptionConsumers(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscription = getRequiredStringParam(request.arguments(), "subscriptionName"); - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscription); - result.put("timestamp", System.currentTimeMillis()); - - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - List> consumers = new ArrayList<>(); - - if (meta.partitions > 0) { - var ps = pulsarAdmin.topics().getPartitionedStats(topic, true); - ps.getPartitions().forEach((partition, ts) -> { - var subStats = ts.getSubscriptions() != null - ? ts.getSubscriptions().get(subscription) : null; + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscription = + getRequiredStringParam(request.arguments(), "subscriptionName"); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscription); + result.put("timestamp", System.currentTimeMillis()); + + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + List> consumers = new ArrayList<>(); + + if (meta.partitions > 0) { + var ps = pulsarAdmin.topics().getPartitionedStats(topic, true); + ps.getPartitions() + .forEach( + (partition, ts) -> { + var subStats = + ts.getSubscriptions() != null + ? ts.getSubscriptions().get(subscription) + : null; if (subStats != null && subStats.getConsumers() != null) { - for (var c : subStats.getConsumers()) { - Map one = new HashMap<>(); - one.put("partition", partition); - one.put("consumerName", c.getConsumerName()); - one.put("address", c.getAddress()); - one.put("connectedSince", c.getConnectedSince()); - one.put("msgRateOut", c.getMsgRateOut()); - one.put("msgThroughputOut", c.getMsgThroughputOut()); - one.put("availablePermits", c.getAvailablePermits()); - one.put("unackedMessages", c.getUnackedMessages()); - consumers.add(one); - } - } - }); - } else { - var stats = pulsarAdmin.topics().getStats(topic); - var subStats = stats.getSubscriptions() != null - ? stats.getSubscriptions().get(subscription) : null; - if (subStats == null) { - return createErrorResult("Subscription not found: " + subscription); - } - if (subStats.getConsumers() != null) { - for (var c : subStats.getConsumers()) { + for (var c : subStats.getConsumers()) { Map one = new HashMap<>(); + one.put("partition", partition); one.put("consumerName", c.getConsumerName()); one.put("address", c.getAddress()); one.put("connectedSince", c.getConnectedSince()); @@ -563,31 +577,56 @@ private void registerListSubscriptionConsumers(McpSyncServer mcpServer) { one.put("availablePermits", c.getAvailablePermits()); one.put("unackedMessages", c.getUnackedMessages()); consumers.add(one); + } } - } + }); + } else { + var stats = pulsarAdmin.topics().getStats(topic); + var subStats = + stats.getSubscriptions() != null + ? stats.getSubscriptions().get(subscription) + : null; + if (subStats == null) { + return createErrorResult("Subscription not found: " + subscription); + } + if (subStats.getConsumers() != null) { + for (var c : subStats.getConsumers()) { + Map one = new HashMap<>(); + one.put("consumerName", c.getConsumerName()); + one.put("address", c.getAddress()); + one.put("connectedSince", c.getConnectedSince()); + one.put("msgRateOut", c.getMsgRateOut()); + one.put("msgThroughputOut", c.getMsgThroughputOut()); + one.put("availablePermits", c.getAvailablePermits()); + one.put("unackedMessages", c.getUnackedMessages()); + consumers.add(one); } + } + } - result.put("consumerCount", consumers.size()); - result.put("consumers", consumers); + result.put("consumerCount", consumers.size()); + result.put("consumers", consumers); - addTopicBreakdown(result, topic); - return createSuccessResult("Subscription consumers retrieved", result); + addTopicBreakdown(result, topic); + return createSuccessResult("Subscription consumers retrieved", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to list subscription consumers", e); - return createErrorResult("Failed to list subscription consumers: " + e.getMessage()); - } + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to list subscription consumers", e); + return createErrorResult( + "Failed to list subscription consumers: " + e.getMessage()); + } }) - .build()); - } - - private void registerGetSubscriptionCursorPositions(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-subscription-cursor-positions", - "Get cursor positions (markDelete/read) of a subscription; supports partitioned topics", - """ + .build()); + } + + private void registerGetSubscriptionCursorPositions(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-subscription-cursor-positions", + "Get cursor positions (markDelete/read) of a subscription; supports partitioned topics", + """ { "type": "object", "properties": { @@ -602,78 +641,83 @@ private void registerGetSubscriptionCursorPositions(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscription = getRequiredStringParam(request.arguments(), "subscriptionName"); - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscription); - result.put("timestamp", System.currentTimeMillis()); - - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - Map positions = new LinkedHashMap<>(); - int found = 0; - - if (meta.partitions > 0) { - var ps = pulsarAdmin.topics().getPartitionedStats(topic, true); - for (String partition : ps.getPartitions().keySet()) { - try { - var internal = pulsarAdmin.topics().getInternalStats(partition); - if (internal != null && internal.cursors != null - && internal.cursors.containsKey(subscription)) { - var cur = internal.cursors.get(subscription); - Map info = new HashMap<>(); - info.put("markDeletePosition", cur.markDeletePosition); - info.put("readPosition", cur.readPosition); - info.put("messagesConsumedCounter", cur.messagesConsumedCounter); - positions.put(partition, info); - found++; - } else { - positions.put(partition, - Map.of("message", "cursor not found on this partition")); - } - } catch (Exception ie) { - positions.put(partition, Map.of("error", ie.getMessage())); - } - } - } else { - var internal = pulsarAdmin.topics().getInternalStats(topic); - if (internal != null && internal.cursors != null - && internal.cursors.containsKey(subscription)) { - var cur = internal.cursors.get(subscription); - Map info = new HashMap<>(); - info.put("markDeletePosition", cur.markDeletePosition); - info.put("readPosition", cur.readPosition); - info.put("messagesConsumedCounter", cur.messagesConsumedCounter); - positions.put(topic, info); - found = 1; - } else { - return createErrorResult("Cursor not found for subscription: " + subscription); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscription = + getRequiredStringParam(request.arguments(), "subscriptionName"); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscription); + result.put("timestamp", System.currentTimeMillis()); + + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + Map positions = new LinkedHashMap<>(); + int found = 0; + + if (meta.partitions > 0) { + var ps = pulsarAdmin.topics().getPartitionedStats(topic, true); + for (String partition : ps.getPartitions().keySet()) { + try { + var internal = pulsarAdmin.topics().getInternalStats(partition); + if (internal != null + && internal.cursors != null + && internal.cursors.containsKey(subscription)) { + var cur = internal.cursors.get(subscription); + Map info = new HashMap<>(); + info.put("markDeletePosition", cur.markDeletePosition); + info.put("readPosition", cur.readPosition); + info.put("messagesConsumedCounter", cur.messagesConsumedCounter); + positions.put(partition, info); + found++; + } else { + positions.put( + partition, Map.of("message", "cursor not found on this partition")); + } + } catch (Exception ie) { + positions.put(partition, Map.of("error", ie.getMessage())); } + } + } else { + var internal = pulsarAdmin.topics().getInternalStats(topic); + if (internal != null + && internal.cursors != null + && internal.cursors.containsKey(subscription)) { + var cur = internal.cursors.get(subscription); + Map info = new HashMap<>(); + info.put("markDeletePosition", cur.markDeletePosition); + info.put("readPosition", cur.readPosition); + info.put("messagesConsumedCounter", cur.messagesConsumedCounter); + positions.put(topic, info); + found = 1; + } else { + return createErrorResult( + "Cursor not found for subscription: " + subscription); + } + } - result.put("foundOnPartitions", found); - result.put("positions", positions); + result.put("foundOnPartitions", found); + result.put("positions", positions); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Subscription cursor positions retrieved", result); + return createSuccessResult("Subscription cursor positions retrieved", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get subscription cursor positions", e); - return createErrorResult("Failed to get subscription cursor positions: " + e.getMessage()); - } + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get subscription cursor positions", e); + return createErrorResult( + "Failed to get subscription cursor positions: " + e.getMessage()); + } }) - .build()); - } - + .build()); + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/TenantTools.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/TenantTools.java index 2b26bea..453c267 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/TenantTools.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/TenantTools.java @@ -29,55 +29,57 @@ public class TenantTools extends BasePulsarTools { - public TenantTools(PulsarAdmin pulsarAdmin) { - super(pulsarAdmin); - } - - public void registerTools(McpSyncServer mcpServer) { - registerListTenant(mcpServer); - registerGetTenantInfo(mcpServer); - registerCreateTenant(mcpServer); - registerUpdateTenant(mcpServer); - registerDeleteTenant(mcpServer); - registerGetTenantStats(mcpServer); - } - - private void registerListTenant(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "list-tenants", - "List all Pulsar tenants", - """ + public TenantTools(PulsarAdmin pulsarAdmin) { + super(pulsarAdmin); + } + + public void registerTools(McpSyncServer mcpServer) { + registerListTenant(mcpServer); + registerGetTenantInfo(mcpServer); + registerCreateTenant(mcpServer); + registerUpdateTenant(mcpServer); + registerDeleteTenant(mcpServer); + registerGetTenantStats(mcpServer); + } + + private void registerListTenant(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "list-tenants", + "List all Pulsar tenants", + """ { "type": "object", "properties": {}, "required": [] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - List tenants = pulsarAdmin.tenants().getTenants(); - Map result = Map.of( - "tenants", tenants, - "count", tenants.size() - ); - return createSuccessResult("Tenant list retrieved successfully", result); - } catch (Exception e) { - LOGGER.error("Failed to list tenants", e); - return createErrorResult("Failed to list tenants", List.of(safeErrorMessage(e))); - } - }).build() - ); - } - - private void registerGetTenantInfo(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-tenant-info", - "Get information about a Pulsar tenant", - """ + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + List tenants = pulsarAdmin.tenants().getTenants(); + Map result = + Map.of("tenants", tenants, "count", tenants.size()); + return createSuccessResult("Tenant list retrieved successfully", result); + } catch (Exception e) { + LOGGER.error("Failed to list tenants", e); + return createErrorResult( + "Failed to list tenants", List.of(safeErrorMessage(e))); + } + }) + .build()); + } + + private void registerGetTenantInfo(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-tenant-info", + "Get information about a Pulsar tenant", + """ { "type": "object", "properties": { @@ -88,36 +90,39 @@ private void registerGetTenantInfo(McpSyncServer mcpServer) { }, "required": ["tenant"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String tenant = getRequiredStringParam(request.arguments(), "tenant"); - TenantInfo tenantInfo = pulsarAdmin.tenants().getTenantInfo(tenant); - Map result = Map.of( - "tenant", tenant, - "allowedClusters", tenantInfo.getAllowedClusters(), - "adminRoles", tenantInfo.getAdminRoles() - ); - return createSuccessResult("Tenant info retrieved successfully", result); - } catch (PulsarAdminException.NotFoundException e) { - return createErrorResult("Tenant not found", List.of("Tenant does not exist")); - } catch (Exception e) { - LOGGER.error("Failed to get tenant info", e); - return createErrorResult("Failed to get tenant info", List.of(safeErrorMessage(e))); - } - }).build() - ); - } - - private void registerCreateTenant(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "create-tenant", - "Create a new Pulsar tenant", - """ + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String tenant = getRequiredStringParam(request.arguments(), "tenant"); + TenantInfo tenantInfo = pulsarAdmin.tenants().getTenantInfo(tenant); + Map result = + Map.of( + "tenant", tenant, + "allowedClusters", tenantInfo.getAllowedClusters(), + "adminRoles", tenantInfo.getAdminRoles()); + return createSuccessResult("Tenant info retrieved successfully", result); + } catch (PulsarAdminException.NotFoundException e) { + return createErrorResult("Tenant not found", List.of("Tenant does not exist")); + } catch (Exception e) { + LOGGER.error("Failed to get tenant info", e); + return createErrorResult( + "Failed to get tenant info", List.of(safeErrorMessage(e))); + } + }) + .build()); + } + + private void registerCreateTenant(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "create-tenant", + "Create a new Pulsar tenant", + """ { "type": "object", "properties": { @@ -138,84 +143,92 @@ private void registerCreateTenant(McpSyncServer mcpServer) { }, "required": ["tenant"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String tenant = getRequiredStringParam(request.arguments(), "tenant"); + """); - try { - pulsarAdmin.tenants().getTenantInfo(tenant); - return createErrorResult("Tenant already exists: " + tenant, - List.of("Choose a different tenant name")); - } catch (PulsarAdminException.NotFoundException ignore) { + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String tenant = getRequiredStringParam(request.arguments(), "tenant"); - } catch (PulsarAdminException e) { - return createErrorResult("Failed to verify tenant existence: " + e.getMessage()); - } + try { + pulsarAdmin.tenants().getTenantInfo(tenant); + return createErrorResult( + "Tenant already exists: " + tenant, + List.of("Choose a different tenant name")); + } catch (PulsarAdminException.NotFoundException ignore) { + + } catch (PulsarAdminException e) { + return createErrorResult( + "Failed to verify tenant existence: " + e.getMessage()); + } - Set adminRoles = getSetParam(request.arguments(), "adminRoles"); - Set allowedClusters = getSetParam(request.arguments(), "allowedClusters"); + Set adminRoles = getSetParam(request.arguments(), "adminRoles"); + Set allowedClusters = + getSetParam(request.arguments(), "allowedClusters"); - List availableClusters0; - try { - availableClusters0 = pulsarAdmin.clusters().getClusters(); - } catch (Exception ex) { - LOGGER.warn("Failed to get clusters", ex); - availableClusters0 = List.of(); - } - Set availableClusters = (availableClusters0 == null) - ? Set.of() - : new HashSet<>(availableClusters0); - - if (allowedClusters.isEmpty()) { - if (!availableClusters.isEmpty()) { - allowedClusters = Set.copyOf(availableClusters); - } else { - allowedClusters = Set.of("standalone"); - } - } else { - if (!availableClusters.isEmpty()) { - Set invalid = new HashSet<>(allowedClusters); - invalid.removeAll(availableClusters); - if (!invalid.isEmpty()) { - return createErrorResult("Invalid clusters in allowedClusters: " + invalid); - } - } + List availableClusters0; + try { + availableClusters0 = pulsarAdmin.clusters().getClusters(); + } catch (Exception ex) { + LOGGER.warn("Failed to get clusters", ex); + availableClusters0 = List.of(); + } + Set availableClusters = + (availableClusters0 == null) ? Set.of() : new HashSet<>(availableClusters0); + + if (allowedClusters.isEmpty()) { + if (!availableClusters.isEmpty()) { + allowedClusters = Set.copyOf(availableClusters); + } else { + allowedClusters = Set.of("standalone"); + } + } else { + if (!availableClusters.isEmpty()) { + Set invalid = new HashSet<>(allowedClusters); + invalid.removeAll(availableClusters); + if (!invalid.isEmpty()) { + return createErrorResult( + "Invalid clusters in allowedClusters: " + invalid); } - - TenantInfo tenantInfo = TenantInfo.builder() - .adminRoles(adminRoles) - .allowedClusters(allowedClusters) - .build(); - - pulsarAdmin.tenants().createTenant(tenant, tenantInfo); - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("allowedClusters", allowedClusters); - result.put("adminRoles", adminRoles == null ? Set.of() : adminRoles); - result.put("created", true); - - return createSuccessResult("Tenant created successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to create tenant", e); - return createErrorResult("Failed to create tenant", List.of(safeErrorMessage(e))); + } } - }).build()); - } - private void registerDeleteTenant(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "delete-tenant", - "Delete a specific Pulsar tenant", - """ + TenantInfo tenantInfo = + TenantInfo.builder() + .adminRoles(adminRoles) + .allowedClusters(allowedClusters) + .build(); + + pulsarAdmin.tenants().createTenant(tenant, tenantInfo); + + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("allowedClusters", allowedClusters); + result.put("adminRoles", adminRoles == null ? Set.of() : adminRoles); + result.put("created", true); + + return createSuccessResult("Tenant created successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to create tenant", e); + return createErrorResult( + "Failed to create tenant", List.of(safeErrorMessage(e))); + } + }) + .build()); + } + + private void registerDeleteTenant(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "delete-tenant", + "Delete a specific Pulsar tenant", + """ { "type": "object", "properties": { @@ -231,72 +244,69 @@ private void registerDeleteTenant(McpSyncServer mcpServer) { }, "required": ["tenant"] } - """ - ); + """); - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String tenant = getRequiredStringParam(request.arguments(), "tenant"); - boolean force = getBooleanParam(request.arguments(), "force", false); - - try { - pulsarAdmin.tenants().getTenantInfo(tenant); - } catch (PulsarAdminException.NotFoundException e) { - return createErrorResult("Tenant not found: " + tenant); - } + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String tenant = getRequiredStringParam(request.arguments(), "tenant"); + boolean force = getBooleanParam(request.arguments(), "force", false); - if (isSystemTenant(tenant)) { - return createErrorResult( - "System tenant cannot be deleted", - List.of("Tenant: " + tenant) - ); - } + try { + pulsarAdmin.tenants().getTenantInfo(tenant); + } catch (PulsarAdminException.NotFoundException e) { + return createErrorResult("Tenant not found: " + tenant); + } - if (!force) { - List namespaces = pulsarAdmin.namespaces().getNamespaces(tenant); - if (namespaces != null && !namespaces.isEmpty()) { - return createErrorResult( - "Tenant has namespaces. Use 'force=true' to delete.", - List.of( - "Namespaces: " + namespaces, - "Set 'force' parameter to true to force deletion", - "Or manually delete all namespaces first" - ) - ); - } - } + if (isSystemTenant(tenant)) { + return createErrorResult( + "System tenant cannot be deleted", List.of("Tenant: " + tenant)); + } - pulsarAdmin.tenants().deleteTenant(tenant); - - Map resultData = new HashMap<>(); - resultData.put("tenant", tenant); - resultData.put("force", force); - resultData.put("deleted", true); - - return createSuccessResult( - "Tenant deleted successfully", - resultData - ); - - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid parameter", List.of(e.getMessage())); - } catch (Exception e) { - LOGGER.error("Failed to delete tenant", e); - String errorMessage = (e.getMessage() != null && !e.getMessage().isBlank()) - ? e.getMessage().split("\n")[0].trim() - : "Unknown error occurred"; - return createErrorResult("Failed to delete tenant", List.of(errorMessage)); + if (!force) { + List namespaces = pulsarAdmin.namespaces().getNamespaces(tenant); + if (namespaces != null && !namespaces.isEmpty()) { + return createErrorResult( + "Tenant has namespaces. Use 'force=true' to delete.", + List.of( + "Namespaces: " + namespaces, + "Set 'force' parameter to true to force deletion", + "Or manually delete all namespaces first")); + } } - }).build()); - } - private void registerUpdateTenant(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "update-tenant", - "Update the configuration of a specific Pulsar tenant", - """ + pulsarAdmin.tenants().deleteTenant(tenant); + + Map resultData = new HashMap<>(); + resultData.put("tenant", tenant); + resultData.put("force", force); + resultData.put("deleted", true); + + return createSuccessResult("Tenant deleted successfully", resultData); + + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid parameter", List.of(e.getMessage())); + } catch (Exception e) { + LOGGER.error("Failed to delete tenant", e); + String errorMessage = + (e.getMessage() != null && !e.getMessage().isBlank()) + ? e.getMessage().split("\n")[0].trim() + : "Unknown error occurred"; + return createErrorResult("Failed to delete tenant", List.of(errorMessage)); + } + }) + .build()); + } + + private void registerUpdateTenant(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "update-tenant", + "Update the configuration of a specific Pulsar tenant", + """ { "type": "object", "properties": { @@ -317,77 +327,83 @@ private void registerUpdateTenant(McpSyncServer mcpServer) { }, "required": ["tenant"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String tenant = getRequiredStringParam(request.arguments(), "tenant"); - - TenantInfo currentInfo; - try { - currentInfo = pulsarAdmin.tenants().getTenantInfo(tenant); - } catch (PulsarAdminException.NotFoundException e) { - return createErrorResult("Tenant not found: " + tenant + ". Create the tenant first."); - } - - Set currentRoles = currentInfo.getAdminRoles(); - Set currentAllowed = currentInfo.getAllowedClusters(); - if (currentRoles == null) { - currentRoles = Set.of(); - } - if (currentAllowed == null) { - currentAllowed = Set.of(); - } - - Set adminRoles = getSetParamOrDefault(request.arguments(), - "adminRoles", currentRoles); - Set allowedClusters = getSetParamOrDefault(request.arguments(), - "allowedClusters", currentAllowed); - - List availableClusters0 = pulsarAdmin.clusters().getClusters(); - Set availableClusters = (availableClusters0 == null) - ? Set.of() - : new HashSet<>(availableClusters0); - if (!allowedClusters.isEmpty() && !availableClusters.isEmpty()) { - Set invalid = new HashSet<>(allowedClusters); - invalid.removeAll(availableClusters); - if (!invalid.isEmpty()) { - return createErrorResult("Invalid clusters in allowedClusters: " + invalid); - } - } + """); - TenantInfo tenantInfo = TenantInfo.builder() - .adminRoles(adminRoles) - .allowedClusters(allowedClusters) - .build(); + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String tenant = getRequiredStringParam(request.arguments(), "tenant"); - pulsarAdmin.tenants().updateTenant(tenant, tenantInfo); - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("adminRoles", adminRoles); - result.put("allowedClusters", allowedClusters); - result.put("updated", true); + TenantInfo currentInfo; + try { + currentInfo = pulsarAdmin.tenants().getTenantInfo(tenant); + } catch (PulsarAdminException.NotFoundException e) { + return createErrorResult( + "Tenant not found: " + tenant + ". Create the tenant first."); + } - return createSuccessResult("Tenant updated successfully", result); + Set currentRoles = currentInfo.getAdminRoles(); + Set currentAllowed = currentInfo.getAllowedClusters(); + if (currentRoles == null) { + currentRoles = Set.of(); + } + if (currentAllowed == null) { + currentAllowed = Set.of(); + } - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to update tenant", e); - return createErrorResult("Failed to update tenant", List.of(safeErrorMessage(e))); + Set adminRoles = + getSetParamOrDefault(request.arguments(), "adminRoles", currentRoles); + Set allowedClusters = + getSetParamOrDefault( + request.arguments(), "allowedClusters", currentAllowed); + + List availableClusters0 = pulsarAdmin.clusters().getClusters(); + Set availableClusters = + (availableClusters0 == null) ? Set.of() : new HashSet<>(availableClusters0); + if (!allowedClusters.isEmpty() && !availableClusters.isEmpty()) { + Set invalid = new HashSet<>(allowedClusters); + invalid.removeAll(availableClusters); + if (!invalid.isEmpty()) { + return createErrorResult("Invalid clusters in allowedClusters: " + invalid); + } } - }).build()); - } - private void registerGetTenantStats(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-tenant-stats", - "Get basic stats for a specific Pulsar tenant", - """ + TenantInfo tenantInfo = + TenantInfo.builder() + .adminRoles(adminRoles) + .allowedClusters(allowedClusters) + .build(); + + pulsarAdmin.tenants().updateTenant(tenant, tenantInfo); + + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("adminRoles", adminRoles); + result.put("allowedClusters", allowedClusters); + result.put("updated", true); + + return createSuccessResult("Tenant updated successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to update tenant", e); + return createErrorResult( + "Failed to update tenant", List.of(safeErrorMessage(e))); + } + }) + .build()); + } + + private void registerGetTenantStats(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-tenant-stats", + "Get basic stats for a specific Pulsar tenant", + """ { "type": "object", "properties": { @@ -398,77 +414,79 @@ private void registerGetTenantStats(McpSyncServer mcpServer) { }, "required": ["tenant"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String tenant = getRequiredStringParam(request.arguments(), "tenant"); - - List namespaces0 = pulsarAdmin.namespaces().getNamespaces(tenant); - List namespaces = (namespaces0 == null) ? List.of() : namespaces0; - - - int totalTopics = 0; - Map namespaceTopicCounts = new HashMap<>(); - - for (String namespace : namespaces) { - try { - List topics0 = pulsarAdmin.topics().getList(namespace); - List topics = (topics0 == null) ? List.of() : topics0; - namespaceTopicCounts.put(namespace, topics.size()); - totalTopics += topics.size(); - } catch (Exception e) { - LOGGER.warn("Failed to get topics for namespace {}", namespace, e); - namespaceTopicCounts.put(namespace, 0); - } - } - - Map result = new HashMap<>(); - result.put("tenant", tenant); - result.put("namespaceCount", namespaces.size()); - result.put("namespaces", namespaces); - result.put("totalTopics", totalTopics); - result.put("topicCounts", namespaceTopicCounts); - - return createSuccessResult("Tenant stats retrieved successfully", result); - - - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get tenant stats", e); - return createErrorResult("Failed to get tenant stats", List.of(safeErrorMessage(e))); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String tenant = getRequiredStringParam(request.arguments(), "tenant"); + + List namespaces0 = pulsarAdmin.namespaces().getNamespaces(tenant); + List namespaces = (namespaces0 == null) ? List.of() : namespaces0; + + int totalTopics = 0; + Map namespaceTopicCounts = new HashMap<>(); + + for (String namespace : namespaces) { + try { + List topics0 = pulsarAdmin.topics().getList(namespace); + List topics = (topics0 == null) ? List.of() : topics0; + namespaceTopicCounts.put(namespace, topics.size()); + totalTopics += topics.size(); + } catch (Exception e) { + LOGGER.warn("Failed to get topics for namespace {}", namespace, e); + namespaceTopicCounts.put(namespace, 0); + } } - }).build()); - } - private Set getSetParam(Map args, String key) { - Object obj = args.get(key); - if (obj instanceof List list) { - return list.stream().filter(Objects::nonNull).map(String::valueOf).collect(Collectors.toSet()); - } - return Set.of(); + Map result = new HashMap<>(); + result.put("tenant", tenant); + result.put("namespaceCount", namespaces.size()); + result.put("namespaces", namespaces); + result.put("totalTopics", totalTopics); + result.put("topicCounts", namespaceTopicCounts); + + return createSuccessResult("Tenant stats retrieved successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get tenant stats", e); + return createErrorResult( + "Failed to get tenant stats", List.of(safeErrorMessage(e))); + } + }) + .build()); + } + + private Set getSetParam(Map args, String key) { + Object obj = args.get(key); + if (obj instanceof List list) { + return list.stream() + .filter(Objects::nonNull) + .map(String::valueOf) + .collect(Collectors.toSet()); } - - private Set getSetParamOrDefault(Map args, String key, Set defaultValue) { - Set value = getSetParam(args, key); - return value.isEmpty() ? defaultValue : value; - } - - private String safeErrorMessage(Exception e) { - if (e.getMessage() == null || e.getMessage().isBlank()) { - return "Unknown error occurred"; - } - return e.getMessage().split("\n")[0].trim(); - } - - private boolean isSystemTenant(String tenant) { - return tenant.equals("pulsar") - || tenant.equals("public") - || tenant.equals("sample"); + return Set.of(); + } + + private Set getSetParamOrDefault( + Map args, String key, Set defaultValue) { + Set value = getSetParam(args, key); + return value.isEmpty() ? defaultValue : value; + } + + private String safeErrorMessage(Exception e) { + if (e.getMessage() == null || e.getMessage().isBlank()) { + return "Unknown error occurred"; } + return e.getMessage().split("\n")[0].trim(); + } + private boolean isSystemTenant(String tenant) { + return tenant.equals("pulsar") || tenant.equals("public") || tenant.equals("sample"); + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/TopicTools.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/TopicTools.java index 9e2ff5d..6951942 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/TopicTools.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/tools/TopicTools.java @@ -25,32 +25,33 @@ public class TopicTools extends BasePulsarTools { - public TopicTools(PulsarAdmin pulsarAdmin) { - super(pulsarAdmin); - } - - public void registerTools(McpSyncServer mcpServer) { - registerListTopics(mcpServer); - registerCreateTopics(mcpServer); - registerDeleteTopics(mcpServer); - registerGetTopicStats(mcpServer); - registerGetTopicMetadata(mcpServer); - registerUpdateTopicPartitions(mcpServer); - registerCompactTopic(mcpServer); - registerUnloadTopic(mcpServer); - registerGetTopicBacklog(mcpServer); - registerExpireTopicMessages(mcpServer); - registerPeekTopicMessages(mcpServer); - registerResetTopicCursor(mcpServer); - registerGetTopicInternalStats(mcpServer); - registerGetPartitionedMetadata(mcpServer); - } - - private void registerListTopics(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "list-topics", - "List all topics under a specific namespace", - """ + public TopicTools(PulsarAdmin pulsarAdmin) { + super(pulsarAdmin); + } + + public void registerTools(McpSyncServer mcpServer) { + registerListTopics(mcpServer); + registerCreateTopics(mcpServer); + registerDeleteTopics(mcpServer); + registerGetTopicStats(mcpServer); + registerGetTopicMetadata(mcpServer); + registerUpdateTopicPartitions(mcpServer); + registerCompactTopic(mcpServer); + registerUnloadTopic(mcpServer); + registerGetTopicBacklog(mcpServer); + registerExpireTopicMessages(mcpServer); + registerPeekTopicMessages(mcpServer); + registerResetTopicCursor(mcpServer); + registerGetTopicInternalStats(mcpServer); + registerGetPartitionedMetadata(mcpServer); + } + + private void registerListTopics(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "list-topics", + "List all topics under a specific namespace", + """ { "type": "object", "properties": { @@ -66,40 +67,43 @@ private void registerListTopics(McpSyncServer mcpServer) { }, "required": [] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String namespace = resolveNamespace(request.arguments()); - - List topics = pulsarAdmin.topics().getList(namespace); - if (topics == null) { - topics = List.of(); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String namespace = resolveNamespace(request.arguments()); - Map result = new HashMap<>(); - result.put("namespace", namespace); - result.put("topics", topics); - result.put("count", topics.size()); - - return createSuccessResult("Topics listed successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to list topics", e); - return createErrorResult("Failed to list topics: " + e.getMessage()); + List topics = pulsarAdmin.topics().getList(namespace); + if (topics == null) { + topics = List.of(); } - }).build()); - } - - private void registerCreateTopics(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "create-topics", - "Create one or more topics under a specific namespace", - """ + + Map result = new HashMap<>(); + result.put("namespace", namespace); + result.put("topics", topics); + result.put("count", topics.size()); + + return createSuccessResult("Topics listed successfully", result); + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to list topics", e); + return createErrorResult("Failed to list topics: " + e.getMessage()); + } + }) + .build()); + } + + private void registerCreateTopics(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "create-topics", + "Create one or more topics under a specific namespace", + """ { "type": "object", "properties": { @@ -125,52 +129,55 @@ private void registerCreateTopics(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - - boolean persistent = getBooleanParam(request.arguments(), "persistent", true); - if (topic.startsWith("persistent://") && !persistent) { - topic = "non-" + topic; - } else if (topic.startsWith("non-persistent://") && persistent) { - topic = topic.replaceFirst("non-persistent://", "persistent://"); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + + boolean persistent = getBooleanParam(request.arguments(), "persistent", true); + if (topic.startsWith("persistent://") && !persistent) { + topic = "non-" + topic; + } else if (topic.startsWith("non-persistent://") && persistent) { + topic = topic.replaceFirst("non-persistent://", "persistent://"); + } - Integer partitions = getIntParam(request.arguments(), "partitions", 0); + Integer partitions = getIntParam(request.arguments(), "partitions", 0); - if (partitions > 0) { - pulsarAdmin.topics().createPartitionedTopic(topic, partitions); - } else { - pulsarAdmin.topics().createNonPartitionedTopic(topic); - } + if (partitions > 0) { + pulsarAdmin.topics().createPartitionedTopic(topic, partitions); + } else { + pulsarAdmin.topics().createNonPartitionedTopic(topic); + } - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("created", true); - result.put("partitions", partitions); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("created", true); + result.put("partitions", partitions); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Topics created successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to create topics", e); - return createErrorResult("Failed to create topics: " + e.getMessage()); - } - }).build()); - } - - private void registerDeleteTopics(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "delete-topics", - "Delete one or more topics", - """ + return createSuccessResult("Topics created successfully", result); + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to create topics", e); + return createErrorResult("Failed to create topics: " + e.getMessage()); + } + }) + .build()); + } + + private void registerDeleteTopics(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "delete-topics", + "Delete one or more topics", + """ { "type": "object", "properties": { @@ -201,46 +208,49 @@ private void registerDeleteTopics(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - Boolean force = getBooleanParam(request.arguments(), "force", false); - - var metadata = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - if (metadata != null && metadata.partitions > 0) { - pulsarAdmin.topics().deletePartitionedTopic(topic, force); - } else { - pulsarAdmin.topics().delete(topic, force); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + Boolean force = getBooleanParam(request.arguments(), "force", false); + + var metadata = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + if (metadata != null && metadata.partitions > 0) { + pulsarAdmin.topics().deletePartitionedTopic(topic, force); + } else { + pulsarAdmin.topics().delete(topic, force); + } - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("deleted", true); - result.put("force", force); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("deleted", true); + result.put("force", force); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Topic deleted successfully", result); + return createSuccessResult("Topic deleted successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to delete topic", e); - return createErrorResult("Failed to delete topic: " + e.getMessage()); - } - }).build()); - } - - private void registerGetTopicStats(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-topic-stats", - "Get statistics for a specific topic", - """ + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to delete topic", e); + return createErrorResult("Failed to delete topic: " + e.getMessage()); + } + }) + .build()); + } + + private void registerGetTopicStats(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-topic-stats", + "Get statistics for a specific topic", + """ { "type": "object", "properties": { @@ -251,53 +261,56 @@ private void registerGetTopicStats(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - Map result = new HashMap<>(); - result.put("topic", topic); - - if (meta != null && meta.partitions > 0) { - var ps = pulsarAdmin.topics().getPartitionedStats(topic, false); - result.put("msgRateIn", ps.getMsgRateIn()); - result.put("msgRateOut", ps.getMsgRateOut()); - result.put("msgThroughputIn", ps.getMsgThroughputIn()); - result.put("msgThroughputOut", ps.getMsgThroughputOut()); - result.put("storageSize", ps.getStorageSize()); - } else { - TopicStats stats = pulsarAdmin.topics().getStats(topic); - result.put("msgRateIn", stats.getMsgRateIn()); - result.put("msgRateOut", stats.getMsgRateOut()); - result.put("msgThroughputIn", stats.getMsgThroughputIn()); - result.put("msgThroughputOut", stats.getMsgThroughputOut()); - result.put("storageSize", stats.getStorageSize()); - result.put("subscriptions", stats.getSubscriptions()); // 可能为 null,直接透传 - result.put("publishers", stats.getPublishers()); - result.put("replication", stats.getReplication()); - } - - return createSuccessResult("Topic stats retrieved successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get topic stats", e); - return createErrorResult("Failed to get topic stats: " + e.getMessage()); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + Map result = new HashMap<>(); + result.put("topic", topic); + + if (meta != null && meta.partitions > 0) { + var ps = pulsarAdmin.topics().getPartitionedStats(topic, false); + result.put("msgRateIn", ps.getMsgRateIn()); + result.put("msgRateOut", ps.getMsgRateOut()); + result.put("msgThroughputIn", ps.getMsgThroughputIn()); + result.put("msgThroughputOut", ps.getMsgThroughputOut()); + result.put("storageSize", ps.getStorageSize()); + } else { + TopicStats stats = pulsarAdmin.topics().getStats(topic); + result.put("msgRateIn", stats.getMsgRateIn()); + result.put("msgRateOut", stats.getMsgRateOut()); + result.put("msgThroughputIn", stats.getMsgThroughputIn()); + result.put("msgThroughputOut", stats.getMsgThroughputOut()); + result.put("storageSize", stats.getStorageSize()); + result.put("subscriptions", stats.getSubscriptions()); // 可能为 null,直接透传 + result.put("publishers", stats.getPublishers()); + result.put("replication", stats.getReplication()); } - }).build()); - } - - private void registerGetTopicMetadata(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-topic-metadata", - "Get metadata information for a specific topic", - """ + + return createSuccessResult("Topic stats retrieved successfully", result); + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get topic stats", e); + return createErrorResult("Failed to get topic stats: " + e.getMessage()); + } + }) + .build()); + } + + private void registerGetTopicMetadata(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-topic-metadata", + "Get metadata information for a specific topic", + """ { "type": "object", "properties": { @@ -308,38 +321,41 @@ private void registerGetTopicMetadata(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - - var metadata = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - int partitions = (metadata == null) ? 0 : metadata.partitions; - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("partitions", partitions); - result.put("isPartitioned", partitions > 0); - - return createSuccessResult("Topic metadata fetched successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get topic metadata", e); - return createErrorResult("Failed to get topic metadata: " + e.getMessage()); - } - }).build()); - } - - private void registerUpdateTopicPartitions(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "update-topic-partitions", - "Update the number of partitions for a partitioned Pulsar topic", - """ + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + + var metadata = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + int partitions = (metadata == null) ? 0 : metadata.partitions; + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("partitions", partitions); + result.put("isPartitioned", partitions > 0); + + return createSuccessResult("Topic metadata fetched successfully", result); + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get topic metadata", e); + return createErrorResult("Failed to get topic metadata: " + e.getMessage()); + } + }) + .build()); + } + + private void registerUpdateTopicPartitions(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "update-topic-partitions", + "Update the number of partitions for a partitioned Pulsar topic", + """ { "type": "object", "properties": { @@ -360,64 +376,69 @@ private void registerUpdateTopicPartitions(McpSyncServer mcpServer) { }, "required": ["topic", "partitions"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - Integer partitions = getIntParam(request.arguments(), "partitions", 0); - - if (partitions <= 0) { - return createErrorResult("Invalid partitions parameter: " - + "must be at least 1"); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + Integer partitions = getIntParam(request.arguments(), "partitions", 0); + + if (partitions <= 0) { + return createErrorResult( + "Invalid partitions parameter: " + "must be at least 1"); + } - var currentMetadata = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - int currentPartitions = currentMetadata.partitions; + var currentMetadata = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + int currentPartitions = currentMetadata.partitions; - if (currentPartitions == 0) { - return createErrorResult("Topic is not partitioned. " - + "Use create-partitioned-topic to create a partitioned topic."); - } + if (currentPartitions == 0) { + return createErrorResult( + "Topic is not partitioned. " + + "Use create-partitioned-topic to create a partitioned topic."); + } - if (partitions <= currentPartitions) { - return createErrorResult("New partition count (" - + partitions - + ") must be greater than current partition count (" - + currentPartitions - + ")"); - } + if (partitions <= currentPartitions) { + return createErrorResult( + "New partition count (" + + partitions + + ") must be greater than current partition count (" + + currentPartitions + + ")"); + } - pulsarAdmin.topics().updatePartitionedTopic(topic, partitions); + pulsarAdmin.topics().updatePartitionedTopic(topic, partitions); - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("previousPartitions", currentPartitions); - result.put("newPartitions", partitions); - result.put("updated", true); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("previousPartitions", currentPartitions); + result.put("newPartitions", partitions); + result.put("updated", true); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Topic partitions updated successfully", result); + return createSuccessResult("Topic partitions updated successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid input parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to update topic partitions", e); - return createErrorResult("Failed to update topic partitions: " + e.getMessage()); - } + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid input parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to update topic partitions", e); + return createErrorResult( + "Failed to update topic partitions: " + e.getMessage()); + } }) - .build()); - } - - private void registerCompactTopic(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "compact-topic", - "Compact a specified topic", - """ + .build()); + } + + private void registerCompactTopic(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "compact-topic", + "Compact a specified topic", + """ { "type": "object", "properties": { @@ -428,43 +449,47 @@ private void registerCompactTopic(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - if (meta != null && meta.partitions > 0) { - for (int i = 0; i < meta.partitions; i++) { - pulsarAdmin.topics().triggerCompaction(topic + "-partition-" + i); - } - } else { - pulsarAdmin.topics().triggerCompaction(topic); - } - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("compactionTriggered", true); - addTopicBreakdown(result, topic); - - return createSuccessResult("Compaction triggered successfully for topic: ", result); - - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to compact topic", e); - return createErrorResult("Failed to compact topic: " + e.getMessage()); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + if (meta != null && meta.partitions > 0) { + for (int i = 0; i < meta.partitions; i++) { + pulsarAdmin.topics().triggerCompaction(topic + "-partition-" + i); + } + } else { + pulsarAdmin.topics().triggerCompaction(topic); } - }).build()); - } - - private void registerUnloadTopic(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "unload-topic", - "Unload a specified topic", - """ + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("compactionTriggered", true); + addTopicBreakdown(result, topic); + + return createSuccessResult( + "Compaction triggered successfully for topic: ", result); + + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to compact topic", e); + return createErrorResult("Failed to compact topic: " + e.getMessage()); + } + }) + .build()); + } + + private void registerUnloadTopic(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "unload-topic", + "Unload a specified topic", + """ { "type": "object", "properties": { @@ -475,46 +500,49 @@ private void registerUnloadTopic(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - if (meta != null && meta.partitions > 0) { - for (int i = 0; i < meta.partitions; i++) { - pulsarAdmin.topics().unload(topic + "-partition-" + i); - } - } else { - pulsarAdmin.topics().unload(topic); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + if (meta != null && meta.partitions > 0) { + for (int i = 0; i < meta.partitions; i++) { + pulsarAdmin.topics().unload(topic + "-partition-" + i); + } + } else { + pulsarAdmin.topics().unload(topic); + } - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("unloaded", true); + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("unloaded", true); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Topic unloaded successfully: ", result); + return createSuccessResult("Topic unloaded successfully: ", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to unload topic", e); - return createErrorResult("Failed to unload topic: " + e.getMessage()); - } - }).build()); - } - - private void registerGetTopicBacklog(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-topic-backlog", - "Get the backlog size of a specified topic", - """ + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to unload topic", e); + return createErrorResult("Failed to unload topic: " + e.getMessage()); + } + }) + .build()); + } + + private void registerGetTopicBacklog(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-topic-backlog", + "Get the backlog size of a specified topic", + """ { "type": "object", "properties": { @@ -525,60 +553,63 @@ private void registerGetTopicBacklog(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); + """); - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); - TopicStats stats = pulsarAdmin.topics().getStats(topic); + TopicStats stats = pulsarAdmin.topics().getStats(topic); - Map result = new HashMap<>(); - result.put("topic", topic); + Map result = new HashMap<>(); + result.put("topic", topic); - Map subscriptionBacklogs = new HashMap<>(); - long totalBacklog = 0; + Map subscriptionBacklogs = new HashMap<>(); + long totalBacklog = 0; - for (var entry : stats.getSubscriptions().entrySet()) { - String subscriptionName = entry.getKey(); - var subscriptionStats = entry.getValue(); + for (var entry : stats.getSubscriptions().entrySet()) { + String subscriptionName = entry.getKey(); + var subscriptionStats = entry.getValue(); - long backlog = subscriptionStats.getBacklogSize(); - totalBacklog += backlog; + long backlog = subscriptionStats.getBacklogSize(); + totalBacklog += backlog; - Map subInfo = new HashMap<>(); - subInfo.put("backlog", backlog); - subInfo.put("type", subscriptionStats.getType()); - subInfo.put("consumers", subscriptionStats.getConsumers()); + Map subInfo = new HashMap<>(); + subInfo.put("backlog", backlog); + subInfo.put("type", subscriptionStats.getType()); + subInfo.put("consumers", subscriptionStats.getConsumers()); - subscriptionBacklogs.put(subscriptionName, subInfo); - } + subscriptionBacklogs.put(subscriptionName, subInfo); + } - result.put("totalBacklog", totalBacklog); - result.put("subscriptionBacklogs", subscriptionBacklogs); - result.put("subscriptionCount", stats.getSubscriptions().size()); + result.put("totalBacklog", totalBacklog); + result.put("subscriptionBacklogs", subscriptionBacklogs); + result.put("subscriptionCount", stats.getSubscriptions().size()); - addTopicBreakdown(result, topic); + addTopicBreakdown(result, topic); - return createSuccessResult("Topic backlog fetched successfully", result); + return createSuccessResult("Topic backlog fetched successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get topic backlog", e); - return createErrorResult("Failed to get topic backlog: " + e.getMessage()); - } - }).build()); - } - - private void registerExpireTopicMessages(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "expire-topic-messages", - "Expire messages for all subscriptions on a topic older than a given time", - """ + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get topic backlog", e); + return createErrorResult("Failed to get topic backlog: " + e.getMessage()); + } + }) + .build()); + } + + private void registerExpireTopicMessages(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "expire-topic-messages", + "Expire messages for all subscriptions on a topic older than a given time", + """ { "type": "object", "properties": { @@ -598,55 +629,64 @@ private void registerExpireTopicMessages(McpSyncServer mcpServer) { }, "required": ["topic", "subscriptionName"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscriptionName = getRequiredStringParam(request.arguments(), "subscriptionName"); - Integer expireTimeInSeconds = getIntParam(request.arguments(), "expireTimeInSeconds", 0); - - if (expireTimeInSeconds == null || expireTimeInSeconds <= 0) { - return createErrorResult("expireTimeInSeconds must be > 0"); - } - - var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - if (meta != null && meta.partitions > 0) { - for (int i = 0; i < meta.partitions; i++) { - pulsarAdmin.topics().expireMessages(topic - + "-partition-" - + i, subscriptionName, expireTimeInSeconds); - } - } else { - pulsarAdmin.topics().expireMessages(topic, subscriptionName, expireTimeInSeconds); - } - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("subscriptionName", subscriptionName); - result.put("expireTimeInSeconds", expireTimeInSeconds); - result.put("expired", true); - - addTopicBreakdown(result, topic); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscriptionName = + getRequiredStringParam(request.arguments(), "subscriptionName"); + Integer expireTimeInSeconds = + getIntParam(request.arguments(), "expireTimeInSeconds", 0); + + if (expireTimeInSeconds == null || expireTimeInSeconds <= 0) { + return createErrorResult("expireTimeInSeconds must be > 0"); + } - return createSuccessResult("Expired messages on topic successfully", result); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to expire messages on topic", e); - return createErrorResult("Failed to expire messages on topic: " + e.getMessage()); + var meta = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + if (meta != null && meta.partitions > 0) { + for (int i = 0; i < meta.partitions; i++) { + pulsarAdmin + .topics() + .expireMessages( + topic + "-partition-" + i, subscriptionName, expireTimeInSeconds); + } + } else { + pulsarAdmin + .topics() + .expireMessages(topic, subscriptionName, expireTimeInSeconds); } - }).build()); - } - - private void registerPeekTopicMessages(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "peek-topic-messages", - "Peek messages from a subscription of a topic", - """ + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("subscriptionName", subscriptionName); + result.put("expireTimeInSeconds", expireTimeInSeconds); + result.put("expired", true); + + addTopicBreakdown(result, topic); + + return createSuccessResult("Expired messages on topic successfully", result); + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to expire messages on topic", e); + return createErrorResult( + "Failed to expire messages on topic: " + e.getMessage()); + } + }) + .build()); + } + + private void registerPeekTopicMessages(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "peek-topic-messages", + "Peek messages from a subscription of a topic", + """ { "type": "object", "properties": { @@ -666,66 +706,73 @@ private void registerPeekTopicMessages(McpSyncServer mcpServer) { }, "required": ["topic", "subscription", "count"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscription = getRequiredStringParam(request.arguments(), "subscription"); - Integer count = getIntParam(request.arguments(), "count", 1); - - if (count == null || count <= 0) { - return createErrorResult("count must be >= 1"); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscription = + getRequiredStringParam(request.arguments(), "subscription"); + Integer count = getIntParam(request.arguments(), "count", 1); + + if (count == null || count <= 0) { + return createErrorResult("count must be >= 1"); + } - var raw = pulsarAdmin.topics().peekMessages(topic, subscription, count); - List> messages = new ArrayList<>(); - if (raw != null) { - for (var msg : raw) { - Map m = new HashMap<>(); - try { - m.put("messageId", String.valueOf(msg.getMessageId())); - m.put("publishTime", msg.getPublishTime()); - m.put("eventTime", msg.getEventTime()); - m.put("key", msg.getKey()); - m.put("properties", msg.getProperties()); - byte[] payload = msg.getData(); - m.put("payloadBase64", payload == null - ? null : java.util.Base64.getEncoder().encodeToString(payload)); - } catch (Throwable t) { - m.put("error", "Failed to materialize message: " + t.getMessage()); - } - messages.add(m); - } - } + var raw = pulsarAdmin.topics().peekMessages(topic, subscription, count); + List> messages = new ArrayList<>(); + if (raw != null) { + for (var msg : raw) { + Map m = new HashMap<>(); + try { + m.put("messageId", String.valueOf(msg.getMessageId())); + m.put("publishTime", msg.getPublishTime()); + m.put("eventTime", msg.getEventTime()); + m.put("key", msg.getKey()); + m.put("properties", msg.getProperties()); + byte[] payload = msg.getData(); + m.put( + "payloadBase64", + payload == null + ? null + : java.util.Base64.getEncoder().encodeToString(payload)); + } catch (Throwable t) { + m.put("error", "Failed to materialize message: " + t.getMessage()); + } + messages.add(m); + } + } - Map results = new HashMap<>(); - results.put("topic", topic); - results.put("subscription", subscription); - results.put("count", count); - results.put("messages", messages); + Map results = new HashMap<>(); + results.put("topic", topic); + results.put("subscription", subscription); + results.put("count", count); + results.put("messages", messages); - addTopicBreakdown(results, topic); + addTopicBreakdown(results, topic); - return createSuccessResult("Messages peeked successfully", results); + return createSuccessResult("Messages peeked successfully", results); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Error peeking topic messages", e); - return createErrorResult("Failed to peek messages: " + e.getMessage()); - } - }).build()); - } - - private void registerResetTopicCursor(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "reset-topic-cursor", - "Reset the subscription cursor to a specific timestamp", - """ + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Error peeking topic messages", e); + return createErrorResult("Failed to peek messages: " + e.getMessage()); + } + }) + .build()); + } + + private void registerResetTopicCursor(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "reset-topic-cursor", + "Reset the subscription cursor to a specific timestamp", + """ { "type": "object", "properties": { @@ -745,50 +792,54 @@ private void registerResetTopicCursor(McpSyncServer mcpServer) { }, "required": ["topic", "subscription"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - String subscription = getRequiredStringParam(request.arguments(), "subscription"); - Long timestamp = getLongParam(request.arguments(), "timestamp", 0L); - - if (timestamp == null) { - timestamp = 0L; - } - - if (timestamp <= 0L){ - pulsarAdmin.topics().resetCursor(topic, subscription, 0L); - } else { - pulsarAdmin.topics().resetCursor(topic, subscription, timestamp); - } - - Map response = new HashMap<>(); - response.put("topic", topic); - response.put("subscription", subscription); - response.put("timestamp", timestamp); - response.put("reset", true); - - addTopicBreakdown(response, topic); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + String subscription = + getRequiredStringParam(request.arguments(), "subscription"); + Long timestamp = getLongParam(request.arguments(), "timestamp", 0L); + + if (timestamp == null) { + timestamp = 0L; + } - return createSuccessResult("Cursor reset successfully", response); - } catch (IllegalArgumentException e) { - return createErrorResult(e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to reset topic cursor", e); - return createErrorResult("Failed to reset topic cursor: " + e.getMessage()); + if (timestamp <= 0L) { + pulsarAdmin.topics().resetCursor(topic, subscription, 0L); + } else { + pulsarAdmin.topics().resetCursor(topic, subscription, timestamp); } - }).build()); - } - - private void registerGetTopicInternalStats(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-internal-stats", - "Get internal stats of a Pulsar topic", - """ + + Map response = new HashMap<>(); + response.put("topic", topic); + response.put("subscription", subscription); + response.put("timestamp", timestamp); + response.put("reset", true); + + addTopicBreakdown(response, topic); + + return createSuccessResult("Cursor reset successfully", response); + } catch (IllegalArgumentException e) { + return createErrorResult(e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to reset topic cursor", e); + return createErrorResult("Failed to reset topic cursor: " + e.getMessage()); + } + }) + .build()); + } + + private void registerGetTopicInternalStats(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-internal-stats", + "Get internal stats of a Pulsar topic", + """ { "type": "object", "properties": { @@ -799,69 +850,74 @@ private void registerGetTopicInternalStats(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - - var internalStats = pulsarAdmin.topics().getInternalStats(topic); - - Map result = new HashMap<>(); - result.put("topic", topic); - result.put("entriesAddedCounter", internalStats.entriesAddedCounter); - result.put("numberOfEntries", internalStats.numberOfEntries); - result.put("totalSize", internalStats.totalSize); - result.put("currentLedgerEntries", internalStats.currentLedgerEntries); - result.put("currentLedgerSize", internalStats.currentLedgerSize); - result.put("lastLedgerCreatedTimestamp", internalStats.lastLedgerCreatedTimestamp); - result.put("lastLedgerCreationFailureTimestamp", - internalStats.lastLedgerCreationFailureTimestamp); - result.put("waitingCursorCount", internalStats.waitingCursorsCount); - result.put("pendingAddEntriesCount", internalStats.pendingAddEntriesCount); - - if (internalStats.ledgers != null && !internalStats.ledgers.isEmpty()) { - result.put("ledgers", internalStats.ledgers); - result.put("ledgerCount", internalStats.ledgers.size()); - } + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + + var internalStats = pulsarAdmin.topics().getInternalStats(topic); + + Map result = new HashMap<>(); + result.put("topic", topic); + result.put("entriesAddedCounter", internalStats.entriesAddedCounter); + result.put("numberOfEntries", internalStats.numberOfEntries); + result.put("totalSize", internalStats.totalSize); + result.put("currentLedgerEntries", internalStats.currentLedgerEntries); + result.put("currentLedgerSize", internalStats.currentLedgerSize); + result.put( + "lastLedgerCreatedTimestamp", internalStats.lastLedgerCreatedTimestamp); + result.put( + "lastLedgerCreationFailureTimestamp", + internalStats.lastLedgerCreationFailureTimestamp); + result.put("waitingCursorCount", internalStats.waitingCursorsCount); + result.put("pendingAddEntriesCount", internalStats.pendingAddEntriesCount); + + if (internalStats.ledgers != null && !internalStats.ledgers.isEmpty()) { + result.put("ledgers", internalStats.ledgers); + result.put("ledgerCount", internalStats.ledgers.size()); + } - if (internalStats.cursors != null && !internalStats.cursors.isEmpty()) { - result.put("cursorCount", internalStats.cursors.size()); - Map cursors = new HashMap<>(); - internalStats.cursors.forEach((name, cursor) -> { - Map cursorInfo = new HashMap<>(); - cursorInfo.put("markDeletePosition", cursor.markDeletePosition); - cursorInfo.put("readPosition", cursor.readPosition); - cursorInfo.put("waitingReadOp", cursor.waitingReadOp); - cursorInfo.put("pendingReadOps", cursor.pendingReadOps); - cursorInfo.put("messagesConsumedCounter", - cursor.messagesConsumedCounter); - cursorInfo.put("cursorLedger", cursor.cursorLedger); - cursors.put(name, cursorInfo); - }); - result.put("cursors", cursors); - } - addTopicBreakdown(result, topic); - return createSuccessResult("Internal stats retrieved successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid input parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get internal stats", e); - return createErrorResult("Failed to get internal stats: " + e.getMessage()); + if (internalStats.cursors != null && !internalStats.cursors.isEmpty()) { + result.put("cursorCount", internalStats.cursors.size()); + Map cursors = new HashMap<>(); + internalStats.cursors.forEach( + (name, cursor) -> { + Map cursorInfo = new HashMap<>(); + cursorInfo.put("markDeletePosition", cursor.markDeletePosition); + cursorInfo.put("readPosition", cursor.readPosition); + cursorInfo.put("waitingReadOp", cursor.waitingReadOp); + cursorInfo.put("pendingReadOps", cursor.pendingReadOps); + cursorInfo.put( + "messagesConsumedCounter", cursor.messagesConsumedCounter); + cursorInfo.put("cursorLedger", cursor.cursorLedger); + cursors.put(name, cursorInfo); + }); + result.put("cursors", cursors); } + addTopicBreakdown(result, topic); + return createSuccessResult("Internal stats retrieved successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid input parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get internal stats", e); + return createErrorResult("Failed to get internal stats: " + e.getMessage()); + } }) - .build()); - } - - private void registerGetPartitionedMetadata(McpSyncServer mcpServer) { - McpSchema.Tool tool = createTool( - "get-partitioned-metadata", - "Get partitioned metadata of a Pulsar topic", - """ + .build()); + } + + private void registerGetPartitionedMetadata(McpSyncServer mcpServer) { + McpSchema.Tool tool = + createTool( + "get-partitioned-metadata", + "Get partitioned metadata of a Pulsar topic", + """ { "type": "object", "properties": { @@ -872,79 +928,85 @@ private void registerGetPartitionedMetadata(McpSyncServer mcpServer) { }, "required": ["topic"] } - """ - ); - - mcpServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(tool) - .callHandler((exchange, request) -> { - try { - String topic = buildFullTopicName(request.arguments()); - - var partitionedMetadata = pulsarAdmin.topics().getPartitionedTopicMetadata(topic); - - Map result = new HashMap<>(); - result.put("topic", topic); - - int partitions = (partitionedMetadata == null) ? 0 : partitionedMetadata.partitions; - result.put("partitions", partitions); - result.put("isPartitioned", partitions > 0); - - if (partitions > 0) { - double msgRateIn = 0.0, msgRateOut = 0.0, msgThroughputIn = 0.0, msgThroughputOut = 0.0; - long storageSize = 0L; - Map partitionInfo = new HashMap<>(); - - for (int i = 0; i < partitions; i++) { - String p = topic + "-partition-" + i; - try { - TopicStats s = pulsarAdmin.topics().getStats(p); - if (s == null) { - continue; - } - msgRateIn += s.getMsgRateIn(); - msgRateOut += s.getMsgRateOut(); - msgThroughputIn += s.getMsgThroughputIn(); - msgThroughputOut += s.getMsgThroughputOut(); - storageSize += s.getStorageSize(); - - Map partStats = new HashMap<>(); - partStats.put("msgRateIn", s.getMsgRateIn()); - partStats.put("msgRateOut", s.getMsgRateOut()); - partStats.put("storageSize", s.getStorageSize()); - int subCount = (s.getSubscriptions() == null) - ? 0 : s.getSubscriptions().size(); - partStats.put("subscriptionCount", subCount); - partitionInfo.put(p, partStats); - } catch (Exception ex) { - Map err = new HashMap<>(); - err.put("error", "Failed to get stats: " + ex.getMessage()); - partitionInfo.put(p, err); - } - } - - result.put("msgRateIn", msgRateIn); - result.put("msgRateOut", msgRateOut); - result.put("msgThroughputIn", msgThroughputIn); - result.put("msgThroughputOut", msgThroughputOut); - result.put("storageSize", storageSize); - result.put("partitionStats", partitionInfo); - } else { - result.put("message", "Topic is not partitioned"); + """); + + mcpServer.addTool( + McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler( + (exchange, request) -> { + try { + String topic = buildFullTopicName(request.arguments()); + + var partitionedMetadata = + pulsarAdmin.topics().getPartitionedTopicMetadata(topic); + + Map result = new HashMap<>(); + result.put("topic", topic); + + int partitions = + (partitionedMetadata == null) ? 0 : partitionedMetadata.partitions; + result.put("partitions", partitions); + result.put("isPartitioned", partitions > 0); + + if (partitions > 0) { + double msgRateIn = 0.0, + msgRateOut = 0.0, + msgThroughputIn = 0.0, + msgThroughputOut = 0.0; + long storageSize = 0L; + Map partitionInfo = new HashMap<>(); + + for (int i = 0; i < partitions; i++) { + String p = topic + "-partition-" + i; + try { + TopicStats s = pulsarAdmin.topics().getStats(p); + if (s == null) { + continue; + } + msgRateIn += s.getMsgRateIn(); + msgRateOut += s.getMsgRateOut(); + msgThroughputIn += s.getMsgThroughputIn(); + msgThroughputOut += s.getMsgThroughputOut(); + storageSize += s.getStorageSize(); + + Map partStats = new HashMap<>(); + partStats.put("msgRateIn", s.getMsgRateIn()); + partStats.put("msgRateOut", s.getMsgRateOut()); + partStats.put("storageSize", s.getStorageSize()); + int subCount = + (s.getSubscriptions() == null) ? 0 : s.getSubscriptions().size(); + partStats.put("subscriptionCount", subCount); + partitionInfo.put(p, partStats); + } catch (Exception ex) { + Map err = new HashMap<>(); + err.put("error", "Failed to get stats: " + ex.getMessage()); + partitionInfo.put(p, err); } + } - addTopicBreakdown(result, topic); - return createSuccessResult("Partitioned metadata retrieved successfully", result); - - } catch (IllegalArgumentException e) { - return createErrorResult("Invalid input parameter: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Failed to get partitioned metadata", e); - return createErrorResult("Failed to get partitioned metadata: " + e.getMessage()); + result.put("msgRateIn", msgRateIn); + result.put("msgRateOut", msgRateOut); + result.put("msgThroughputIn", msgThroughputIn); + result.put("msgThroughputOut", msgThroughputOut); + result.put("storageSize", storageSize); + result.put("partitionStats", partitionInfo); + } else { + result.put("message", "Topic is not partitioned"); } - }) - .build()); - } + addTopicBreakdown(result, topic); + return createSuccessResult( + "Partitioned metadata retrieved successfully", result); + + } catch (IllegalArgumentException e) { + return createErrorResult("Invalid input parameter: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Failed to get partitioned metadata", e); + return createErrorResult( + "Failed to get partitioned metadata: " + e.getMessage()); + } + }) + .build()); + } } - diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/AbstractMCPServer.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/AbstractMCPServer.java index f3cfd7b..4231023 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/AbstractMCPServer.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/AbstractMCPServer.java @@ -31,198 +31,214 @@ public abstract class AbstractMCPServer { - protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractMCPServer.class); + protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractMCPServer.class); - protected PulsarClientManager pulsarClientManager; - protected static PulsarAdmin pulsarAdmin; - protected static PulsarClient pulsarClient; + protected PulsarClientManager pulsarClientManager; + protected static PulsarAdmin pulsarAdmin; + protected static PulsarClient pulsarClient; - public void injectClientManager(PulsarClientManager manager) { - this.pulsarClientManager = manager; + public void injectClientManager(PulsarClientManager manager) { + this.pulsarClientManager = manager; + } + + public void initializePulsar() { + if (this.pulsarClientManager == null) { + this.pulsarClientManager = new PulsarClientManager(); } + this.pulsarClientManager.initialize(); - public void initializePulsar() { - if (this.pulsarClientManager == null) { - this.pulsarClientManager = new PulsarClientManager(); - } - this.pulsarClientManager.initialize(); + pulsarAdmin = pulsarClientManager.getAdmin(); + pulsarClient = pulsarClientManager.getClient(); + } - pulsarAdmin = pulsarClientManager.getAdmin(); - pulsarClient = pulsarClientManager.getClient(); + protected void registerAllTools(McpSyncServer mcpServer) { + try { + registerToolsConditionally(mcpServer, getAllAvailableTools(), pulsarClientManager); + } catch (Exception e) { + throw new RuntimeException("Failed to register tools", e); } + } - protected void registerAllTools(McpSyncServer mcpServer) { - try { - registerToolsConditionally(mcpServer, getAllAvailableTools(), pulsarClientManager); - } catch (Exception e) { - throw new RuntimeException("Failed to register tools", e); - } + protected static void registerToolsConditionally( + McpSyncServer mcpServer, Set enabledTools, PulsarClientManager pulsarClientManager) { + if (pulsarAdmin == null) { + throw new RuntimeException("PulsarAdmin has not been initialized"); } - protected static void registerToolsConditionally( - McpSyncServer mcpServer, Set enabledTools, - PulsarClientManager pulsarClientManager) { - if (pulsarAdmin == null) { - throw new RuntimeException("PulsarAdmin has not been initialized"); - } - - if (enabledTools.stream().anyMatch(tool -> tool.contains("cluster") || tool.contains("broker"))) { - registerToolGroup("ClusterTools", () -> { - var clusterTools = new ClusterTools(pulsarAdmin); - clusterTools.registerTools(mcpServer); - }); - } - - if (enabledTools.stream().anyMatch(tool -> tool.contains("topic"))) { - registerToolGroup("TopicTools", () -> { - var topicTools = new TopicTools(pulsarAdmin); - topicTools.registerTools(mcpServer); - }); - } - - if (enabledTools.stream().anyMatch(tool -> tool.contains("tenant"))) { - registerToolGroup("TenantTools", () -> { - var tenantTools = new TenantTools(pulsarAdmin); - tenantTools.registerTools(mcpServer); - }); - } - - if (enabledTools.stream().anyMatch(tool -> tool.contains("namespace") - || tool.contains("retention") || tool.contains("backlog"))) { - registerToolGroup("NamespaceTools", () -> { - var namespaceTools = new NamespaceTools(pulsarAdmin); - namespaceTools.registerTools(mcpServer); - }); - } + if (enabledTools.stream() + .anyMatch(tool -> tool.contains("cluster") || tool.contains("broker"))) { + registerToolGroup( + "ClusterTools", + () -> { + var clusterTools = new ClusterTools(pulsarAdmin); + clusterTools.registerTools(mcpServer); + }); + } - if (enabledTools.stream().anyMatch(tool -> tool.contains("schema"))) { - registerToolGroup("SchemaTools", () -> { - var schemaTools = new SchemaTools(pulsarAdmin); - schemaTools.registerTools(mcpServer); - }); - } + if (enabledTools.stream().anyMatch(tool -> tool.contains("topic"))) { + registerToolGroup( + "TopicTools", + () -> { + var topicTools = new TopicTools(pulsarAdmin); + topicTools.registerTools(mcpServer); + }); + } - if (enabledTools.stream().anyMatch(tool -> tool.contains("message"))) { - registerToolGroup("MessageTools", () -> { - var messageTools = new MessageTools(pulsarAdmin, pulsarClientManager); - messageTools.registerTools(mcpServer); - }); - } + if (enabledTools.stream().anyMatch(tool -> tool.contains("tenant"))) { + registerToolGroup( + "TenantTools", + () -> { + var tenantTools = new TenantTools(pulsarAdmin); + tenantTools.registerTools(mcpServer); + }); + } - if (enabledTools.stream().anyMatch(tool -> tool.contains("subscription") || tool.contains("unsubscribe"))) { - registerToolGroup("SubscriptionTools", () -> { - var subscriptionTools = new SubscriptionTools(pulsarAdmin); - subscriptionTools.registerTools(mcpServer); - }); - } + if (enabledTools.stream() + .anyMatch( + tool -> + tool.contains("namespace") + || tool.contains("retention") + || tool.contains("backlog"))) { + registerToolGroup( + "NamespaceTools", + () -> { + var namespaceTools = new NamespaceTools(pulsarAdmin); + namespaceTools.registerTools(mcpServer); + }); + } - if (enabledTools.stream().anyMatch(tool -> tool.contains("monitor") - || tool.contains("health") || tool.contains("backlog-analysis"))) { - registerToolGroup("MonitoringTools", () -> { - var monitoringTools = new MonitoringTools(pulsarAdmin); - monitoringTools.registerTools(mcpServer); - }); - } + if (enabledTools.stream().anyMatch(tool -> tool.contains("schema"))) { + registerToolGroup( + "SchemaTools", + () -> { + var schemaTools = new SchemaTools(pulsarAdmin); + schemaTools.registerTools(mcpServer); + }); } - private static void registerToolGroup(String toolGroupName, Runnable registrationTask) { - try { - registrationTask.run(); - } catch (NoClassDefFoundError e) { - LOGGER.error("{} dependencies missing: {}", toolGroupName, e.getMessage()); - } catch (Exception e) { - if (e.getCause() instanceof ClassNotFoundException) { - LOGGER.error("{} not available in this configuration (class not found)", toolGroupName); - } else { - LOGGER.error("{} dependencies missing: {}", toolGroupName, e.getMessage()); - if (Boolean.parseBoolean(System.getProperty("mcp.debug", "false"))) { - LOGGER.debug("Exception details", e); - } - } - } + if (enabledTools.stream().anyMatch(tool -> tool.contains("message"))) { + registerToolGroup( + "MessageTools", + () -> { + var messageTools = new MessageTools(pulsarAdmin, pulsarClientManager); + messageTools.registerTools(mcpServer); + }); } - protected static Set getAllAvailableTools() { - return Set.of( - "list-clusters", - "get-cluster-info", - "create-cluster", - "update-cluster-config", - "delete-cluster", - "get-cluster-stats", - "list-brokers", - "get-broker-stats", - "get-cluster-failure-domain", - "set-cluster-failure-domain", - - "list-tenants", - "get-tenant-info", - "create-tenant", - "update-tenant", - "delete-tenant", - "get-tenant-stats", - - "list-namespaces", - "get-namespace-info", - "create-namespace", - "delete-namespace", - "set-retention-policy", - "get-retention-policy", - "set-backlog-quota", - "get-backlog-quota", - "clear-namespace-backlog", - "get-namespace-stats", - - "list-topics", - "create-topic", - "delete-topic", - "get-topic-stats", - "get-topic-metadata", - "update-topic-partitions", - "compact-topic", - "unload-topic", - "get-topic-backlog", - "expire-topic-messages", - "peek-messages", - "reset-topic-cursor", - "get-topic-internal-stats", - "get-partitioned-metadata", - - "list-subscriptions", - "create-subscription", - "delete-subscription", - "get-subscription-stats", - "reset-subscription-cursor", - "skip-messages", - "expire-subscription-messages", - "unsubscribe", - "list-subscription-consumers", - "get-subscription-cursor-positions", - - "send-message", - "peek-message", - "examine-messages", - "get-message-by-id", - "get-message-backlog", - "get-message-stats", - "receive-messages", - "skip-all-messages", - "expire-all-messages", - - "get-schema-info", - "get-schema-version", - "get-all-schema-versions", - "upload-schema", - "delete-schema", - "test-schema-compatibility", - - "monitor-cluster-performance", - "monitor-topic-performance", - "monitor-subscription-performance", - "health-check", - "connection-diagnostics", - "backlog-analysis" - ); + if (enabledTools.stream() + .anyMatch(tool -> tool.contains("subscription") || tool.contains("unsubscribe"))) { + registerToolGroup( + "SubscriptionTools", + () -> { + var subscriptionTools = new SubscriptionTools(pulsarAdmin); + subscriptionTools.registerTools(mcpServer); + }); } + if (enabledTools.stream() + .anyMatch( + tool -> + tool.contains("monitor") + || tool.contains("health") + || tool.contains("backlog-analysis"))) { + registerToolGroup( + "MonitoringTools", + () -> { + var monitoringTools = new MonitoringTools(pulsarAdmin); + monitoringTools.registerTools(mcpServer); + }); + } + } + + private static void registerToolGroup(String toolGroupName, Runnable registrationTask) { + try { + registrationTask.run(); + } catch (NoClassDefFoundError e) { + LOGGER.error("{} dependencies missing: {}", toolGroupName, e.getMessage()); + } catch (Exception e) { + if (e.getCause() instanceof ClassNotFoundException) { + LOGGER.error("{} not available in this configuration (class not found)", toolGroupName); + } else { + LOGGER.error("{} dependencies missing: {}", toolGroupName, e.getMessage()); + if (Boolean.parseBoolean(System.getProperty("mcp.debug", "false"))) { + LOGGER.debug("Exception details", e); + } + } + } + } + + protected static Set getAllAvailableTools() { + return Set.of( + "list-clusters", + "get-cluster-info", + "create-cluster", + "update-cluster-config", + "delete-cluster", + "get-cluster-stats", + "list-brokers", + "get-broker-stats", + "get-cluster-failure-domain", + "set-cluster-failure-domain", + "list-tenants", + "get-tenant-info", + "create-tenant", + "update-tenant", + "delete-tenant", + "get-tenant-stats", + "list-namespaces", + "get-namespace-info", + "create-namespace", + "delete-namespace", + "set-retention-policy", + "get-retention-policy", + "set-backlog-quota", + "get-backlog-quota", + "clear-namespace-backlog", + "get-namespace-stats", + "list-topics", + "create-topic", + "delete-topic", + "get-topic-stats", + "get-topic-metadata", + "update-topic-partitions", + "compact-topic", + "unload-topic", + "get-topic-backlog", + "expire-topic-messages", + "peek-messages", + "reset-topic-cursor", + "get-topic-internal-stats", + "get-partitioned-metadata", + "list-subscriptions", + "create-subscription", + "delete-subscription", + "get-subscription-stats", + "reset-subscription-cursor", + "skip-messages", + "expire-subscription-messages", + "unsubscribe", + "list-subscription-consumers", + "get-subscription-cursor-positions", + "send-message", + "peek-message", + "examine-messages", + "get-message-by-id", + "get-message-backlog", + "get-message-stats", + "receive-messages", + "skip-all-messages", + "expire-all-messages", + "get-schema-info", + "get-schema-version", + "get-all-schema-versions", + "upload-schema", + "delete-schema", + "test-schema-compatibility", + "monitor-cluster-performance", + "monitor-topic-performance", + "monitor-subscription-performance", + "health-check", + "connection-diagnostics", + "backlog-analysis"); + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/HttpMCPServer.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/HttpMCPServer.java index 30ffa2a..538ef3b 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/HttpMCPServer.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/HttpMCPServer.java @@ -30,129 +30,129 @@ public class HttpMCPServer extends AbstractMCPServer implements Transport { - private static final Logger logger = LoggerFactory.getLogger(HttpMCPServer.class); + private static final Logger logger = LoggerFactory.getLogger(HttpMCPServer.class); - private final AtomicBoolean running = new AtomicBoolean(false); - private Server jettyServer; + private final AtomicBoolean running = new AtomicBoolean(false); + private Server jettyServer; - public HttpMCPServer() { - super(); - } + public HttpMCPServer() { + super(); + } - @Override - public void start(PulsarMCPCliOptions options) throws Exception { - if (!running.compareAndSet(false, true)) { - logger.warn("Server is already running"); - return; - } - try { - if (this.pulsarClientManager == null) { - running.set(false); - throw new IllegalStateException("PulsarClientManager not injected."); - } - try { - pulsarAdmin = pulsarClientManager.getAdmin(); - pulsarClient = pulsarClientManager.getClient(); - } catch (Exception e) { - running.set(false); - throw new RuntimeException("Failed to obtain PulsarAdmin from PulsarClientManager", e); - } - logger.info("Starting HTTP Streaming Pulsar MCP server"); - - ObjectMapper mapper = new ObjectMapper() - .findAndRegisterModules() - .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); - - var streamingTransport = HttpServletStreamableServerTransportProvider - .builder() - .objectMapper(mapper) - .build(); - - var mcpServer = McpServer.sync(streamingTransport) - .serverInfo("pulsar-admin-http-streaming", "1.0.0") - .capabilities(McpSchema.ServerCapabilities.builder() - .tools(true) - .build()) - .build(); - - registerAllTools(mcpServer); - startJettyServer(streamingTransport, options.getHttpPort()); - - logger.info("HTTP Streaming Pulsar MCP server started " - + "at http://localhost:{}/mcp", options.getHttpPort()); - - } catch (Exception e) { - running.set(false); - logger.error("Failed to start HTTP streaming server", e); - throw e; - } + @Override + public void start(PulsarMCPCliOptions options) throws Exception { + if (!running.compareAndSet(false, true)) { + logger.warn("Server is already running"); + return; } + try { + if (this.pulsarClientManager == null) { + running.set(false); + throw new IllegalStateException("PulsarClientManager not injected."); + } + try { + pulsarAdmin = pulsarClientManager.getAdmin(); + pulsarClient = pulsarClientManager.getClient(); + } catch (Exception e) { + running.set(false); + throw new RuntimeException("Failed to obtain PulsarAdmin from PulsarClientManager", e); + } + logger.info("Starting HTTP Streaming Pulsar MCP server"); + + ObjectMapper mapper = + new ObjectMapper() + .findAndRegisterModules() + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + + var streamingTransport = + HttpServletStreamableServerTransportProvider.builder().objectMapper(mapper).build(); + + var mcpServer = + McpServer.sync(streamingTransport) + .serverInfo("pulsar-admin-http-streaming", "1.0.0") + .capabilities(McpSchema.ServerCapabilities.builder().tools(true).build()) + .build(); + + registerAllTools(mcpServer); + startJettyServer(streamingTransport, options.getHttpPort()); + + logger.info( + "HTTP Streaming Pulsar MCP server started " + "at http://localhost:{}/mcp", + options.getHttpPort()); + + } catch (Exception e) { + running.set(false); + logger.error("Failed to start HTTP streaming server", e); + throw e; + } + } - @Override - public void stop() { - if (!running.compareAndSet(true, false)) { - return; - } - logger.info("Stopping HTTP Streaming Pulsar MCP server...."); - if (jettyServer != null) { - try { - if (jettyServer.isRunning()) { - jettyServer.stop(); - } - } catch (Exception e) { - logger.warn("Error stopping Jetty: {}", e.getMessage()); - } - } - if (pulsarClientManager != null) { - try { - pulsarClientManager.close(); - } catch (Exception e) { - logger.warn("Error closing PulsarClientManager: {}", e.getMessage()); - } + @Override + public void stop() { + if (!running.compareAndSet(true, false)) { + return; + } + logger.info("Stopping HTTP Streaming Pulsar MCP server...."); + if (jettyServer != null) { + try { + if (jettyServer.isRunning()) { + jettyServer.stop(); } - logger.info("HTTP Streaming Pulsar MCP server stopped"); + } catch (Exception e) { + logger.warn("Error stopping Jetty: {}", e.getMessage()); + } } - - @Override - public PulsarMCPCliOptions.TransportType getType() { - return PulsarMCPCliOptions.TransportType.HTTP; + if (pulsarClientManager != null) { + try { + pulsarClientManager.close(); + } catch (Exception e) { + logger.warn("Error closing PulsarClientManager: {}", e.getMessage()); + } } + logger.info("HTTP Streaming Pulsar MCP server stopped"); + } - private void startJettyServer(HttpServletStreamableServerTransportProvider - streamingTransport, int httpPort) throws Exception { - jettyServer = new Server(httpPort); + @Override + public PulsarMCPCliOptions.TransportType getType() { + return PulsarMCPCliOptions.TransportType.HTTP; + } - var context = new ServletContextHandler(); - context.setContextPath("/"); - jettyServer.setHandler(context); + private void startJettyServer( + HttpServletStreamableServerTransportProvider streamingTransport, int httpPort) + throws Exception { + jettyServer = new Server(httpPort); - ServletHolder servletHolder = new ServletHolder(streamingTransport); - servletHolder.setAsyncSupported(true); + var context = new ServletContextHandler(); + context.setContextPath("/"); + jettyServer.setHandler(context); - context.addServlet(servletHolder, "/mcp"); - context.addServlet(servletHolder, "/mcp/*"); - context.addServlet(servletHolder, "/mcp/stream"); - context.addServlet(servletHolder, "/mcp/stream/*"); + ServletHolder servletHolder = new ServletHolder(streamingTransport); + servletHolder.setAsyncSupported(true); - jettyServer.start(); - } + context.addServlet(servletHolder, "/mcp"); + context.addServlet(servletHolder, "/mcp/*"); + context.addServlet(servletHolder, "/mcp/stream"); + context.addServlet(servletHolder, "/mcp/stream/*"); - public static void main(String[] args) { - try { - HttpMCPServer transport = new HttpMCPServer(); - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); + jettyServer.start(); + } - PulsarClientManager manager = new PulsarClientManager(); - manager.initialize(); - transport.injectClientManager(manager); + public static void main(String[] args) { + try { + HttpMCPServer transport = new HttpMCPServer(); + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); - transport.start(options); + PulsarClientManager manager = new PulsarClientManager(); + manager.initialize(); + transport.injectClientManager(manager); - Thread.currentThread().join(); + transport.start(options); - } catch (Exception e) { - logger.error("Error starting HTTP Streaming Pulsar MCP server: {}", e.getMessage(), e); - System.exit(1); - } + Thread.currentThread().join(); + + } catch (Exception e) { + logger.error("Error starting HTTP Streaming Pulsar MCP server: {}", e.getMessage(), e); + System.exit(1); } + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/StdioMCPServer.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/StdioMCPServer.java index b039114..a8aea78 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/StdioMCPServer.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/StdioMCPServer.java @@ -23,77 +23,78 @@ public class StdioMCPServer extends AbstractMCPServer implements Transport { - private static final Logger logger = LoggerFactory.getLogger(StdioMCPServer.class); - private final AtomicBoolean running = new AtomicBoolean(false); - - public StdioMCPServer() { - super(); + private static final Logger logger = LoggerFactory.getLogger(StdioMCPServer.class); + private final AtomicBoolean running = new AtomicBoolean(false); + + public StdioMCPServer() { + super(); + } + + @Override + public void start(PulsarMCPCliOptions options) { + if (!running.compareAndSet(false, true)) { + logger.warn("Stdio transport is already running"); + return; } - @Override - public void start(PulsarMCPCliOptions options) { - if (!running.compareAndSet(false, true)) { - logger.warn("Stdio transport is already running"); - return; - } - - if (this.pulsarClientManager == null) { - running.set(false); - throw new IllegalStateException("PulsarClientManager not injected."); - } - - try { - initializePulsar(); - } catch (Exception e) { - running.set(false); - logger.error("Failed to initialize PulsarAdmin", e); - throw new RuntimeException("Cannot start MCP server without Pulsar connection. " - + "Please ensure Pulsar is running at" - + System.getProperty("PULSAR_ADMIN_URL", "http://localhost:8080"), e); - } - - var mcpServer = McpServer.sync(new StdioServerTransportProvider()) - .serverInfo("pulsar-admin-stdio", "1.0.0") - .capabilities(McpSchema.ServerCapabilities.builder().tools(true).build()) - .build(); - - registerAllTools(mcpServer); - + if (this.pulsarClientManager == null) { + running.set(false); + throw new IllegalStateException("PulsarClientManager not injected."); } + try { + initializePulsar(); + } catch (Exception e) { + running.set(false); + logger.error("Failed to initialize PulsarAdmin", e); + throw new RuntimeException( + "Cannot start MCP server without Pulsar connection. " + + "Please ensure Pulsar is running at" + + System.getProperty("PULSAR_ADMIN_URL", "http://localhost:8080"), + e); + } - @Override - public void stop() { - if (!running.get()) { - return; - } - - running.set(false); + var mcpServer = + McpServer.sync(new StdioServerTransportProvider()) + .serverInfo("pulsar-admin-stdio", "1.0.0") + .capabilities(McpSchema.ServerCapabilities.builder().tools(true).build()) + .build(); - if (pulsarClientManager != null) { - try { - pulsarClientManager.close(); - } catch (Exception e) { - logger.warn("Error closing PulsarManager: {}", e.getMessage()); - } - } + registerAllTools(mcpServer); + } - logger.info("Pulsar MCP server stopped successfully"); + @Override + public void stop() { + if (!running.get()) { + return; } - @Override - public PulsarMCPCliOptions.TransportType getType() { - return PulsarMCPCliOptions.TransportType.STDIO; + running.set(false); + + if (pulsarClientManager != null) { + try { + pulsarClientManager.close(); + } catch (Exception e) { + logger.warn("Error closing PulsarManager: {}", e.getMessage()); + } } - public static void main(String[] args) { - try { - StdioMCPServer server = new StdioMCPServer(); - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); - server.start(options); - } catch (Exception e) { - logger.error("Error starting Pulsar MCP server: {}", e.getMessage(), e); - System.exit(1); - } + logger.info("Pulsar MCP server stopped successfully"); + } + + @Override + public PulsarMCPCliOptions.TransportType getType() { + return PulsarMCPCliOptions.TransportType.STDIO; + } + + public static void main(String[] args) { + try { + StdioMCPServer server = new StdioMCPServer(); + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); + server.start(options); + } catch (Exception e) { + logger.error("Error starting Pulsar MCP server: {}", e.getMessage(), e); + System.exit(1); } + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/Transport.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/Transport.java index 674f337..99b3d25 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/Transport.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/Transport.java @@ -17,13 +17,13 @@ public interface Transport { - void start(PulsarMCPCliOptions options) throws Exception; + void start(PulsarMCPCliOptions options) throws Exception; - void stop() throws Exception; + void stop() throws Exception; - PulsarMCPCliOptions.TransportType getType(); + PulsarMCPCliOptions.TransportType getType(); - default String getDescription(){ - return getType().getDescription(); - } + default String getDescription() { + return getType().getDescription(); + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/TransportLauncher.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/TransportLauncher.java index 4c130c6..5866e5d 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/TransportLauncher.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/TransportLauncher.java @@ -19,52 +19,55 @@ public class TransportLauncher { - public static void start(PulsarMCPCliOptions options) throws Exception { - startTransport(options); - } - - private static void startTransport(PulsarMCPCliOptions options) throws Exception { - TransportManager transportManager = new TransportManager(); + public static void start(PulsarMCPCliOptions options) throws Exception { + startTransport(options); + } - StdioMCPServer stdio = new StdioMCPServer(); - HttpMCPServer http = new HttpMCPServer(); + private static void startTransport(PulsarMCPCliOptions options) throws Exception { + TransportManager transportManager = new TransportManager(); - PulsarClientManager manager = new PulsarClientManager(); - manager.initialize(); - stdio.injectClientManager(manager); - http.injectClientManager(manager); + StdioMCPServer stdio = new StdioMCPServer(); + HttpMCPServer http = new HttpMCPServer(); - transportManager.registerTransport(stdio); - transportManager.registerTransport(http); + PulsarClientManager manager = new PulsarClientManager(); + manager.initialize(); + stdio.injectClientManager(manager); + http.injectClientManager(manager); - final PulsarMCPCliOptions.TransportType chosen = options.getTransport(); - final Transport[] started = new Transport[1]; + transportManager.registerTransport(stdio); + transportManager.registerTransport(http); + final PulsarMCPCliOptions.TransportType chosen = options.getTransport(); + final Transport[] started = new Transport[1]; - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - if (started[0] != null) { - try { - started[0].stop(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - try { - manager.close(); - } catch (Exception ignore) { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + if (started[0] != null) { + try { + started[0].stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + try { + manager.close(); + } catch (Exception ignore) { - } - }, "pulsar-manager-shutdown")); + } + }, + "pulsar-manager-shutdown")); - switch (chosen) { - case HTTP -> { - transportManager.startTransport(PulsarMCPCliOptions.TransportType.HTTP, options); - started[0] = http; - } - case STDIO -> { - transportManager.startTransport(PulsarMCPCliOptions.TransportType.STDIO, options); - started[0] = stdio; - } - } + switch (chosen) { + case HTTP -> { + transportManager.startTransport(PulsarMCPCliOptions.TransportType.HTTP, options); + started[0] = http; + } + case STDIO -> { + transportManager.startTransport(PulsarMCPCliOptions.TransportType.STDIO, options); + started[0] = stdio; + } } + } } diff --git a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/TransportManager.java b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/TransportManager.java index 8c7eb43..78e4414 100644 --- a/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/TransportManager.java +++ b/pulsar-admin-mcp-contrib/src/main/java/org/apache/pulsar/admin/mcp/transport/TransportManager.java @@ -20,19 +20,18 @@ public class TransportManager { - private final Map transports = new ConcurrentHashMap<>(); + private final Map transports = new ConcurrentHashMap<>(); - public void registerTransport(Transport transport) { - TransportType type = transport.getType(); - transports.put(type, transport); - } + public void registerTransport(Transport transport) { + TransportType type = transport.getType(); + transports.put(type, transport); + } - public void startTransport(TransportType type, PulsarMCPCliOptions options) throws Exception { - Transport transport = transports.get(type); - if (transport == null) { - throw new IllegalArgumentException("Transport not registered: " + type); - } - transport.start(options); + public void startTransport(TransportType type, PulsarMCPCliOptions options) throws Exception { + Transport transport = transports.get(type); + if (transport == null) { + throw new IllegalArgumentException("Transport not registered: " + type); } - + transport.start(options); + } } diff --git a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/client/PulsarClientManagerTest.java b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/client/PulsarClientManagerTest.java index 422db5f..7b2c900 100644 --- a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/client/PulsarClientManagerTest.java +++ b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/client/PulsarClientManagerTest.java @@ -19,167 +19,164 @@ public class PulsarClientManagerTest { - private PulsarClientManager manager; - private String originalAdminUrl; - private String originalServiceUrl; + private PulsarClientManager manager; + private String originalAdminUrl; + private String originalServiceUrl; - @BeforeMethod - public void setUp() { - manager = new PulsarClientManager(); + @BeforeMethod + public void setUp() { + manager = new PulsarClientManager(); - originalAdminUrl = System.getenv("PULSAR_ADMIN_URL"); - originalServiceUrl = System.getenv("PULSAR_SERVICE_URL"); + originalAdminUrl = System.getenv("PULSAR_ADMIN_URL"); + originalServiceUrl = System.getenv("PULSAR_SERVICE_URL"); - System.clearProperty("PULSAR_ADMIN_URL"); - System.clearProperty("PULSAR_SERVICE_URL"); - } + System.clearProperty("PULSAR_ADMIN_URL"); + System.clearProperty("PULSAR_SERVICE_URL"); + } - @AfterMethod - public void tearDown() { - if (manager != null) { - try { - manager.close(); - } catch (Exception ignore) { - } - } - if (originalAdminUrl != null) { - System.setProperty("PULSAR_ADMIN_URL", originalAdminUrl); - } else { - System.clearProperty("PULSAR_ADMIN_URL"); - } - - if (originalServiceUrl != null) { - System.setProperty("PULSAR_SERVICE_URL", originalServiceUrl); - } else { - System.clearProperty("PULSAR_SERVICE_URL"); - } + @AfterMethod + public void tearDown() { + if (manager != null) { + try { + manager.close(); + } catch (Exception ignore) { + } } - - @Test( - expectedExceptions = RuntimeException.class, - expectedExceptionsMessageRegExp = ".*Failed to initialize PulsarAdmin.*" - ) - public void initialize_shouldThrowException_whenPulsarNotRunning() { - System.setProperty("PULSAR_ADMIN_URL", "http://localhost:99999"); - System.setProperty("PULSAR_SERVICE_URL", "pulsar://localhost:99999"); - - manager.initialize(); + if (originalAdminUrl != null) { + System.setProperty("PULSAR_ADMIN_URL", originalAdminUrl); + } else { + System.clearProperty("PULSAR_ADMIN_URL"); } - @Test - public void getClient_shouldNotThrowException_whenPulsarNotRunning() { - System.setProperty("PULSAR_ADMIN_URL", "http://localhost:8080"); - System.setProperty("PULSAR_SERVICE_URL", "pulsar://localhost:99999"); - - try { - org.apache.pulsar.client.api.PulsarClient client = manager.getClient(); - org.testng.Assert.assertNotNull(client); - } catch (Exception e) { - org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarClient")); - } + if (originalServiceUrl != null) { + System.setProperty("PULSAR_SERVICE_URL", originalServiceUrl); + } else { + System.clearProperty("PULSAR_SERVICE_URL"); } - - @Test - public void close_shouldCloseBothAdminAndClient() throws Exception { - PulsarClientManager testManager = new PulsarClientManager(); - - testManager.close(); - - testManager.close(); + } + + @Test( + expectedExceptions = RuntimeException.class, + expectedExceptionsMessageRegExp = ".*Failed to initialize PulsarAdmin.*") + public void initialize_shouldThrowException_whenPulsarNotRunning() { + System.setProperty("PULSAR_ADMIN_URL", "http://localhost:99999"); + System.setProperty("PULSAR_SERVICE_URL", "pulsar://localhost:99999"); + + manager.initialize(); + } + + @Test + public void getClient_shouldNotThrowException_whenPulsarNotRunning() { + System.setProperty("PULSAR_ADMIN_URL", "http://localhost:8080"); + System.setProperty("PULSAR_SERVICE_URL", "pulsar://localhost:99999"); + + try { + org.apache.pulsar.client.api.PulsarClient client = manager.getClient(); + org.testng.Assert.assertNotNull(client); + } catch (Exception e) { + org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarClient")); } + } - @Test - public void initialize_shouldSetDefaultUrls() { - try { - manager.initialize(); - org.testng.Assert.fail("Expected exception for missing Pulsar connection"); - } catch (RuntimeException e) { - org.testng.Assert.assertTrue( - e.getMessage().contains("Failed to initialize PulsarAdmin") - ); - } - } + @Test + public void close_shouldCloseBothAdminAndClient() throws Exception { + PulsarClientManager testManager = new PulsarClientManager(); - @Test - public void getAdmin_shouldUseDefaultUrl() { - try { - manager.getAdmin(); - org.testng.Assert.fail("Expected exception"); - } catch (RuntimeException e) { - org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarAdmin")); - } - } + testManager.close(); - @Test - public void getAdmin_shouldUseEnvVarWhenSet() { - System.setProperty("PULSAR_ADMIN_URL", "http://localhost:8080"); + testManager.close(); + } - try { - manager.getAdmin(); - org.testng.Assert.fail("Expected exception"); - } catch (RuntimeException e) { - org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarAdmin")); - } + @Test + public void initialize_shouldSetDefaultUrls() { + try { + manager.initialize(); + org.testng.Assert.fail("Expected exception for missing Pulsar connection"); + } catch (RuntimeException e) { + org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarAdmin")); } - - @Test - public void getClient_shouldUseDefaultUrl() { - org.apache.pulsar.client.api.PulsarClient client = manager.getClient(); - org.testng.Assert.assertNotNull(client); + } + + @Test + public void getAdmin_shouldUseDefaultUrl() { + try { + manager.getAdmin(); + org.testng.Assert.fail("Expected exception"); + } catch (RuntimeException e) { + org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarAdmin")); } + } - @Test - public void getClient_shouldUseEnvVarWhenSet() { - System.setProperty("PULSAR_SERVICE_URL", "pulsar://localhost:6650"); + @Test + public void getAdmin_shouldUseEnvVarWhenSet() { + System.setProperty("PULSAR_ADMIN_URL", "http://localhost:8080"); - org.apache.pulsar.client.api.PulsarClient client = manager.getClient(); - org.testng.Assert.assertNotNull(client); + try { + manager.getAdmin(); + org.testng.Assert.fail("Expected exception"); + } catch (RuntimeException e) { + org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarAdmin")); } - - @Test - public void initialize_shouldCallGetAdminAndGetClient() { - try { - manager.initialize(); - org.testng.Assert.fail("Expected exception"); - } catch (RuntimeException e) { - org.testng.Assert.assertNotNull(e.getMessage()); - } + } + + @Test + public void getClient_shouldUseDefaultUrl() { + org.apache.pulsar.client.api.PulsarClient client = manager.getClient(); + org.testng.Assert.assertNotNull(client); + } + + @Test + public void getClient_shouldUseEnvVarWhenSet() { + System.setProperty("PULSAR_SERVICE_URL", "pulsar://localhost:6650"); + + org.apache.pulsar.client.api.PulsarClient client = manager.getClient(); + org.testng.Assert.assertNotNull(client); + } + + @Test + public void initialize_shouldCallGetAdminAndGetClient() { + try { + manager.initialize(); + org.testng.Assert.fail("Expected exception"); + } catch (RuntimeException e) { + org.testng.Assert.assertNotNull(e.getMessage()); } - - @Test - public void close_shouldBeIdempotent() throws Exception { - manager.close(); - manager.close(); - manager.close(); + } + + @Test + public void close_shouldBeIdempotent() throws Exception { + manager.close(); + manager.close(); + manager.close(); + } + + @Test + public void initialize_shouldHandleInvalidUrl() { + System.setProperty("PULSAR_ADMIN_URL", "invalid-url"); + System.setProperty("PULSAR_SERVICE_URL", "invalid-url"); + + try { + manager.initialize(); + org.testng.Assert.fail("Expected exception"); + } catch (RuntimeException e) { + org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize")); } - - @Test - public void initialize_shouldHandleInvalidUrl() { - System.setProperty("PULSAR_ADMIN_URL", "invalid-url"); - System.setProperty("PULSAR_SERVICE_URL", "invalid-url"); - - try { - manager.initialize(); - org.testng.Assert.fail("Expected exception"); - } catch (RuntimeException e) { - org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize")); - } + } + + @Test + public void singletonBehavior_shouldReturnSameInstance() { + try { + manager.getAdmin(); + org.testng.Assert.fail("Expected exception"); + } catch (RuntimeException e) { + org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarAdmin")); } - @Test - public void singletonBehavior_shouldReturnSameInstance() { - try { - manager.getAdmin(); - org.testng.Assert.fail("Expected exception"); - } catch (RuntimeException e) { - org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarAdmin")); - } - - try { - manager.getAdmin(); - org.testng.Assert.fail("Expected exception"); - } catch (RuntimeException e) { - org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarAdmin")); - } + try { + manager.getAdmin(); + org.testng.Assert.fail("Expected exception"); + } catch (RuntimeException e) { + org.testng.Assert.assertTrue(e.getMessage().contains("Failed to initialize PulsarAdmin")); } + } } diff --git a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/config/PulsarMCPCliOptionsTest.java b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/config/PulsarMCPCliOptionsTest.java index 72c16fe..8609dd4 100644 --- a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/config/PulsarMCPCliOptionsTest.java +++ b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/config/PulsarMCPCliOptionsTest.java @@ -17,176 +17,167 @@ public class PulsarMCPCliOptionsTest { - @Test - public void parseArgs_shouldParseDefaultOptions() { - String[] args = {}; - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); - - org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.STDIO); - org.testng.Assert.assertEquals(options.getHttpPort(), 8889); - } - - @Test - public void parseArgs_shouldParseTransportOption() { - String[] args = {"--transport", "http"}; - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); - - org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.HTTP); - org.testng.Assert.assertEquals(options.getHttpPort(), 8889); - } - - @Test - public void parseArgs_shouldParseTransportOptionWithShortForm() { - String[] args = {"-t", "stdio"}; - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); - - org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.STDIO); - org.testng.Assert.assertEquals(options.getHttpPort(), 8889); - } - - @Test - public void parseArgs_shouldParsePortOption() { - String[] args = {"--port", "9999"}; - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); - - org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.STDIO); - org.testng.Assert.assertEquals(options.getHttpPort(), 9999); - } - - @Test - public void parseArgs_shouldParseBothOptions() { - String[] args = {"--transport", "http", "--port", "8080"}; - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); - - org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.HTTP); - org.testng.Assert.assertEquals(options.getHttpPort(), 8080); - } - - @Test( - expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = "Missing value for --transport" - ) - public void parseArgs_shouldThrowException_whenTransportValueMissing() { - PulsarMCPCliOptions.parseArgs(new String[]{"--transport"}); - } - - @Test( - expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = "Missing value for --port" - ) - public void parseArgs_shouldThrowException_whenPortValueMissing() { - PulsarMCPCliOptions.parseArgs(new String[]{"--port"}); - } - - @Test( - expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = ".*invalid is not a valid TransportType.*Valid Options: stdio,http.*" - ) - public void parseArgs_shouldThrowException_whenInvalidTransport() { - PulsarMCPCliOptions.parseArgs(new String[]{"--transport", "invalid"}); - } - - @Test( - expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = "Invalid port number for --port" - ) - public void parseArgs_shouldThrowException_whenInvalidPort() { - PulsarMCPCliOptions.parseArgs(new String[]{"--port", "invalid"}); - } - - @Test( - expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = "Unknown argument: --unknown" - ) - public void parseArgs_shouldThrowException_whenUnknownArgument() { - PulsarMCPCliOptions.parseArgs(new String[]{"--unknown", "value"}); - } - - @Test - public void parseArgs_shouldHandleCaseInsensitiveTransport() { - String[] args = {"--transport", "HTTP"}; - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); - - org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.HTTP); - } - - @Test - public void parseArgs_shouldHandleMixedCaseTransport() { - String[] args = {"--transport", "Stdio"}; - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); - - org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.STDIO); - } - - @Test - public void toString_shouldReturnCorrectFormat() throws Exception { - PulsarMCPCliOptions options = new PulsarMCPCliOptions(); - - java.lang.reflect.Field transportField = PulsarMCPCliOptions.class.getDeclaredField("transport"); - transportField.setAccessible(true); - transportField.set(options, PulsarMCPCliOptions.TransportType.HTTP); - - java.lang.reflect.Field portField = PulsarMCPCliOptions.class.getDeclaredField("httpPort"); - portField.setAccessible(true); - portField.set(options, 9999); - - String result = options.toString(); - org.testng.Assert.assertTrue(result.contains("transport=HTTP")); - org.testng.Assert.assertTrue(result.contains("httpPort=9999")); - } - - @Test - public void transportType_fromString_shouldReturnCorrectType() { - org.testng.Assert.assertEquals( - PulsarMCPCliOptions.TransportType.fromString("stdio"), - PulsarMCPCliOptions.TransportType.STDIO - ); - org.testng.Assert.assertEquals( - PulsarMCPCliOptions.TransportType.fromString("http"), - PulsarMCPCliOptions.TransportType.HTTP - ); - org.testng.Assert.assertEquals( - PulsarMCPCliOptions.TransportType.fromString("STDIO"), - PulsarMCPCliOptions.TransportType.STDIO - ); - org.testng.Assert.assertEquals( - PulsarMCPCliOptions.TransportType.fromString("HTTP"), - PulsarMCPCliOptions.TransportType.HTTP - ); - } - - @Test( - expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = ".*invalid is not a valid TransportType.*Valid Options: stdio,http.*" - ) - public void transportType_fromString_shouldThrowException_whenInvalidType() { - PulsarMCPCliOptions.TransportType.fromString("invalid"); - } - - @Test - public void transportType_values_shouldHaveCorrectValues() { - PulsarMCPCliOptions.TransportType[] values = PulsarMCPCliOptions.TransportType.values(); - - org.testng.Assert.assertEquals(values.length, 2); - org.testng.Assert.assertEquals(values[0], PulsarMCPCliOptions.TransportType.STDIO); - org.testng.Assert.assertEquals(values[1], PulsarMCPCliOptions.TransportType.HTTP); - } - - @Test - public void transportType_getValue_shouldReturnCorrectValue() { - org.testng.Assert.assertEquals(PulsarMCPCliOptions.TransportType.STDIO.getValue(), "stdio"); - org.testng.Assert.assertEquals(PulsarMCPCliOptions.TransportType.HTTP.getValue(), "http"); - } - - @Test - public void transportType_getDescription_shouldReturnCorrectDescription() { - org.testng.Assert.assertEquals( - PulsarMCPCliOptions.TransportType.STDIO.getDescription(), - "Standard input/output (Claude Desktop)" - ); - org.testng.Assert.assertEquals( - PulsarMCPCliOptions.TransportType.HTTP.getDescription(), - "HTTP Streaming Events (Web application)" - ); - } + @Test + public void parseArgs_shouldParseDefaultOptions() { + String[] args = {}; + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); + + org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.STDIO); + org.testng.Assert.assertEquals(options.getHttpPort(), 8889); + } + + @Test + public void parseArgs_shouldParseTransportOption() { + String[] args = {"--transport", "http"}; + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); + + org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.HTTP); + org.testng.Assert.assertEquals(options.getHttpPort(), 8889); + } + + @Test + public void parseArgs_shouldParseTransportOptionWithShortForm() { + String[] args = {"-t", "stdio"}; + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); + + org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.STDIO); + org.testng.Assert.assertEquals(options.getHttpPort(), 8889); + } + + @Test + public void parseArgs_shouldParsePortOption() { + String[] args = {"--port", "9999"}; + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); + + org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.STDIO); + org.testng.Assert.assertEquals(options.getHttpPort(), 9999); + } + + @Test + public void parseArgs_shouldParseBothOptions() { + String[] args = {"--transport", "http", "--port", "8080"}; + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); + + org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.HTTP); + org.testng.Assert.assertEquals(options.getHttpPort(), 8080); + } + + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "Missing value for --transport") + public void parseArgs_shouldThrowException_whenTransportValueMissing() { + PulsarMCPCliOptions.parseArgs(new String[] {"--transport"}); + } + + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "Missing value for --port") + public void parseArgs_shouldThrowException_whenPortValueMissing() { + PulsarMCPCliOptions.parseArgs(new String[] {"--port"}); + } + + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = + ".*invalid is not a valid TransportType.*Valid Options: stdio,http.*") + public void parseArgs_shouldThrowException_whenInvalidTransport() { + PulsarMCPCliOptions.parseArgs(new String[] {"--transport", "invalid"}); + } + + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "Invalid port number for --port") + public void parseArgs_shouldThrowException_whenInvalidPort() { + PulsarMCPCliOptions.parseArgs(new String[] {"--port", "invalid"}); + } + + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "Unknown argument: --unknown") + public void parseArgs_shouldThrowException_whenUnknownArgument() { + PulsarMCPCliOptions.parseArgs(new String[] {"--unknown", "value"}); + } + + @Test + public void parseArgs_shouldHandleCaseInsensitiveTransport() { + String[] args = {"--transport", "HTTP"}; + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); + + org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.HTTP); + } + + @Test + public void parseArgs_shouldHandleMixedCaseTransport() { + String[] args = {"--transport", "Stdio"}; + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(args); + + org.testng.Assert.assertEquals(options.getTransport(), PulsarMCPCliOptions.TransportType.STDIO); + } + + @Test + public void toString_shouldReturnCorrectFormat() throws Exception { + PulsarMCPCliOptions options = new PulsarMCPCliOptions(); + + java.lang.reflect.Field transportField = + PulsarMCPCliOptions.class.getDeclaredField("transport"); + transportField.setAccessible(true); + transportField.set(options, PulsarMCPCliOptions.TransportType.HTTP); + + java.lang.reflect.Field portField = PulsarMCPCliOptions.class.getDeclaredField("httpPort"); + portField.setAccessible(true); + portField.set(options, 9999); + + String result = options.toString(); + org.testng.Assert.assertTrue(result.contains("transport=HTTP")); + org.testng.Assert.assertTrue(result.contains("httpPort=9999")); + } + + @Test + public void transportType_fromString_shouldReturnCorrectType() { + org.testng.Assert.assertEquals( + PulsarMCPCliOptions.TransportType.fromString("stdio"), + PulsarMCPCliOptions.TransportType.STDIO); + org.testng.Assert.assertEquals( + PulsarMCPCliOptions.TransportType.fromString("http"), + PulsarMCPCliOptions.TransportType.HTTP); + org.testng.Assert.assertEquals( + PulsarMCPCliOptions.TransportType.fromString("STDIO"), + PulsarMCPCliOptions.TransportType.STDIO); + org.testng.Assert.assertEquals( + PulsarMCPCliOptions.TransportType.fromString("HTTP"), + PulsarMCPCliOptions.TransportType.HTTP); + } + + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = + ".*invalid is not a valid TransportType.*Valid Options: stdio,http.*") + public void transportType_fromString_shouldThrowException_whenInvalidType() { + PulsarMCPCliOptions.TransportType.fromString("invalid"); + } + + @Test + public void transportType_values_shouldHaveCorrectValues() { + PulsarMCPCliOptions.TransportType[] values = PulsarMCPCliOptions.TransportType.values(); + + org.testng.Assert.assertEquals(values.length, 2); + org.testng.Assert.assertEquals(values[0], PulsarMCPCliOptions.TransportType.STDIO); + org.testng.Assert.assertEquals(values[1], PulsarMCPCliOptions.TransportType.HTTP); + } + + @Test + public void transportType_getValue_shouldReturnCorrectValue() { + org.testng.Assert.assertEquals(PulsarMCPCliOptions.TransportType.STDIO.getValue(), "stdio"); + org.testng.Assert.assertEquals(PulsarMCPCliOptions.TransportType.HTTP.getValue(), "http"); + } + + @Test + public void transportType_getDescription_shouldReturnCorrectDescription() { + org.testng.Assert.assertEquals( + PulsarMCPCliOptions.TransportType.STDIO.getDescription(), + "Standard input/output (Claude Desktop)"); + org.testng.Assert.assertEquals( + PulsarMCPCliOptions.TransportType.HTTP.getDescription(), + "HTTP Streaming Events (Web application)"); + } } diff --git a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/tools/BasePulsarToolsTest.java b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/tools/BasePulsarToolsTest.java index 1a7b05f..76aba72 100644 --- a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/tools/BasePulsarToolsTest.java +++ b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/tools/BasePulsarToolsTest.java @@ -26,378 +26,391 @@ public class BasePulsarToolsTest { - @Mock - private PulsarAdmin mockPulsarAdmin; + @Mock private PulsarAdmin mockPulsarAdmin; - private BasePulsarToolsTest.TestPulsarTools testTools; + private BasePulsarToolsTest.TestPulsarTools testTools; - private AutoCloseable mocks; + private AutoCloseable mocks; - @BeforeMethod - public void setUp() { - this.mocks = MockitoAnnotations.openMocks(this); - this.testTools = new BasePulsarToolsTest.TestPulsarTools(mockPulsarAdmin); - } + @BeforeMethod + public void setUp() { + this.mocks = MockitoAnnotations.openMocks(this); + this.testTools = new BasePulsarToolsTest.TestPulsarTools(mockPulsarAdmin); + } - @AfterMethod - public void tearDown() throws Exception { - if (this.mocks != null) { - this.mocks.close(); - } + @AfterMethod + public void tearDown() throws Exception { + if (this.mocks != null) { + this.mocks.close(); } - - @Test( - expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = "pulsarAdmin cannot be null" - ) - public void constructor_shouldThrowException_whenPulsarAdminIsNull() { - new BasePulsarToolsTest.TestPulsarTools(null); + } + + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "pulsarAdmin cannot be null") + public void constructor_shouldThrowException_whenPulsarAdminIsNull() { + new BasePulsarToolsTest.TestPulsarTools(null); + } + + @Test + public void createSuccessResult_shouldCreateSuccessResultWithData() { + Map data = new HashMap<>(); + data.put("key", "value"); + data.put("count", 42); + + McpSchema.CallToolResult result = testTools.createSuccessResult("Test message", data); + + org.testng.Assert.assertFalse(result.isError()); + org.testng.Assert.assertEquals(result.content().size(), 1); + org.testng.Assert.assertTrue(result.content().get(0) instanceof McpSchema.TextContent); + String content = ((McpSchema.TextContent) result.content().get(0)).text(); + org.testng.Assert.assertTrue(content.contains("Test message")); + org.testng.Assert.assertTrue(content.contains("key")); + org.testng.Assert.assertTrue(content.contains("value")); + } + + @Test + public void createSuccessResult_shouldCreateSuccessResultWithoutData() { + McpSchema.CallToolResult result = testTools.createSuccessResult("Test message", null); + + org.testng.Assert.assertFalse(result.isError()); + org.testng.Assert.assertEquals(result.content().size(), 1); + org.testng.Assert.assertTrue(result.content().get(0) instanceof McpSchema.TextContent); + String content = ((McpSchema.TextContent) result.content().get(0)).text(); + org.testng.Assert.assertEquals(content, "Test message\n"); + } + + @Test + public void createErrorResult_shouldCreateErrorResultWithMessage() { + McpSchema.CallToolResult result = testTools.createErrorResult("Test error"); + + org.testng.Assert.assertTrue(result.isError()); + org.testng.Assert.assertEquals(result.content().size(), 1); + org.testng.Assert.assertTrue(result.content().get(0) instanceof McpSchema.TextContent); + String content = ((McpSchema.TextContent) result.content().get(0)).text(); + org.testng.Assert.assertEquals(content, "Error: Test error"); + } + + @Test + public void createErrorResult_shouldCreateErrorResultWithSuggestions() { + List suggestions = List.of("Suggestion 1", "Suggestion 2"); + McpSchema.CallToolResult result = testTools.createErrorResult("Test error", suggestions); + + org.testng.Assert.assertTrue(result.isError()); + org.testng.Assert.assertEquals(result.content().size(), 1); + org.testng.Assert.assertTrue(result.content().get(0) instanceof McpSchema.TextContent); + String content = ((McpSchema.TextContent) result.content().get(0)).text(); + org.testng.Assert.assertTrue(content.contains("Test error")); + org.testng.Assert.assertTrue(content.contains("Suggestion 1")); + org.testng.Assert.assertTrue(content.contains("Suggestion 2")); + } + + @Test + public void getStringParam_shouldReturnValue_whenKeyExists() { + Map params = new HashMap<>(); + params.put("testKey", "testValue"); + params.put("nullKey", null); + + org.testng.Assert.assertEquals(testTools.getStringParam(params, "testKey"), "testValue"); + org.testng.Assert.assertEquals(testTools.getStringParam(params, "nullKey"), ""); + org.testng.Assert.assertEquals(testTools.getStringParam(params, "nonExistentKey"), ""); + } + + @Test + public void getRequiredStringParam_shouldReturnValue_whenKeyExistsAndNotEmpty() { + Map params = new HashMap<>(); + params.put("testKey", "testValue"); + params.put("emptyKey", " "); + params.put("nullKey", null); + + org.testng.Assert.assertEquals( + testTools.getRequiredStringParam(params, "testKey"), "testValue"); + + // emptyKey + try { + testTools.getRequiredStringParam(params, "emptyKey"); + org.testng.Assert.fail("Expected IllegalArgumentException for emptyKey"); + } catch (IllegalArgumentException e) { + org.testng.Assert.assertTrue( + e.getMessage().contains("Required parameter 'emptyKey' is missing")); } - @Test - public void createSuccessResult_shouldCreateSuccessResultWithData() { - Map data = new HashMap<>(); - data.put("key", "value"); - data.put("count", 42); - - McpSchema.CallToolResult result = testTools.createSuccessResult("Test message", data); - - org.testng.Assert.assertFalse(result.isError()); - org.testng.Assert.assertEquals(result.content().size(), 1); - org.testng.Assert.assertTrue(result.content().get(0) instanceof McpSchema.TextContent); - String content = ((McpSchema.TextContent) result.content().get(0)).text(); - org.testng.Assert.assertTrue(content.contains("Test message")); - org.testng.Assert.assertTrue(content.contains("key")); - org.testng.Assert.assertTrue(content.contains("value")); + // nullKey + try { + testTools.getRequiredStringParam(params, "nullKey"); + org.testng.Assert.fail("Expected IllegalArgumentException for nullKey"); + } catch (IllegalArgumentException e) { + org.testng.Assert.assertTrue( + e.getMessage().contains("Required parameter 'nullKey' is missing")); } - @Test - public void createSuccessResult_shouldCreateSuccessResultWithoutData() { - McpSchema.CallToolResult result = testTools.createSuccessResult("Test message", null); - - org.testng.Assert.assertFalse(result.isError()); - org.testng.Assert.assertEquals(result.content().size(), 1); - org.testng.Assert.assertTrue(result.content().get(0) instanceof McpSchema.TextContent); - String content = ((McpSchema.TextContent) result.content().get(0)).text(); - org.testng.Assert.assertEquals(content, "Test message\n"); + // nonExistentKey + try { + testTools.getRequiredStringParam(params, "nonExistentKey"); + org.testng.Assert.fail("Expected IllegalArgumentException for nonExistentKey"); + } catch (IllegalArgumentException e) { + org.testng.Assert.assertTrue( + e.getMessage().contains("Required parameter 'nonExistentKey' is missing")); } - - @Test - public void createErrorResult_shouldCreateErrorResultWithMessage() { - McpSchema.CallToolResult result = testTools.createErrorResult("Test error"); - - org.testng.Assert.assertTrue(result.isError()); - org.testng.Assert.assertEquals(result.content().size(), 1); - org.testng.Assert.assertTrue(result.content().get(0) instanceof McpSchema.TextContent); - String content = ((McpSchema.TextContent) result.content().get(0)).text(); - org.testng.Assert.assertEquals(content, "Error: Test error"); + } + + @Test + public void getIntParam_shouldReturnValue_whenKeyExists() { + Map params = new HashMap<>(); + params.put("intKey", 42); + params.put("stringKey", "123"); + params.put("doubleKey", 45.6); + params.put("nullKey", null); + + org.testng.Assert.assertEquals(testTools.getIntParam(params, "intKey", 0), Integer.valueOf(42)); + org.testng.Assert.assertEquals( + testTools.getIntParam(params, "stringKey", 0), Integer.valueOf(123)); + org.testng.Assert.assertEquals( + testTools.getIntParam(params, "doubleKey", 0), Integer.valueOf(45)); + org.testng.Assert.assertEquals(testTools.getIntParam(params, "nullKey", 0), Integer.valueOf(0)); + org.testng.Assert.assertEquals( + testTools.getIntParam(params, "nonExistentKey", 0), Integer.valueOf(0)); + } + + @Test + public void getIntParam_shouldReturnDefault_whenInvalidFormat() { + Map params = new HashMap<>(); + params.put("invalidKey", "not-a-number"); + + org.testng.Assert.assertEquals( + testTools.getIntParam(params, "invalidKey", 0), Integer.valueOf(0)); + } + + @Test + public void getBooleanParam_shouldReturnValue_whenKeyExists() { + Map params = new HashMap<>(); + params.put("boolKey", true); + params.put("stringKey", "true"); + params.put("nullKey", null); + + org.testng.Assert.assertEquals( + testTools.getBooleanParam(params, "boolKey", false), Boolean.TRUE); + org.testng.Assert.assertEquals( + testTools.getBooleanParam(params, "stringKey", false), Boolean.TRUE); + org.testng.Assert.assertEquals( + testTools.getBooleanParam(params, "nullKey", false), Boolean.FALSE); + org.testng.Assert.assertEquals( + testTools.getBooleanParam(params, "nonExistentKey", false), Boolean.FALSE); + } + + @Test + public void getLongParam_shouldReturnValue_whenKeyExists() { + Map params = new HashMap<>(); + params.put("longKey", 123L); + params.put("intKey", 456); + params.put("stringKey", "789"); + params.put("nullKey", null); + + org.testng.Assert.assertEquals( + testTools.getLongParam(params, "longKey", 0L), Long.valueOf(123L)); + org.testng.Assert.assertEquals( + testTools.getLongParam(params, "intKey", 0L), Long.valueOf(456L)); + org.testng.Assert.assertEquals( + testTools.getLongParam(params, "stringKey", 0L), Long.valueOf(789L)); + org.testng.Assert.assertEquals(testTools.getLongParam(params, "nullKey", 0L), Long.valueOf(0L)); + org.testng.Assert.assertEquals( + testTools.getLongParam(params, "nonExistentKey", 0L), Long.valueOf(0L)); + } + + @Test + public void getLongParam_shouldReturnDefault_whenInvalidFormat() { + Map params = new HashMap<>(); + params.put("invalidKey", "not-a-number"); + + org.testng.Assert.assertEquals( + testTools.getLongParam(params, "invalidKey", 0L), Long.valueOf(0L)); + } + + @Test + public void buildFullTopicName_shouldBuildPersistentTopic_whenTopicStartsWithPersistent() { + Map params = new HashMap<>(); + params.put("topic", "persistent://tenant/namespace/topic"); + + String result = testTools.buildFullTopicName(params); + org.testng.Assert.assertEquals(result, "persistent://tenant/namespace/topic"); + } + + @Test + public void buildFullTopicName_shouldBuildNonPersistentTopic_whenTopicStartsWithNonPersistent() { + Map params = new HashMap<>(); + params.put("topic", "non-persistent://tenant/namespace/topic"); + + String result = testTools.buildFullTopicName(params); + org.testng.Assert.assertEquals(result, "non-persistent://tenant/namespace/topic"); + } + + @Test + public void buildFullTopicName_shouldBuildTopicFromComponents() { + Map params = new HashMap<>(); + params.put("topic", "my-topic"); + params.put("tenant", "my-tenant"); + params.put("namespace", "my-namespace"); + params.put("persistent", true); + + String result = testTools.buildFullTopicName(params); + org.testng.Assert.assertEquals(result, "persistent://my-tenant/my-namespace/my-topic"); + } + + @Test + public void buildFullTopicName_shouldBuildNonPersistentTopicFromComponents() { + Map params = new HashMap<>(); + params.put("topic", "my-topic"); + params.put("tenant", "my-tenant"); + params.put("namespace", "my-namespace"); + params.put("persistent", false); + + String result = testTools.buildFullTopicName(params); + org.testng.Assert.assertEquals(result, "non-persistent://my-tenant/my-namespace/my-topic"); + } + + @Test + public void buildFullTopicName_shouldUseDefaultValues() { + Map params = new HashMap<>(); + params.put("topic", "my-topic"); + + String result = testTools.buildFullTopicName(params); + org.testng.Assert.assertEquals(result, "persistent://public/default/my-topic"); + } + + @Test + public void resolveNamespace_shouldReturnFullNamespace_whenContainsSlash() { + Map params = new HashMap<>(); + params.put("namespace", "tenant/namespace"); + + String result = testTools.resolveNamespace(params); + org.testng.Assert.assertEquals(result, "tenant/namespace"); + } + + @Test + public void resolveNamespace_shouldBuildFromTenantAndNamespace() { + Map params = new HashMap<>(); + params.put("tenant", "my-tenant"); + params.put("namespace", "my-namespace"); + + String result = testTools.resolveNamespace(params); + org.testng.Assert.assertEquals(result, "my-tenant/my-namespace"); + } + + @Test + public void resolveNamespace_shouldUseDefaultValues() { + Map params = new HashMap<>(); + String result = testTools.resolveNamespace(params); + org.testng.Assert.assertTrue( + "public/default".equals(result) || "/".equals(result), + "Unexpected default namespace: " + result); + } + + @Test + public void addTopicBreakdown_shouldBreakDownPersistentTopic() { + Map result = new HashMap<>(); + testTools.addTopicBreakdown(result, "persistent://tenant/namespace/topic"); + + org.testng.Assert.assertEquals(result.get("tenant"), "tenant"); + org.testng.Assert.assertEquals(result.get("namespace"), "namespace"); + org.testng.Assert.assertEquals(result.get("topicName"), "topic"); + } + + @Test + public void addTopicBreakdown_shouldBreakDownNonPersistentTopic() { + Map result = new HashMap<>(); + testTools.addTopicBreakdown(result, "non-persistent://tenant/namespace/topic"); + + org.testng.Assert.assertEquals(result.get("tenant"), "tenant"); + org.testng.Assert.assertEquals(result.get("namespace"), "namespace"); + org.testng.Assert.assertEquals(result.get("topicName"), "topic"); + } + + @Test + public void addTopicBreakdown_shouldNotBreakDownInvalidTopic() { + Map result = new HashMap<>(); + testTools.addTopicBreakdown(result, "invalid-topic"); + + org.testng.Assert.assertTrue(result.isEmpty()); + } + + @Test + public void createTool_shouldCreateToolWithCorrectProperties() { + McpSchema.Tool tool = BasePulsarTools.createTool("test-tool", "Test description", "{}"); + + org.testng.Assert.assertEquals(tool.name(), "test-tool"); + org.testng.Assert.assertEquals(tool.description(), "Test description"); + Object schema = tool.inputSchema(); + if (schema != null) { + try { + java.lang.reflect.Field props = schema.getClass().getDeclaredField("properties"); + props.setAccessible(true); + Object val = props.get(schema); + org.testng.Assert.assertNull(val, "JsonSchema.properties should be null for empty schema"); + } catch (Exception ignore) { + } + } else { + org.testng.Assert.fail("Unexpected inputSchema type: " + "null"); } + } - @Test - public void createErrorResult_shouldCreateErrorResultWithSuggestions() { - List suggestions = List.of("Suggestion 1", "Suggestion 2"); - McpSchema.CallToolResult result = testTools.createErrorResult("Test error", suggestions); - - org.testng.Assert.assertTrue(result.isError()); - org.testng.Assert.assertEquals(result.content().size(), 1); - org.testng.Assert.assertTrue(result.content().get(0) instanceof McpSchema.TextContent); - String content = ((McpSchema.TextContent) result.content().get(0)).text(); - org.testng.Assert.assertTrue(content.contains("Test error")); - org.testng.Assert.assertTrue(content.contains("Suggestion 1")); - org.testng.Assert.assertTrue(content.contains("Suggestion 2")); + private static class TestPulsarTools extends BasePulsarTools { + TestPulsarTools(PulsarAdmin pulsarAdmin) { + super(pulsarAdmin); } - @Test - public void getStringParam_shouldReturnValue_whenKeyExists() { - Map params = new HashMap<>(); - params.put("testKey", "testValue"); - params.put("nullKey", null); - - org.testng.Assert.assertEquals(testTools.getStringParam(params, "testKey"), "testValue"); - org.testng.Assert.assertEquals(testTools.getStringParam(params, "nullKey"), ""); - org.testng.Assert.assertEquals(testTools.getStringParam(params, "nonExistentKey"), ""); + @Override + public McpSchema.CallToolResult createSuccessResult(String message, Object data) { + return super.createSuccessResult(message, data); } - @Test - public void getRequiredStringParam_shouldReturnValue_whenKeyExistsAndNotEmpty() { - Map params = new HashMap<>(); - params.put("testKey", "testValue"); - params.put("emptyKey", " "); - params.put("nullKey", null); - - org.testng.Assert.assertEquals(testTools.getRequiredStringParam(params, "testKey"), "testValue"); - - // emptyKey - try { - testTools.getRequiredStringParam(params, "emptyKey"); - org.testng.Assert.fail("Expected IllegalArgumentException for emptyKey"); - } catch (IllegalArgumentException e) { - org.testng.Assert.assertTrue(e.getMessage().contains("Required parameter 'emptyKey' is missing")); - } - - // nullKey - try { - testTools.getRequiredStringParam(params, "nullKey"); - org.testng.Assert.fail("Expected IllegalArgumentException for nullKey"); - } catch (IllegalArgumentException e) { - org.testng.Assert.assertTrue(e.getMessage().contains("Required parameter 'nullKey' is missing")); - } - - // nonExistentKey - try { - testTools.getRequiredStringParam(params, "nonExistentKey"); - org.testng.Assert.fail("Expected IllegalArgumentException for nonExistentKey"); - } catch (IllegalArgumentException e) { - org.testng.Assert.assertTrue(e.getMessage().contains("Required parameter 'nonExistentKey' is missing")); - } + @Override + public McpSchema.CallToolResult createErrorResult(String message) { + return super.createErrorResult(message); } - @Test - public void getIntParam_shouldReturnValue_whenKeyExists() { - Map params = new HashMap<>(); - params.put("intKey", 42); - params.put("stringKey", "123"); - params.put("doubleKey", 45.6); - params.put("nullKey", null); - - org.testng.Assert.assertEquals(testTools.getIntParam(params, "intKey", 0), Integer.valueOf(42)); - org.testng.Assert.assertEquals(testTools.getIntParam(params, "stringKey", 0), Integer.valueOf(123)); - org.testng.Assert.assertEquals(testTools.getIntParam(params, "doubleKey", 0), Integer.valueOf(45)); - org.testng.Assert.assertEquals(testTools.getIntParam(params, "nullKey", 0), Integer.valueOf(0)); - org.testng.Assert.assertEquals(testTools.getIntParam(params, "nonExistentKey", 0), Integer.valueOf(0)); + @Override + public McpSchema.CallToolResult createErrorResult(String message, List suggestions) { + return super.createErrorResult(message, suggestions); } - @Test - public void getIntParam_shouldReturnDefault_whenInvalidFormat() { - Map params = new HashMap<>(); - params.put("invalidKey", "not-a-number"); - - org.testng.Assert.assertEquals(testTools.getIntParam(params, "invalidKey", 0), Integer.valueOf(0)); + @Override + public String getStringParam(Map map, String key) { + return super.getStringParam(map, key); } - @Test - public void getBooleanParam_shouldReturnValue_whenKeyExists() { - Map params = new HashMap<>(); - params.put("boolKey", true); - params.put("stringKey", "true"); - params.put("nullKey", null); - - org.testng.Assert.assertEquals(testTools.getBooleanParam(params, "boolKey", false), Boolean.TRUE); - org.testng.Assert.assertEquals(testTools.getBooleanParam(params, "stringKey", false), Boolean.TRUE); - org.testng.Assert.assertEquals(testTools.getBooleanParam(params, "nullKey", false), Boolean.FALSE); - org.testng.Assert.assertEquals(testTools.getBooleanParam(params, "nonExistentKey", false), Boolean.FALSE); + @Override + public String getRequiredStringParam(Map map, String key) { + return super.getRequiredStringParam(map, key); } - @Test - public void getLongParam_shouldReturnValue_whenKeyExists() { - Map params = new HashMap<>(); - params.put("longKey", 123L); - params.put("intKey", 456); - params.put("stringKey", "789"); - params.put("nullKey", null); - - org.testng.Assert.assertEquals(testTools.getLongParam(params, "longKey", 0L), Long.valueOf(123L)); - org.testng.Assert.assertEquals(testTools.getLongParam(params, "intKey", 0L), Long.valueOf(456L)); - org.testng.Assert.assertEquals(testTools.getLongParam(params, "stringKey", 0L), Long.valueOf(789L)); - org.testng.Assert.assertEquals(testTools.getLongParam(params, "nullKey", 0L), Long.valueOf(0L)); - org.testng.Assert.assertEquals(testTools.getLongParam(params, "nonExistentKey", 0L), Long.valueOf(0L)); + @Override + public Integer getIntParam(Map map, String key, Integer defaultValue) { + return super.getIntParam(map, key, defaultValue); } - @Test - public void getLongParam_shouldReturnDefault_whenInvalidFormat() { - Map params = new HashMap<>(); - params.put("invalidKey", "not-a-number"); - - org.testng.Assert.assertEquals(testTools.getLongParam(params, "invalidKey", 0L), Long.valueOf(0L)); + @Override + public Boolean getBooleanParam(Map map, String key, Boolean defaultValue) { + return super.getBooleanParam(map, key, defaultValue); } - @Test - public void buildFullTopicName_shouldBuildPersistentTopic_whenTopicStartsWithPersistent() { - Map params = new HashMap<>(); - params.put("topic", "persistent://tenant/namespace/topic"); - - String result = testTools.buildFullTopicName(params); - org.testng.Assert.assertEquals(result, "persistent://tenant/namespace/topic"); + @Override + public Long getLongParam(Map arguments, String timestamp, Long defaultValue) { + return super.getLongParam(arguments, timestamp, defaultValue); } - @Test - public void buildFullTopicName_shouldBuildNonPersistentTopic_whenTopicStartsWithNonPersistent() { - Map params = new HashMap<>(); - params.put("topic", "non-persistent://tenant/namespace/topic"); - - String result = testTools.buildFullTopicName(params); - org.testng.Assert.assertEquals(result, "non-persistent://tenant/namespace/topic"); - } - - @Test - public void buildFullTopicName_shouldBuildTopicFromComponents() { - Map params = new HashMap<>(); - params.put("topic", "my-topic"); - params.put("tenant", "my-tenant"); - params.put("namespace", "my-namespace"); - params.put("persistent", true); - - String result = testTools.buildFullTopicName(params); - org.testng.Assert.assertEquals(result, "persistent://my-tenant/my-namespace/my-topic"); - } - - @Test - public void buildFullTopicName_shouldBuildNonPersistentTopicFromComponents() { - Map params = new HashMap<>(); - params.put("topic", "my-topic"); - params.put("tenant", "my-tenant"); - params.put("namespace", "my-namespace"); - params.put("persistent", false); - - String result = testTools.buildFullTopicName(params); - org.testng.Assert.assertEquals(result, "non-persistent://my-tenant/my-namespace/my-topic"); - } - - @Test - public void buildFullTopicName_shouldUseDefaultValues() { - Map params = new HashMap<>(); - params.put("topic", "my-topic"); - - String result = testTools.buildFullTopicName(params); - org.testng.Assert.assertEquals(result, "persistent://public/default/my-topic"); - } - - @Test - public void resolveNamespace_shouldReturnFullNamespace_whenContainsSlash() { - Map params = new HashMap<>(); - params.put("namespace", "tenant/namespace"); - - String result = testTools.resolveNamespace(params); - org.testng.Assert.assertEquals(result, "tenant/namespace"); - } - - @Test - public void resolveNamespace_shouldBuildFromTenantAndNamespace() { - Map params = new HashMap<>(); - params.put("tenant", "my-tenant"); - params.put("namespace", "my-namespace"); - - String result = testTools.resolveNamespace(params); - org.testng.Assert.assertEquals(result, "my-tenant/my-namespace"); - } - - @Test - public void resolveNamespace_shouldUseDefaultValues() { - Map params = new HashMap<>(); - String result = testTools.resolveNamespace(params); - org.testng.Assert.assertTrue( - "public/default".equals(result) || "/".equals(result), - "Unexpected default namespace: " + result - ); - } - - - @Test - public void addTopicBreakdown_shouldBreakDownPersistentTopic() { - Map result = new HashMap<>(); - testTools.addTopicBreakdown(result, "persistent://tenant/namespace/topic"); - - org.testng.Assert.assertEquals(result.get("tenant"), "tenant"); - org.testng.Assert.assertEquals(result.get("namespace"), "namespace"); - org.testng.Assert.assertEquals(result.get("topicName"), "topic"); - } - - @Test - public void addTopicBreakdown_shouldBreakDownNonPersistentTopic() { - Map result = new HashMap<>(); - testTools.addTopicBreakdown(result, "non-persistent://tenant/namespace/topic"); - - org.testng.Assert.assertEquals(result.get("tenant"), "tenant"); - org.testng.Assert.assertEquals(result.get("namespace"), "namespace"); - org.testng.Assert.assertEquals(result.get("topicName"), "topic"); - } - - @Test - public void addTopicBreakdown_shouldNotBreakDownInvalidTopic() { - Map result = new HashMap<>(); - testTools.addTopicBreakdown(result, "invalid-topic"); - - org.testng.Assert.assertTrue(result.isEmpty()); + @Override + public String buildFullTopicName(Map arguments) { + return super.buildFullTopicName(arguments); } - @Test - public void createTool_shouldCreateToolWithCorrectProperties() { - McpSchema.Tool tool = BasePulsarTools.createTool("test-tool", "Test description", "{}"); - - org.testng.Assert.assertEquals(tool.name(), "test-tool"); - org.testng.Assert.assertEquals(tool.description(), "Test description"); - Object schema = tool.inputSchema(); - if (schema != null) { - try { - java.lang.reflect.Field props = schema.getClass().getDeclaredField("properties"); - props.setAccessible(true); - Object val = props.get(schema); - org.testng.Assert.assertNull(val, "JsonSchema.properties should be null for empty schema"); - } catch (Exception ignore) { - } - } else { - org.testng.Assert.fail("Unexpected inputSchema type: " + "null"); - } + @Override + public String resolveNamespace(Map arguments) { + return super.resolveNamespace(arguments); } - private static class TestPulsarTools extends BasePulsarTools { - TestPulsarTools(PulsarAdmin pulsarAdmin) { - super(pulsarAdmin); - } - - @Override - public McpSchema.CallToolResult createSuccessResult(String message, Object data) { - return super.createSuccessResult(message, data); - } - - @Override - public McpSchema.CallToolResult createErrorResult(String message) { - return super.createErrorResult(message); - } - - @Override - public McpSchema.CallToolResult createErrorResult(String message, List suggestions) { - return super.createErrorResult(message, suggestions); - } - - @Override - public String getStringParam(Map map, String key) { - return super.getStringParam(map, key); - } - - @Override - public String getRequiredStringParam(Map map, String key) { - return super.getRequiredStringParam(map, key); - } - - @Override - public Integer getIntParam(Map map, String key, Integer defaultValue) { - return super.getIntParam(map, key, defaultValue); - } - - @Override - public Boolean getBooleanParam(Map map, String key, Boolean defaultValue) { - return super.getBooleanParam(map, key, defaultValue); - } - - @Override - public Long getLongParam(Map arguments, String timestamp, Long defaultValue) { - return super.getLongParam(arguments, timestamp, defaultValue); - } - - @Override - public String buildFullTopicName(Map arguments) { - return super.buildFullTopicName(arguments); - } - - @Override - public String resolveNamespace(Map arguments) { - return super.resolveNamespace(arguments); - } - - @Override - public void addTopicBreakdown(Map result, String fullTopicName) { - super.addTopicBreakdown(result, fullTopicName); - } + @Override + public void addTopicBreakdown(Map result, String fullTopicName) { + super.addTopicBreakdown(result, fullTopicName); } + } } diff --git a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/HttpMCPServerTest.java b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/HttpMCPServerTest.java index 4ffc962..925a8c6 100644 --- a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/HttpMCPServerTest.java +++ b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/HttpMCPServerTest.java @@ -26,185 +26,168 @@ public class HttpMCPServerTest { - @Mock - private PulsarClientManager mockPulsarClientManager; + @Mock private PulsarClientManager mockPulsarClientManager; - @Mock - private PulsarAdmin mockPulsarAdmin; + @Mock private PulsarAdmin mockPulsarAdmin; - @Mock - private PulsarClient mockPulsarClient; + @Mock private PulsarClient mockPulsarClient; - private HttpMCPServer server; - private AutoCloseable mocks; + private HttpMCPServer server; + private AutoCloseable mocks; - @BeforeMethod - public void setUp() { - this.mocks = MockitoAnnotations.openMocks(this); - this.server = new HttpMCPServer(); - } + @BeforeMethod + public void setUp() { + this.mocks = MockitoAnnotations.openMocks(this); + this.server = new HttpMCPServer(); + } - @AfterMethod - public void tearDown() throws Exception { - if (mocks != null) { - mocks.close(); - } - if (server != null) { - try { - server.stop(); - } catch (Exception ignore) { - - } - } + @AfterMethod + public void tearDown() throws Exception { + if (mocks != null) { + mocks.close(); } + if (server != null) { + try { + server.stop(); + } catch (Exception ignore) { - @Test( - expectedExceptions = IllegalStateException.class, - expectedExceptionsMessageRegExp = ".*PulsarClientManager not injected.*" - ) - public void start_shouldThrowException_whenPulsarClientManagerNotInjected() throws Exception { - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - server.start(options); + } } - - @Test( - expectedExceptions = RuntimeException.class, - expectedExceptionsMessageRegExp = ".*Failed to obtain PulsarAdmin from PulsarClientManager.*" - ) - public void start_shouldThrowException_whenPulsarAdminInitializationFails() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenThrow(new RuntimeException("Admin init failed")); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - server.start(options); + } + + @Test( + expectedExceptions = IllegalStateException.class, + expectedExceptionsMessageRegExp = ".*PulsarClientManager not injected.*") + public void start_shouldThrowException_whenPulsarClientManagerNotInjected() throws Exception { + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + server.start(options); + } + + @Test( + expectedExceptions = RuntimeException.class, + expectedExceptionsMessageRegExp = ".*Failed to obtain PulsarAdmin from PulsarClientManager.*") + public void start_shouldThrowException_whenPulsarAdminInitializationFails() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) + .thenThrow(new RuntimeException("Admin init failed")); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + server.start(options); + } + + @Test( + expectedExceptions = RuntimeException.class, + expectedExceptionsMessageRegExp = ".*Failed to obtain PulsarAdmin from PulsarClientManager.*") + public void start_shouldThrowException_whenPulsarClientInitializationFails() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()) + .thenThrow(new RuntimeException("Client init failed")); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + server.start(options); + } + + @Test + public void start_shouldStartServerSuccessfully_whenMocked() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {"--port", "9999"}); + server.start(options); + org.testng.Assert.assertNotNull(server); + org.mockito.Mockito.verify(mockPulsarClientManager).getAdmin(); + org.mockito.Mockito.verify(mockPulsarClientManager).getClient(); + } + + @Test + public void start_shouldWarn_whenAlreadyRunning() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + + try { + server.start(options); + server.start(options); + } catch (Exception e) { + org.testng.Assert.assertNotNull(e); } + } - @Test( - expectedExceptions = RuntimeException.class, - expectedExceptionsMessageRegExp = ".*Failed to obtain PulsarAdmin from PulsarClientManager.*" - ) - public void start_shouldThrowException_whenPulsarClientInitializationFails() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenThrow(new RuntimeException("Client init failed")); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - server.start(options); - } + @Test + public void stop_shouldStopServerSuccessfully() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); - @Test - public void start_shouldStartServerSuccessfully_whenMocked() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{"--port", "9999"}); - server.start(options); - org.testng.Assert.assertNotNull(server); - org.mockito.Mockito.verify(mockPulsarClientManager).getAdmin(); - org.mockito.Mockito.verify(mockPulsarClientManager).getClient(); - } + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + + try { + server.start(options); + } catch (Exception e) { - @Test - public void start_shouldWarn_whenAlreadyRunning() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - - try { - server.start(options); - server.start(options); - } catch (Exception e) { - org.testng.Assert.assertNotNull(e); - } } - @Test - public void stop_shouldStopServerSuccessfully() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); + server.stop(); + org.mockito.Mockito.verify(mockPulsarClientManager).close(); + } - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); + @Test + public void stop_shouldHandleWhenNotStarted() { + server.stop(); + } - try { - server.start(options); - } catch (Exception e) { + @Test + public void stop_shouldBeIdempotent() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); - } + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); - server.stop(); - org.mockito.Mockito.verify(mockPulsarClientManager).close(); - } + try { + server.start(options); + } catch (Exception e) { - @Test - public void stop_shouldHandleWhenNotStarted() { - server.stop(); } + server.stop(); + server.stop(); + server.stop(); - @Test - public void stop_shouldBeIdempotent() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); + org.testng.Assert.assertNotNull(server); + } - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); + @Test + public void getType_shouldReturnHttp() { + org.testng.Assert.assertEquals(server.getType(), PulsarMCPCliOptions.TransportType.HTTP); + } - try { - server.start(options); - } catch (Exception e) { + @Test + public void stop_shouldClosePulsarClientManager() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); - } - server.stop(); - server.stop(); - server.stop(); + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); - org.testng.Assert.assertNotNull(server); - } + try { + server.start(options); + } catch (Exception e) { - @Test - public void getType_shouldReturnHttp() { - org.testng.Assert.assertEquals(server.getType(), PulsarMCPCliOptions.TransportType.HTTP); } + org.mockito.Mockito.verify(mockPulsarClientManager, org.mockito.Mockito.never()).close(); - @Test - public void stop_shouldClosePulsarClientManager() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - - try { - server.start(options); - } catch (Exception e) { + server.stop(); - } - org.mockito.Mockito.verify(mockPulsarClientManager, org.mockito.Mockito.never()).close(); + org.mockito.Mockito.verify(mockPulsarClientManager, org.mockito.Mockito.times(1)).close(); + } - server.stop(); - - org.mockito.Mockito.verify(mockPulsarClientManager, org.mockito.Mockito.times(1)).close(); - } - - private void injectPulsarClientManager() throws Exception { - Field field = AbstractMCPServer.class.getDeclaredField("pulsarClientManager"); - field.setAccessible(true); - field.set(server, mockPulsarClientManager); - } + private void injectPulsarClientManager() throws Exception { + Field field = AbstractMCPServer.class.getDeclaredField("pulsarClientManager"); + field.setAccessible(true); + field.set(server, mockPulsarClientManager); + } } diff --git a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/StdioMCPServerTest.java b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/StdioMCPServerTest.java index d162972..0116552 100644 --- a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/StdioMCPServerTest.java +++ b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/StdioMCPServerTest.java @@ -26,186 +26,166 @@ public class StdioMCPServerTest { - @Mock - private PulsarClientManager mockPulsarClientManager; + @Mock private PulsarClientManager mockPulsarClientManager; - @Mock - private PulsarAdmin mockPulsarAdmin; + @Mock private PulsarAdmin mockPulsarAdmin; - @Mock - private PulsarClient mockPulsarClient; + @Mock private PulsarClient mockPulsarClient; - private StdioMCPServer server; - private AutoCloseable mocks; + private StdioMCPServer server; + private AutoCloseable mocks; - @BeforeMethod - public void setUp() { - this.mocks = MockitoAnnotations.openMocks(this); - this.server = new StdioMCPServer(); - } - - @AfterMethod - public void tearDown() throws Exception { - if (mocks != null) { - mocks.close(); - } - if (server != null) { - try { - server.stop(); - } catch (Exception ignore) { - } - } - } - - @Test( - expectedExceptions = IllegalStateException.class, - expectedExceptionsMessageRegExp = ".*PulsarClientManager not injected.*" - ) - public void start_shouldThrowException_whenPulsarClientManagerNotInjected() { - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - server.start(options); - } - - @Test( - expectedExceptions = RuntimeException.class, - expectedExceptionsMessageRegExp = ".*Cannot start MCP server without Pulsar connection.*" - ) - public void start_shouldThrowException_whenPulsarAdminInitializationFails() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenThrow(new RuntimeException("Admin init failed")); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - server.start(options); - } - - @Test( - expectedExceptions = RuntimeException.class, - expectedExceptionsMessageRegExp = ".*Cannot start MCP server without Pulsar connection.*" - ) - public void start_shouldThrowException_whenPulsarClientInitializationFails() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenThrow(new RuntimeException("Client init failed")); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - server.start(options); - } - - @Test - public void start_shouldStartServerSuccessfully() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - server.start(options); - - org.testng.Assert.assertNotNull(server); - } - - @Test - public void start_shouldWarn_whenAlreadyRunning() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - - server.start(options); - server.start(options); - - org.testng.Assert.assertNotNull(server); - } - - @Test - public void stop_shouldStopServerSuccessfully() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - server.start(options); - - server.stop(); - org.mockito.Mockito.verify(mockPulsarClientManager).close(); - } + @BeforeMethod + public void setUp() { + this.mocks = MockitoAnnotations.openMocks(this); + this.server = new StdioMCPServer(); + } - @Test - public void stop_shouldHandleWhenNotStarted() { - server.stop(); + @AfterMethod + public void tearDown() throws Exception { + if (mocks != null) { + mocks.close(); } - - @Test - public void stop_shouldBeIdempotent() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - server.start(options); - - server.stop(); + if (server != null) { + try { server.stop(); - server.stop(); - - org.testng.Assert.assertNotNull(server); - } - - @Test - public void getType_shouldReturnStdio() { - org.testng.Assert.assertEquals(server.getType(), PulsarMCPCliOptions.TransportType.STDIO); - } - - @Test - public void start_shouldHandleConcurrentCalls() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - - server.start(options); - server.start(options); - server.start(options); - - org.testng.Assert.assertNotNull(server); + } catch (Exception ignore) { + } } + } + + @Test( + expectedExceptions = IllegalStateException.class, + expectedExceptionsMessageRegExp = ".*PulsarClientManager not injected.*") + public void start_shouldThrowException_whenPulsarClientManagerNotInjected() { + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + server.start(options); + } + + @Test( + expectedExceptions = RuntimeException.class, + expectedExceptionsMessageRegExp = ".*Cannot start MCP server without Pulsar connection.*") + public void start_shouldThrowException_whenPulsarAdminInitializationFails() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) + .thenThrow(new RuntimeException("Admin init failed")); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + server.start(options); + } + + @Test( + expectedExceptions = RuntimeException.class, + expectedExceptionsMessageRegExp = ".*Cannot start MCP server without Pulsar connection.*") + public void start_shouldThrowException_whenPulsarClientInitializationFails() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()) + .thenThrow(new RuntimeException("Client init failed")); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + server.start(options); + } + + @Test + public void start_shouldStartServerSuccessfully() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + server.start(options); + + org.testng.Assert.assertNotNull(server); + } + + @Test + public void start_shouldWarn_whenAlreadyRunning() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + + server.start(options); + server.start(options); + + org.testng.Assert.assertNotNull(server); + } + + @Test + public void stop_shouldStopServerSuccessfully() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + server.start(options); + + server.stop(); + org.mockito.Mockito.verify(mockPulsarClientManager).close(); + } + + @Test + public void stop_shouldHandleWhenNotStarted() { + server.stop(); + } + + @Test + public void stop_shouldBeIdempotent() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + server.start(options); + + server.stop(); + server.stop(); + server.stop(); + + org.testng.Assert.assertNotNull(server); + } + + @Test + public void getType_shouldReturnStdio() { + org.testng.Assert.assertEquals(server.getType(), PulsarMCPCliOptions.TransportType.STDIO); + } + + @Test + public void start_shouldHandleConcurrentCalls() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + + server.start(options); + server.start(options); + server.start(options); + + org.testng.Assert.assertNotNull(server); + } + + @Test + public void stop_shouldClosePulsarClientManager() throws Exception { + injectPulsarClientManager(); + org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()).thenReturn(mockPulsarAdmin); + org.mockito.Mockito.when(mockPulsarClientManager.getClient()).thenReturn(mockPulsarClient); + + PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[] {}); + server.start(options); + + org.mockito.Mockito.verify(mockPulsarClientManager, org.mockito.Mockito.times(0)).close(); - @Test - public void stop_shouldClosePulsarClientManager() throws Exception { - injectPulsarClientManager(); - org.mockito.Mockito.when(mockPulsarClientManager.getAdmin()) - .thenReturn(mockPulsarAdmin); - org.mockito.Mockito.when(mockPulsarClientManager.getClient()) - .thenReturn(mockPulsarClient); - - PulsarMCPCliOptions options = PulsarMCPCliOptions.parseArgs(new String[]{}); - server.start(options); + server.stop(); - org.mockito.Mockito.verify(mockPulsarClientManager, org.mockito.Mockito.times(0)).close(); + org.mockito.Mockito.verify(mockPulsarClientManager, org.mockito.Mockito.times(1)).close(); + } - server.stop(); - - org.mockito.Mockito.verify(mockPulsarClientManager, org.mockito.Mockito.times(1)).close(); - } - - private void injectPulsarClientManager() throws Exception { - Field field = AbstractMCPServer.class.getDeclaredField("pulsarClientManager"); - field.setAccessible(true); - field.set(server, mockPulsarClientManager); - } + private void injectPulsarClientManager() throws Exception { + Field field = AbstractMCPServer.class.getDeclaredField("pulsarClientManager"); + field.setAccessible(true); + field.set(server, mockPulsarClientManager); + } } - diff --git a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/TransportManagerTest.java b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/TransportManagerTest.java index d474460..0ed6fa9 100644 --- a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/TransportManagerTest.java +++ b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/TransportManagerTest.java @@ -26,140 +26,139 @@ public class TransportManagerTest { - private TransportManager mgr; - private PulsarMCPCliOptions opts; + private TransportManager mgr; + private PulsarMCPCliOptions opts; - @BeforeMethod - public void setUp() { - mgr = new TransportManager(); - opts = new PulsarMCPCliOptions(); - } + @BeforeMethod + public void setUp() { + mgr = new TransportManager(); + opts = new PulsarMCPCliOptions(); + } - @Test - public void startTransport_shouldInvokeRegisteredTransport_withPassedOptions() throws Exception { - Transport http = mock(Transport.class); - when(http.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); + @Test + public void startTransport_shouldInvokeRegisteredTransport_withPassedOptions() throws Exception { + Transport http = mock(Transport.class); + when(http.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); - mgr.registerTransport(http); - mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); + mgr.registerTransport(http); + mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); - verify(http, times(1)).start(opts); - verify(http, times(1)).getType(); - verifyNoMoreInteractions(http); - } + verify(http, times(1)).start(opts); + verify(http, times(1)).getType(); + verifyNoMoreInteractions(http); + } - @Test( - expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = ".*Transport not registered.*" - ) - public void startTransport_shouldThrow_whenTransportNotRegistered() throws Exception { - mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); - } + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = ".*Transport not registered.*") + public void startTransport_shouldThrow_whenTransportNotRegistered() throws Exception { + mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); + } - @Test - public void registerTransport_shouldOverridePrevious_whenSameTypeRegisteredAgain() throws Exception { - Transport t1 = mock(Transport.class); - when(t1.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); + @Test + public void registerTransport_shouldOverridePrevious_whenSameTypeRegisteredAgain() + throws Exception { + Transport t1 = mock(Transport.class); + when(t1.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); - Transport t2 = mock(Transport.class); - when(t2.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); - - mgr.registerTransport(t1); - mgr.registerTransport(t2); - - mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); - - verify(t2, times(1)).start(opts); - verify(t1, never()).start(any()); - } - - @Test - public void registerTransport_shouldQueryTypeOnce_andStoreByType() { - Transport any = mock(Transport.class); - when(any.getType()).thenReturn(PulsarMCPCliOptions.TransportType.STDIO); - - mgr.registerTransport(any); - - verify(any, times(1)).getType(); - verifyNoMoreInteractions(any); - } - - @Test - public void registerTransport_shouldAllowMultipleDifferentTypes() throws Exception { - Transport stdio = mock(Transport.class); - when(stdio.getType()).thenReturn(PulsarMCPCliOptions.TransportType.STDIO); - - Transport http = mock(Transport.class); - when(http.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); - - mgr.registerTransport(stdio); - mgr.registerTransport(http); - - mgr.startTransport(PulsarMCPCliOptions.TransportType.STDIO, opts); - mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); - - verify(stdio, times(1)).start(opts); - verify(http, times(1)).start(opts); - } - - @Test - public void startTransport_shouldWorkAfterMultipleRegistrations() throws Exception { - Transport transport1 = mock(Transport.class); - when(transport1.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); - - Transport transport2 = mock(Transport.class); - when(transport2.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); - - mgr.registerTransport(transport1); - mgr.registerTransport(transport2); - - mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); - - verify(transport2, times(1)).start(opts); - verify(transport1, never()).start(any()); - } - - @Test - public void startTransport_shouldPassNullOptionsToTransport() throws Exception { - Transport http = mock(Transport.class); - when(http.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); - - mgr.registerTransport(http); - mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, null); - - verify(http, times(1)).start(null); - } - - @Test - public void startTransport_shouldCallStartMethod_afterRegistration() throws Exception { - Transport stdio = mock(Transport.class); - when(stdio.getType()).thenReturn(PulsarMCPCliOptions.TransportType.STDIO); + Transport t2 = mock(Transport.class); + when(t2.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); - mgr.registerTransport(stdio); - mgr.startTransport(PulsarMCPCliOptions.TransportType.STDIO, opts); + mgr.registerTransport(t1); + mgr.registerTransport(t2); - verify(stdio, times(1)).start(opts); - } + mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); - @Test - public void registerTransport_shouldHandleConcurrentRegistration() { - Transport transport1 = mock(Transport.class); - Transport transport2 = mock(Transport.class); - when(transport1.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); - when(transport2.getType()).thenReturn(PulsarMCPCliOptions.TransportType.STDIO); + verify(t2, times(1)).start(opts); + verify(t1, never()).start(any()); + } - mgr.registerTransport(transport1); - mgr.registerTransport(transport2); + @Test + public void registerTransport_shouldQueryTypeOnce_andStoreByType() { + Transport any = mock(Transport.class); + when(any.getType()).thenReturn(PulsarMCPCliOptions.TransportType.STDIO); - verify(transport1, times(1)).getType(); - verify(transport2, times(1)).getType(); - } + mgr.registerTransport(any); - @Test( - expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = ".*Transport not registered.*" - ) - public void startTransport_shouldThrow_whenTransportTypeNotRegistered() throws Exception { - mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); - } + verify(any, times(1)).getType(); + verifyNoMoreInteractions(any); + } + + @Test + public void registerTransport_shouldAllowMultipleDifferentTypes() throws Exception { + Transport stdio = mock(Transport.class); + when(stdio.getType()).thenReturn(PulsarMCPCliOptions.TransportType.STDIO); + + Transport http = mock(Transport.class); + when(http.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); + + mgr.registerTransport(stdio); + mgr.registerTransport(http); + + mgr.startTransport(PulsarMCPCliOptions.TransportType.STDIO, opts); + mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); + + verify(stdio, times(1)).start(opts); + verify(http, times(1)).start(opts); + } + + @Test + public void startTransport_shouldWorkAfterMultipleRegistrations() throws Exception { + Transport transport1 = mock(Transport.class); + when(transport1.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); + + Transport transport2 = mock(Transport.class); + when(transport2.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); + + mgr.registerTransport(transport1); + mgr.registerTransport(transport2); + + mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); + + verify(transport2, times(1)).start(opts); + verify(transport1, never()).start(any()); + } + + @Test + public void startTransport_shouldPassNullOptionsToTransport() throws Exception { + Transport http = mock(Transport.class); + when(http.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); + + mgr.registerTransport(http); + mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, null); + + verify(http, times(1)).start(null); + } + + @Test + public void startTransport_shouldCallStartMethod_afterRegistration() throws Exception { + Transport stdio = mock(Transport.class); + when(stdio.getType()).thenReturn(PulsarMCPCliOptions.TransportType.STDIO); + + mgr.registerTransport(stdio); + mgr.startTransport(PulsarMCPCliOptions.TransportType.STDIO, opts); + + verify(stdio, times(1)).start(opts); + } + + @Test + public void registerTransport_shouldHandleConcurrentRegistration() { + Transport transport1 = mock(Transport.class); + Transport transport2 = mock(Transport.class); + when(transport1.getType()).thenReturn(PulsarMCPCliOptions.TransportType.HTTP); + when(transport2.getType()).thenReturn(PulsarMCPCliOptions.TransportType.STDIO); + + mgr.registerTransport(transport1); + mgr.registerTransport(transport2); + + verify(transport1, times(1)).getType(); + verify(transport2, times(1)).getType(); + } + + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = ".*Transport not registered.*") + public void startTransport_shouldThrow_whenTransportTypeNotRegistered() throws Exception { + mgr.startTransport(PulsarMCPCliOptions.TransportType.HTTP, opts); + } } diff --git a/pulsar-auth-contrib/pom.xml b/pulsar-auth-contrib/pom.xml index 95ba76e..f42740c 100644 --- a/pulsar-auth-contrib/pom.xml +++ b/pulsar-auth-contrib/pom.xml @@ -21,8 +21,8 @@ pulsar-java-contrib 1.0.0-SNAPSHOT - 2024 pulsar-auth-contrib + 2024 diff --git a/pulsar-bookkeeper-contrib/pom.xml b/pulsar-bookkeeper-contrib/pom.xml index 90c2125..4f54bbd 100644 --- a/pulsar-bookkeeper-contrib/pom.xml +++ b/pulsar-bookkeeper-contrib/pom.xml @@ -21,8 +21,8 @@ pulsar-java-contrib 1.0.0-SNAPSHOT - 2024 pulsar-bookkeeper-contrib + 2024 diff --git a/pulsar-client-common-contrib/pom.xml b/pulsar-client-common-contrib/pom.xml index 722fa20..db36c7d 100644 --- a/pulsar-client-common-contrib/pom.xml +++ b/pulsar-client-common-contrib/pom.xml @@ -22,7 +22,6 @@ pulsar-java-contrib 1.0.0-SNAPSHOT - 2024 pulsar-client-common-contrib @@ -32,5 +31,6 @@ pulsar-client-all + 2024 diff --git a/pulsar-connector-contrib/pom.xml b/pulsar-connector-contrib/pom.xml index 45335ed..a84bf4e 100644 --- a/pulsar-connector-contrib/pom.xml +++ b/pulsar-connector-contrib/pom.xml @@ -21,7 +21,7 @@ pulsar-java-contrib 1.0.0-SNAPSHOT - 2024 pulsar-connector-contrib + 2024 diff --git a/pulsar-function-contrib/pom.xml b/pulsar-function-contrib/pom.xml index 41ffcf7..6e535e8 100644 --- a/pulsar-function-contrib/pom.xml +++ b/pulsar-function-contrib/pom.xml @@ -21,8 +21,8 @@ pulsar-java-contrib 1.0.0-SNAPSHOT - 2024 pulsar-function-contrib + 2024 diff --git a/pulsar-interceptor-contrib/pom.xml b/pulsar-interceptor-contrib/pom.xml index f5f25d1..7d6234c 100644 --- a/pulsar-interceptor-contrib/pom.xml +++ b/pulsar-interceptor-contrib/pom.xml @@ -21,8 +21,8 @@ pulsar-java-contrib 1.0.0-SNAPSHOT - 2024 pulsar-interceptor-contrib + 2024 diff --git a/pulsar-loadbalance-contrib/pom.xml b/pulsar-loadbalance-contrib/pom.xml index 50438e3..449bda4 100644 --- a/pulsar-loadbalance-contrib/pom.xml +++ b/pulsar-loadbalance-contrib/pom.xml @@ -21,8 +21,8 @@ pulsar-java-contrib 1.0.0-SNAPSHOT - 2024 pulsar-loadbalance-contrib + 2024 diff --git a/pulsar-metrics-contrib/pom.xml b/pulsar-metrics-contrib/pom.xml index bd5e4e6..aa27929 100644 --- a/pulsar-metrics-contrib/pom.xml +++ b/pulsar-metrics-contrib/pom.xml @@ -21,7 +21,7 @@ pulsar-java-contrib 1.0.0-SNAPSHOT - 2024 pulsar-metrics-contrib + 2024 diff --git a/pulsar-rpc-contrib/pom.xml b/pulsar-rpc-contrib/pom.xml index c3ad616..0f56e50 100644 --- a/pulsar-rpc-contrib/pom.xml +++ b/pulsar-rpc-contrib/pom.xml @@ -14,8 +14,7 @@ limitations under the License. --> - + 4.0.0 @@ -23,23 +22,22 @@ pulsar-java-contrib 1.0.0-SNAPSHOT - 2024 pulsar-rpc-contrib - org.apache.pulsar - pulsar-client-admin - test + org.apache.commons + commons-pool2 org.apache.pulsar pulsar-client - org.apache.commons - commons-pool2 + org.apache.pulsar + pulsar-client-admin + test org.awaitility @@ -55,10 +53,11 @@ - src/main/resources true + src/main/resources + 2024 - \ No newline at end of file + diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/DefaultRequestCallBack.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/DefaultRequestCallBack.java index 66f4861..82604c2 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/DefaultRequestCallBack.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/DefaultRequestCallBack.java @@ -20,45 +20,53 @@ import org.apache.pulsar.client.api.MessageId; /** - * Default implementation of {@link RequestCallBack} that handles callback events for Pulsar RPC communications. + * Default implementation of {@link RequestCallBack} that handles callback events for Pulsar RPC + * communications. */ @Slf4j public class DefaultRequestCallBack implements RequestCallBack { - @Override - public void onSendRequestSuccess(String correlationId, MessageId messageId) { + @Override + public void onSendRequestSuccess(String correlationId, MessageId messageId) {} - } + @Override + public void onSendRequestError( + String correlationId, Throwable t, CompletableFuture replyFuture) { + replyFuture.completeExceptionally(t); + } - @Override - public void onSendRequestError(String correlationId, Throwable t, - CompletableFuture replyFuture) { - replyFuture.completeExceptionally(t); - } + @Override + public void onReplySuccess( + String correlationId, String subscription, V value, CompletableFuture replyFuture) { + replyFuture.complete(value); + } - @Override - public void onReplySuccess(String correlationId, String subscription, - V value, CompletableFuture replyFuture) { - replyFuture.complete(value); - } + @Override + public void onReplyError( + String correlationId, + String subscription, + String errorMessage, + CompletableFuture replyFuture) { + replyFuture.completeExceptionally(new Exception(errorMessage)); + } - @Override - public void onReplyError(String correlationId, String subscription, - String errorMessage, CompletableFuture replyFuture) { - replyFuture.completeExceptionally(new Exception(errorMessage)); - } + @Override + public void onTimeout(String correlationId, Throwable t) {} - @Override - public void onTimeout(String correlationId, Throwable t) { - - } - - @Override - public void onReplyMessageAckFailed(String correlationId, Consumer consumer, Message msg, Throwable t) { - consumer.acknowledgeAsync(msg.getMessageId()).exceptionally(ex -> { - log.warn(" [{}] [{}] Acknowledging message {} failed again.", - msg.getTopicName(), correlationId, msg.getMessageId(), ex); - return null; - }); - } + @Override + public void onReplyMessageAckFailed( + String correlationId, Consumer consumer, Message msg, Throwable t) { + consumer + .acknowledgeAsync(msg.getMessageId()) + .exceptionally( + ex -> { + log.warn( + " [{}] [{}] Acknowledging message {} failed again.", + msg.getTopicName(), + correlationId, + msg.getMessageId(), + ex); + return null; + }); + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClient.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClient.java index 3da5528..961980c 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClient.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClient.java @@ -24,146 +24,153 @@ import org.apache.pulsar.shade.com.google.common.annotations.VisibleForTesting; /** - * Provides the functionality to send asynchronous requests and handle replies using Apache Pulsar as the - * messaging system. This client manages request-response interactions ensuring that messages are sent - * to the correct topics and handling responses through callbacks. + * Provides the functionality to send asynchronous requests and handle replies using Apache Pulsar + * as the messaging system. This client manages request-response interactions ensuring that messages + * are sent to the correct topics and handling responses through callbacks. * * @param The type of the request messages. * @param The type of the reply messages. */ public interface PulsarRpcClient extends AutoCloseable { - /** - * Creates a builder for configuring a new {@link PulsarRpcClient}. - * - * @return A new instance of {@link PulsarRpcClientBuilder}. - */ - static PulsarRpcClientBuilder builder(@NonNull Schema requestSchema, - @NonNull Schema replySchema) { - return new PulsarRpcClientBuilderImpl<>(requestSchema, replySchema); - } + /** + * Creates a builder for configuring a new {@link PulsarRpcClient}. + * + * @return A new instance of {@link PulsarRpcClientBuilder}. + */ + static PulsarRpcClientBuilder builder( + @NonNull Schema requestSchema, @NonNull Schema replySchema) { + return new PulsarRpcClientBuilderImpl<>(requestSchema, replySchema); + } - /** - * Synchronously sends a request and waits for the replies. - * - * @param correlationId A unique identifier for the request. - * @param value The value used to generate the request message - * @return The reply value. - * @throws PulsarRpcClientException if an error occurs during the request or while waiting for the reply. - */ - default V request(String correlationId, T value) throws PulsarRpcClientException { - return request(correlationId, value, Collections.emptyMap()); - } + /** + * Synchronously sends a request and waits for the replies. + * + * @param correlationId A unique identifier for the request. + * @param value The value used to generate the request message + * @return The reply value. + * @throws PulsarRpcClientException if an error occurs during the request or while waiting for the + * reply. + */ + default V request(String correlationId, T value) throws PulsarRpcClientException { + return request(correlationId, value, Collections.emptyMap()); + } - /** - * Synchronously sends a request and waits for the replies. - * - * @param correlationId A unique identifier for the request. - * @param value The value used to generate the request message - * @param config Configuration map for creating a request producer, - * will call {@link TypedMessageBuilder#loadConf(Map)} - * @return The reply value. - * @throws PulsarRpcClientException if an error occurs during the request or while waiting for the reply. - */ - V request(String correlationId, T value, Map config) throws PulsarRpcClientException; + /** + * Synchronously sends a request and waits for the replies. + * + * @param correlationId A unique identifier for the request. + * @param value The value used to generate the request message + * @param config Configuration map for creating a request producer, will call {@link + * TypedMessageBuilder#loadConf(Map)} + * @return The reply value. + * @throws PulsarRpcClientException if an error occurs during the request or while waiting for the + * reply. + */ + V request(String correlationId, T value, Map config) + throws PulsarRpcClientException; - /** - * Asynchronously sends a request and returns a future that completes with the reply. - * - * @param correlationId A unique identifier for the request. - * @param value The value used to generate the request message - * @return A CompletableFuture that will complete with the reply value. - */ - default CompletableFuture requestAsync(String correlationId, T value) { - return requestAsync(correlationId, value, Collections.emptyMap()); - } + /** + * Asynchronously sends a request and returns a future that completes with the reply. + * + * @param correlationId A unique identifier for the request. + * @param value The value used to generate the request message + * @return A CompletableFuture that will complete with the reply value. + */ + default CompletableFuture requestAsync(String correlationId, T value) { + return requestAsync(correlationId, value, Collections.emptyMap()); + } - /** - * Asynchronously sends a request and returns a future that completes with the reply. - * - * @param correlationId A unique identifier for the request. - * @param value The value used to generate the request message - * @param config Configuration map for creating a request producer, - * will call {@link TypedMessageBuilder#loadConf(Map)} - * @return A CompletableFuture that will complete with the reply value. - */ - CompletableFuture requestAsync(String correlationId, T value, Map config); + /** + * Asynchronously sends a request and returns a future that completes with the reply. + * + * @param correlationId A unique identifier for the request. + * @param value The value used to generate the request message + * @param config Configuration map for creating a request producer, will call {@link + * TypedMessageBuilder#loadConf(Map)} + * @return A CompletableFuture that will complete with the reply value. + */ + CompletableFuture requestAsync(String correlationId, T value, Map config); - /** - * Deliver the message only at or after the specified absolute timestamp. - * Asynchronously sends a request and returns a future that completes with the reply. - * - * @param correlationId A unique identifier for the request. - * @param value The value used to generate the request message - * @param timestamp Absolute timestamp indicating when the message should be delivered to rpc-server. - * @return A CompletableFuture that will complete with the reply value. - */ - default CompletableFuture requestAtAsync(String correlationId, T value, long timestamp) { - return requestAtAsync(correlationId, value, Collections.emptyMap(), timestamp); - } + /** + * Deliver the message only at or after the specified absolute timestamp. Asynchronously sends a + * request and returns a future that completes with the reply. + * + * @param correlationId A unique identifier for the request. + * @param value The value used to generate the request message + * @param timestamp Absolute timestamp indicating when the message should be delivered to + * rpc-server. + * @return A CompletableFuture that will complete with the reply value. + */ + default CompletableFuture requestAtAsync(String correlationId, T value, long timestamp) { + return requestAtAsync(correlationId, value, Collections.emptyMap(), timestamp); + } - /** - * Deliver the message only at or after the specified absolute timestamp. - * Asynchronously sends a request and returns a future that completes with the reply. - * - * @param correlationId A unique identifier for the request. - * @param value The value used to generate the request message - * @param config Configuration map for creating a request producer, - * will call {@link TypedMessageBuilder#loadConf(Map)} - * @param timestamp Absolute timestamp indicating when the message should be delivered to rpc-server. - * @return A CompletableFuture that will complete with the reply value. - */ - CompletableFuture requestAtAsync(String correlationId, T value, Map config, - long timestamp); + /** + * Deliver the message only at or after the specified absolute timestamp. Asynchronously sends a + * request and returns a future that completes with the reply. + * + * @param correlationId A unique identifier for the request. + * @param value The value used to generate the request message + * @param config Configuration map for creating a request producer, will call {@link + * TypedMessageBuilder#loadConf(Map)} + * @param timestamp Absolute timestamp indicating when the message should be delivered to + * rpc-server. + * @return A CompletableFuture that will complete with the reply value. + */ + CompletableFuture requestAtAsync( + String correlationId, T value, Map config, long timestamp); - /** - * Request to deliver the message only after the specified relative delay. - * Asynchronously sends a request and returns a future that completes with the reply. - * - * @param correlationId A unique identifier for the request. - * @param value The value used to generate the request message - * @param delay The amount of delay before the message will be delivered. - * @param unit The time unit for the delay. - * @return A CompletableFuture that will complete with the reply value. - */ - default CompletableFuture requestAfterAsync(String correlationId, T value, long delay, TimeUnit unit) { - return requestAfterAsync(correlationId, value, Collections.emptyMap(), delay, unit); - } + /** + * Request to deliver the message only after the specified relative delay. Asynchronously sends a + * request and returns a future that completes with the reply. + * + * @param correlationId A unique identifier for the request. + * @param value The value used to generate the request message + * @param delay The amount of delay before the message will be delivered. + * @param unit The time unit for the delay. + * @return A CompletableFuture that will complete with the reply value. + */ + default CompletableFuture requestAfterAsync( + String correlationId, T value, long delay, TimeUnit unit) { + return requestAfterAsync(correlationId, value, Collections.emptyMap(), delay, unit); + } - /** - * Request to deliver the message only after the specified relative delay. - * Asynchronously sends a request and returns a future that completes with the reply. - * - * @param correlationId A unique identifier for the request. - * @param value The value used to generate the request message - * @param config Configuration map for creating a request producer, - * will call {@link TypedMessageBuilder#loadConf(Map)} - * @param delay The amount of delay before the message will be delivered. - * @param unit The time unit for the delay. - * @return A CompletableFuture that will complete with the reply value. - */ - CompletableFuture requestAfterAsync(String correlationId, T value, Map config, - long delay, TimeUnit unit); + /** + * Request to deliver the message only after the specified relative delay. Asynchronously sends a + * request and returns a future that completes with the reply. + * + * @param correlationId A unique identifier for the request. + * @param value The value used to generate the request message + * @param config Configuration map for creating a request producer, will call {@link + * TypedMessageBuilder#loadConf(Map)} + * @param delay The amount of delay before the message will be delivered. + * @param unit The time unit for the delay. + * @return A CompletableFuture that will complete with the reply value. + */ + CompletableFuture requestAfterAsync( + String correlationId, T value, Map config, long delay, TimeUnit unit); - /** - * Removes a request from the tracking map based on its correlation ID. - * - *

When this method is executed, ReplyListener the received message will not be processed again. - * You need to make sure that this request has been processed through the callback, or you need to resend it. - * - * @param correlationId The correlation ID of the request to remove. - */ - void removeRequest(String correlationId); + /** + * Removes a request from the tracking map based on its correlation ID. + * + *

When this method is executed, ReplyListener the received message will not be processed + * again. You need to make sure that this request has been processed through the callback, or you + * need to resend it. + * + * @param correlationId The correlation ID of the request to remove. + */ + void removeRequest(String correlationId); - @VisibleForTesting - int pendingRequestSize(); + @VisibleForTesting + int pendingRequestSize(); - /** - * Closes this client and releases any resources associated with it. This includes closing any active - * producers and consumers and clearing pending requests. - * - * @throws PulsarRpcClientException if there is an error during the closing process. - */ - @Override - void close() throws PulsarRpcClientException; + /** + * Closes this client and releases any resources associated with it. This includes closing any + * active producers and consumers and clearing pending requests. + * + * @throws PulsarRpcClientException if there is an error during the closing process. + */ + @Override + void close() throws PulsarRpcClientException; } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientBuilder.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientBuilder.java index 6fc9e0f..428770a 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientBuilder.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientBuilder.java @@ -22,85 +22,88 @@ /** * Builder class for constructing a {@link PulsarRpcClient} instance. This builder allows for the - * customization of various components required to establish a Pulsar RPC client, including - * schemas for serialization, topic details, and timeout configurations. + * customization of various components required to establish a Pulsar RPC client, including schemas + * for serialization, topic details, and timeout configurations. * * @param the type of request message * @param the type of reply message */ public interface PulsarRpcClientBuilder { - /** - * Specifies the Pulsar topic that this client will send to for requests. - * - * @param requestTopic the Pulsar topic name - * @return this builder instance for chaining - */ - PulsarRpcClientBuilder requestTopic(String requestTopic); + /** + * Specifies the Pulsar topic that this client will send to for requests. + * + * @param requestTopic the Pulsar topic name + * @return this builder instance for chaining + */ + PulsarRpcClientBuilder requestTopic(String requestTopic); - /** - * Specifies the producer configuration map for request messages. - * - * @param requestProducerConfig Configuration map for creating a request message - * producer, will call {@link org.apache.pulsar.client.api.ProducerBuilder#loadConf(java.util.Map)} - * @return this builder instance for chaining - */ - PulsarRpcClientBuilder requestProducerConfig(@NonNull Map requestProducerConfig); + /** + * Specifies the producer configuration map for request messages. + * + * @param requestProducerConfig Configuration map for creating a request message producer, will + * call {@link org.apache.pulsar.client.api.ProducerBuilder#loadConf(java.util.Map)} + * @return this builder instance for chaining + */ + PulsarRpcClientBuilder requestProducerConfig( + @NonNull Map requestProducerConfig); - /** - * Sets the topic on which reply messages will be sent. - * - * @param replyTopic the topic for reply messages - * @return this builder instance for chaining - */ - PulsarRpcClientBuilder replyTopic(@NonNull String replyTopic); + /** + * Sets the topic on which reply messages will be sent. + * + * @param replyTopic the topic for reply messages + * @return this builder instance for chaining + */ + PulsarRpcClientBuilder replyTopic(@NonNull String replyTopic); - /** - * Sets the pattern to subscribe to multiple reply topics dynamically. - * - * @param replyTopicsPattern the pattern matching reply topics - * @return this builder instance for chaining - */ - PulsarRpcClientBuilder replyTopicsPattern(@NonNull Pattern replyTopicsPattern); + /** + * Sets the pattern to subscribe to multiple reply topics dynamically. + * + * @param replyTopicsPattern the pattern matching reply topics + * @return this builder instance for chaining + */ + PulsarRpcClientBuilder replyTopicsPattern(@NonNull Pattern replyTopicsPattern); - /** - * Specifies the subscription name to use for reply messages. - * - * @param replySubscription the subscription name for reply messages - * @return this builder instance for chaining - */ - PulsarRpcClientBuilder replySubscription(@NonNull String replySubscription); + /** + * Specifies the subscription name to use for reply messages. + * + * @param replySubscription the subscription name for reply messages + * @return this builder instance for chaining + */ + PulsarRpcClientBuilder replySubscription(@NonNull String replySubscription); - /** - * Sets the timeout for reply messages. - * - * @param replyTimeout the duration to wait for a reply before timing out - * @return this builder instance for chaining - */ - PulsarRpcClientBuilder replyTimeout(@NonNull Duration replyTimeout); + /** + * Sets the timeout for reply messages. + * + * @param replyTimeout the duration to wait for a reply before timing out + * @return this builder instance for chaining + */ + PulsarRpcClientBuilder replyTimeout(@NonNull Duration replyTimeout); - /** - * Sets the interval for auto-discovery of topics matching the pattern. - * - * @param patternAutoDiscoveryInterval the interval for auto-discovering topics - * @return this builder instance for chaining - */ - PulsarRpcClientBuilder patternAutoDiscoveryInterval(@NonNull Duration patternAutoDiscoveryInterval); + /** + * Sets the interval for auto-discovery of topics matching the pattern. + * + * @param patternAutoDiscoveryInterval the interval for auto-discovering topics + * @return this builder instance for chaining + */ + PulsarRpcClientBuilder patternAutoDiscoveryInterval( + @NonNull Duration patternAutoDiscoveryInterval); - /** - * Sets the {@link RequestCallBack} handler for various request and reply events. - * - * @param callBack the callback handler to manage events - * @return this builder instance for chaining - */ - PulsarRpcClientBuilder requestCallBack(@NonNull RequestCallBack callBack); + /** + * Sets the {@link RequestCallBack} handler for various request and reply events. + * + * @param callBack the callback handler to manage events + * @return this builder instance for chaining + */ + PulsarRpcClientBuilder requestCallBack(@NonNull RequestCallBack callBack); - /** - * Builds and returns a {@link PulsarRpcClient} configured with the current builder settings. - * - * @param pulsarClient the client to use for connecting to server - * @return a new instance of {@link PulsarRpcClient} - * @throws PulsarRpcClientException if an error occurs during the building of the {@link PulsarRpcClient} - */ - PulsarRpcClient build(PulsarClient pulsarClient) throws PulsarRpcClientException; + /** + * Builds and returns a {@link PulsarRpcClient} configured with the current builder settings. + * + * @param pulsarClient the client to use for connecting to server + * @return a new instance of {@link PulsarRpcClient} + * @throws PulsarRpcClientException if an error occurs during the building of the {@link + * PulsarRpcClient} + */ + PulsarRpcClient build(PulsarClient pulsarClient) throws PulsarRpcClientException; } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientBuilderImpl.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientBuilderImpl.java index bcc2370..7c5db18 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientBuilderImpl.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientBuilderImpl.java @@ -28,75 +28,78 @@ @Slf4j @Getter(AccessLevel.PACKAGE) class PulsarRpcClientBuilderImpl implements PulsarRpcClientBuilder { - private final Schema requestSchema; - private final Schema replySchema; - private String requestTopic; - private Map requestProducerConfig; - private String replyTopic; - private String replySubscription; - private Duration replyTimeout = Duration.ofSeconds(3); - private Pattern replyTopicsPattern; - private Duration patternAutoDiscoveryInterval; - private RequestCallBack callBack; + private final Schema requestSchema; + private final Schema replySchema; + private String requestTopic; + private Map requestProducerConfig; + private String replyTopic; + private String replySubscription; + private Duration replyTimeout = Duration.ofSeconds(3); + private Pattern replyTopicsPattern; + private Duration patternAutoDiscoveryInterval; + private RequestCallBack callBack; - /** - * Constructs a PulsarRpcClientBuilderImpl with the necessary schemas for request and reply messages. - * - * @param requestSchema the schema for serializing request messages - * @param replySchema the schema for serializing reply messages - */ - public PulsarRpcClientBuilderImpl(@NonNull Schema requestSchema, @NonNull Schema replySchema) { - this.requestSchema = requestSchema; - this.replySchema = replySchema; - } + /** + * Constructs a PulsarRpcClientBuilderImpl with the necessary schemas for request and reply + * messages. + * + * @param requestSchema the schema for serializing request messages + * @param replySchema the schema for serializing reply messages + */ + public PulsarRpcClientBuilderImpl( + @NonNull Schema requestSchema, @NonNull Schema replySchema) { + this.requestSchema = requestSchema; + this.replySchema = replySchema; + } - public PulsarRpcClientBuilderImpl requestTopic(String requestTopic) { - this.requestTopic = requestTopic; - return this; - } + public PulsarRpcClientBuilderImpl requestTopic(String requestTopic) { + this.requestTopic = requestTopic; + return this; + } - public PulsarRpcClientBuilderImpl requestProducerConfig(@NonNull Map requestProducerConfig) { - this.requestProducerConfig = requestProducerConfig; - return this; - } + public PulsarRpcClientBuilderImpl requestProducerConfig( + @NonNull Map requestProducerConfig) { + this.requestProducerConfig = requestProducerConfig; + return this; + } - public PulsarRpcClientBuilderImpl replyTopic(@NonNull String replyTopic) { - this.replyTopic = replyTopic; - return this; - } + public PulsarRpcClientBuilderImpl replyTopic(@NonNull String replyTopic) { + this.replyTopic = replyTopic; + return this; + } - public PulsarRpcClientBuilderImpl replyTopicsPattern(@NonNull Pattern replyTopicsPattern) { - this.replyTopicsPattern = replyTopicsPattern; - return this; - } + public PulsarRpcClientBuilderImpl replyTopicsPattern(@NonNull Pattern replyTopicsPattern) { + this.replyTopicsPattern = replyTopicsPattern; + return this; + } - public PulsarRpcClientBuilderImpl replySubscription(@NonNull String replySubscription) { - this.replySubscription = replySubscription; - return this; - } + public PulsarRpcClientBuilderImpl replySubscription(@NonNull String replySubscription) { + this.replySubscription = replySubscription; + return this; + } - public PulsarRpcClientBuilderImpl replyTimeout(@NonNull Duration replyTimeout) { - this.replyTimeout = replyTimeout; - return this; - } + public PulsarRpcClientBuilderImpl replyTimeout(@NonNull Duration replyTimeout) { + this.replyTimeout = replyTimeout; + return this; + } - public PulsarRpcClientBuilderImpl patternAutoDiscoveryInterval( - @NonNull Duration patternAutoDiscoveryInterval) { - this.patternAutoDiscoveryInterval = patternAutoDiscoveryInterval; - return this; - } + public PulsarRpcClientBuilderImpl patternAutoDiscoveryInterval( + @NonNull Duration patternAutoDiscoveryInterval) { + this.patternAutoDiscoveryInterval = patternAutoDiscoveryInterval; + return this; + } - public PulsarRpcClientBuilderImpl requestCallBack(@NonNull RequestCallBack callBack) { - this.callBack = callBack; - return this; - } + public PulsarRpcClientBuilderImpl requestCallBack(@NonNull RequestCallBack callBack) { + this.callBack = callBack; + return this; + } - public PulsarRpcClient build(PulsarClient pulsarClient) throws PulsarRpcClientException { - if (requestProducerConfig.containsKey("accessMode") - && requestProducerConfig.get("accessMode") instanceof ProducerAccessMode - && !requestProducerConfig.get("accessMode").equals(ProducerAccessMode.Exclusive)) { - throw new PulsarRpcClientException("Producer cannot set the AccessMode to non-Exclusive."); - } - return PulsarRpcClientImpl.create(pulsarClient, this); + public PulsarRpcClient build(PulsarClient pulsarClient) throws PulsarRpcClientException { + if (requestProducerConfig.containsKey("accessMode") + && requestProducerConfig.get("accessMode") instanceof ProducerAccessMode + && !requestProducerConfig.get("accessMode").equals(ProducerAccessMode.Exclusive)) { + throw new PulsarRpcClientException("Producer cannot set the AccessMode to non-Exclusive."); } + return PulsarRpcClientImpl.create(pulsarClient, this); + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientImpl.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientImpl.java index adb5a63..4c100f9 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientImpl.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientImpl.java @@ -37,176 +37,196 @@ @RequiredArgsConstructor(access = PACKAGE) class PulsarRpcClientImpl implements PulsarRpcClient { - private final ConcurrentHashMap> pendingRequestsMap; - private final Duration replyTimeout; - private final RequestSender sender; - @Getter - private final Producer requestProducer; - private final Consumer replyConsumer; - private final RequestCallBack callback; - - /** - * Creates a new instance of {@link PulsarRpcClientImpl} using the specified builder settings. - * - * @param client The Pulsar client to use for creating producers and consumers. - * @param builder The builder containing configurations for the client. - * @return A new instance of {@link PulsarRpcClientImpl}. - * @throws PulsarRpcClientException if there is an error during the client initialization. - */ - static PulsarRpcClientImpl create( - @NonNull PulsarClient client, - @NonNull PulsarRpcClientBuilderImpl builder) throws PulsarRpcClientException { - ConcurrentHashMap> pendingRequestsMap = new ConcurrentHashMap<>(); - MessageDispatcherFactory dispatcherFactory = new MessageDispatcherFactory<>( - client, - builder.getRequestSchema(), - builder.getReplySchema(), - builder.getReplySubscription()); - - Producer producer; - Consumer consumer; - RequestSender sender = new RequestSender<>(null == builder.getReplyTopic() - ? builder.getReplyTopicsPattern().pattern() : builder.getReplyTopic()); - RequestCallBack callBack = builder.getCallBack() == null ? new DefaultRequestCallBack<>() - : builder.getCallBack(); - ReplyListener replyListener = new ReplyListener<>(pendingRequestsMap, callBack); - try { - producer = dispatcherFactory.requestProducer( - builder.getRequestTopic(), - builder.getRequestProducerConfig()); - - consumer = dispatcherFactory.replyConsumer( - builder.getReplyTopic(), - replyListener, - builder.getReplyTopicsPattern(), - builder.getPatternAutoDiscoveryInterval()); - } catch (IOException e) { - throw new PulsarRpcClientException(e); - } - - return new PulsarRpcClientImpl<>( - pendingRequestsMap, - builder.getReplyTimeout(), - sender, - producer, - consumer, - callBack); + private final ConcurrentHashMap> pendingRequestsMap; + private final Duration replyTimeout; + private final RequestSender sender; + @Getter private final Producer requestProducer; + private final Consumer replyConsumer; + private final RequestCallBack callback; + + /** + * Creates a new instance of {@link PulsarRpcClientImpl} using the specified builder settings. + * + * @param client The Pulsar client to use for creating producers and consumers. + * @param builder The builder containing configurations for the client. + * @return A new instance of {@link PulsarRpcClientImpl}. + * @throws PulsarRpcClientException if there is an error during the client initialization. + */ + static PulsarRpcClientImpl create( + @NonNull PulsarClient client, @NonNull PulsarRpcClientBuilderImpl builder) + throws PulsarRpcClientException { + ConcurrentHashMap> pendingRequestsMap = new ConcurrentHashMap<>(); + MessageDispatcherFactory dispatcherFactory = + new MessageDispatcherFactory<>( + client, + builder.getRequestSchema(), + builder.getReplySchema(), + builder.getReplySubscription()); + + Producer producer; + Consumer consumer; + RequestSender sender = + new RequestSender<>( + null == builder.getReplyTopic() + ? builder.getReplyTopicsPattern().pattern() + : builder.getReplyTopic()); + RequestCallBack callBack = + builder.getCallBack() == null ? new DefaultRequestCallBack<>() : builder.getCallBack(); + ReplyListener replyListener = new ReplyListener<>(pendingRequestsMap, callBack); + try { + producer = + dispatcherFactory.requestProducer( + builder.getRequestTopic(), builder.getRequestProducerConfig()); + + consumer = + dispatcherFactory.replyConsumer( + builder.getReplyTopic(), + replyListener, + builder.getReplyTopicsPattern(), + builder.getPatternAutoDiscoveryInterval()); + } catch (IOException e) { + throw new PulsarRpcClientException(e); } - @Override - public void close() throws PulsarRpcClientException { - try (requestProducer; replyConsumer) { - pendingRequestsMap.forEach((correlationId, future) -> { - future.cancel(false); - }); - pendingRequestsMap.clear(); - } catch (PulsarClientException e) { - throw new PulsarRpcClientException(e); - } + return new PulsarRpcClientImpl<>( + pendingRequestsMap, builder.getReplyTimeout(), sender, producer, consumer, callBack); + } + + @Override + public void close() throws PulsarRpcClientException { + try (requestProducer; + replyConsumer) { + pendingRequestsMap.forEach( + (correlationId, future) -> { + future.cancel(false); + }); + pendingRequestsMap.clear(); + } catch (PulsarClientException e) { + throw new PulsarRpcClientException(e); } - - @Override - public V request(String correlationId, T value, Map config) throws PulsarRpcClientException { - try { - return requestAsync(correlationId, value, config).get(); - } catch (InterruptedException | ExecutionException e) { - ExceptionHandler.handleInterruptedException(e); - throw new PulsarRpcClientException(e.getMessage()); - } - } - - @Override - public CompletableFuture requestAsync(String correlationId, T value, Map config) { - return internalRequest(correlationId, value, config, -1, -1, null); - } - - @Override - public CompletableFuture requestAtAsync(String correlationId, T value, Map config, - long timestamp) { - return internalRequest(correlationId, value, config, timestamp, -1, null); + } + + @Override + public V request(String correlationId, T value, Map config) + throws PulsarRpcClientException { + try { + return requestAsync(correlationId, value, config).get(); + } catch (InterruptedException | ExecutionException e) { + ExceptionHandler.handleInterruptedException(e); + throw new PulsarRpcClientException(e.getMessage()); } - - @Override - public CompletableFuture requestAfterAsync(String correlationId, T value, Map config, - long delay, TimeUnit unit) { - return internalRequest(correlationId, value, config, -1, delay, unit); - } - - private CompletableFuture internalRequest(String correlationId, T value, Map config, - long timestamp, long delay, TimeUnit unit) { - CompletableFuture replyFuture = new CompletableFuture<>(); - long replyTimeoutMillis = replyTimeout.toMillis(); - TypedMessageBuilder requestMessage = newRequestMessage(correlationId, value, config); - if (timestamp == -1 && delay == -1) { - replyFuture.orTimeout(replyTimeoutMillis, TimeUnit.MILLISECONDS) - .exceptionally(e -> { - replyFuture.completeExceptionally(new PulsarRpcClientException(e.getMessage())); - callback.onTimeout(correlationId, e); - removeRequest(correlationId); - return null; - }); - pendingRequestsMap.put(correlationId, replyFuture); - - sender.sendRequest(requestMessage, replyTimeoutMillis) - .thenAccept(requestMessageId -> { - if (replyFuture.isCancelled() || replyFuture.isCompletedExceptionally()) { - removeRequest(correlationId); - } else { - callback.onSendRequestSuccess(correlationId, requestMessageId); - } - }).exceptionally(ex -> { - if (callback != null) { - callback.onSendRequestError(correlationId, ex, replyFuture); - } else { - replyFuture.completeExceptionally(new PulsarRpcClientException(ex.getMessage())); - } - removeRequest(correlationId); - return null; - }); - } else { - // Handle Delayed RPC. - if (pendingRequestsMap.containsKey(correlationId)) { + } + + @Override + public CompletableFuture requestAsync( + String correlationId, T value, Map config) { + return internalRequest(correlationId, value, config, -1, -1, null); + } + + @Override + public CompletableFuture requestAtAsync( + String correlationId, T value, Map config, long timestamp) { + return internalRequest(correlationId, value, config, timestamp, -1, null); + } + + @Override + public CompletableFuture requestAfterAsync( + String correlationId, T value, Map config, long delay, TimeUnit unit) { + return internalRequest(correlationId, value, config, -1, delay, unit); + } + + private CompletableFuture internalRequest( + String correlationId, + T value, + Map config, + long timestamp, + long delay, + TimeUnit unit) { + CompletableFuture replyFuture = new CompletableFuture<>(); + long replyTimeoutMillis = replyTimeout.toMillis(); + TypedMessageBuilder requestMessage = newRequestMessage(correlationId, value, config); + if (timestamp == -1 && delay == -1) { + replyFuture + .orTimeout(replyTimeoutMillis, TimeUnit.MILLISECONDS) + .exceptionally( + e -> { + replyFuture.completeExceptionally(new PulsarRpcClientException(e.getMessage())); + callback.onTimeout(correlationId, e); removeRequest(correlationId); - } - - if (timestamp > 0) { - requestMessage.property(REQUEST_DELIVER_AT_TIME, String.valueOf(timestamp)); - requestMessage.deliverAt(timestamp); - } else if (delay > 0 && unit != null) { - String delayedAt = String.valueOf(System.currentTimeMillis() + unit.toMillis(delay)); - requestMessage.property(REQUEST_DELIVER_AT_TIME, delayedAt); - requestMessage.deliverAfter(delay, unit); - } - sender.sendRequest(requestMessage, replyTimeoutMillis).thenAccept(requestMessageId -> { + return null; + }); + pendingRequestsMap.put(correlationId, replyFuture); + + sender + .sendRequest(requestMessage, replyTimeoutMillis) + .thenAccept( + requestMessageId -> { + if (replyFuture.isCancelled() || replyFuture.isCompletedExceptionally()) { + removeRequest(correlationId); + } else { + callback.onSendRequestSuccess(correlationId, requestMessageId); + } + }) + .exceptionally( + ex -> { + if (callback != null) { + callback.onSendRequestError(correlationId, ex, replyFuture); + } else { + replyFuture.completeExceptionally(new PulsarRpcClientException(ex.getMessage())); + } + removeRequest(correlationId); + return null; + }); + } else { + // Handle Delayed RPC. + if (pendingRequestsMap.containsKey(correlationId)) { + removeRequest(correlationId); + } + + if (timestamp > 0) { + requestMessage.property(REQUEST_DELIVER_AT_TIME, String.valueOf(timestamp)); + requestMessage.deliverAt(timestamp); + } else if (delay > 0 && unit != null) { + String delayedAt = String.valueOf(System.currentTimeMillis() + unit.toMillis(delay)); + requestMessage.property(REQUEST_DELIVER_AT_TIME, delayedAt); + requestMessage.deliverAfter(delay, unit); + } + sender + .sendRequest(requestMessage, replyTimeoutMillis) + .thenAccept( + requestMessageId -> { callback.onSendRequestSuccess(correlationId, requestMessageId); - }).exceptionally(ex -> { + }) + .exceptionally( + ex -> { if (callback != null) { - callback.onSendRequestError(correlationId, ex, replyFuture); + callback.onSendRequestError(correlationId, ex, replyFuture); } else { - replyFuture.completeExceptionally(new PulsarRpcClientException(ex.getMessage())); + replyFuture.completeExceptionally(new PulsarRpcClientException(ex.getMessage())); } return null; - }); - } - - return replyFuture; + }); } - private TypedMessageBuilder newRequestMessage(String correlationId, T value, Map config) { - TypedMessageBuilder messageBuilder = requestProducer.newMessage().key(correlationId).value(value); - if (!config.isEmpty()) { - messageBuilder.loadConf(config); - } - return messageBuilder; - } + return replyFuture; + } - public void removeRequest(String correlationId) { - pendingRequestsMap.remove(correlationId); + private TypedMessageBuilder newRequestMessage( + String correlationId, T value, Map config) { + TypedMessageBuilder messageBuilder = + requestProducer.newMessage().key(correlationId).value(value); + if (!config.isEmpty()) { + messageBuilder.loadConf(config); } + return messageBuilder; + } - @VisibleForTesting - public int pendingRequestSize() { - return pendingRequestsMap.size(); - } + public void removeRequest(String correlationId) { + pendingRequestsMap.remove(correlationId); + } + @VisibleForTesting + public int pendingRequestSize() { + return pendingRequestsMap.size(); + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/ReplyListener.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/ReplyListener.java index 326fe68..db6a35a 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/ReplyListener.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/ReplyListener.java @@ -26,54 +26,66 @@ import org.apache.pulsar.client.api.MessageListener; /** - * Implements the {@link MessageListener} interface to handle reply messages for RPC requests in a Pulsar environment. - * This listener manages the lifecycle of reply messages corresponding to each request, facilitating asynchronous - * communication patterns and error handling based on callback mechanisms. + * Implements the {@link MessageListener} interface to handle reply messages for RPC requests in a + * Pulsar environment. This listener manages the lifecycle of reply messages corresponding to each + * request, facilitating asynchronous communication patterns and error handling based on callback + * mechanisms. * * @param The type of the message payload expected in the reply messages. */ @Slf4j @RequiredArgsConstructor(access = AccessLevel.PACKAGE) class ReplyListener implements MessageListener { - private final ConcurrentHashMap> pendingRequestsMap; - private final RequestCallBack callBack; + private final ConcurrentHashMap> pendingRequestsMap; + private final RequestCallBack callBack; - /** - * Handles the reception of messages from reply-topic. This method is called whenever a message is received - * on the subscribed topic. It processes the message based on its correlation ID and manages successful or - * erroneous consumption through callbacks. - * - * @param consumer The consumer that received the message. Provides context for the message such as subscription - * and topic information. - * @param msg The message received from the topic. Contains data including the payload and metadata like the - * correlation ID and potential error messages. - */ - @Override - public void received(Consumer consumer, Message msg) { - String correlationId = msg.getKey(); - try { - if (!pendingRequestsMap.containsKey(correlationId) && !msg.hasProperty(REQUEST_DELIVER_AT_TIME)) { - log.warn("[{}] [{}] No pending request found for correlationId {}." - + " This may indicate the message has already been processed or timed out.", - consumer.getTopic(), consumer.getConsumerName(), correlationId); - } else { - CompletableFuture future = pendingRequestsMap.computeIfAbsent(correlationId, - key -> new CompletableFuture<>()); - String errorMessage = msg.getProperty(ERROR_MESSAGE); - String serverSub = msg.getProperty(SERVER_SUB); - if (errorMessage != null) { - callBack.onReplyError(correlationId, serverSub, errorMessage, future); - } else { - callBack.onReplySuccess(correlationId, serverSub, msg.getValue(), future); - } - } - } finally { - consumer.acknowledgeAsync(msg).exceptionally(ex -> { - log.warn("[{}] [{}] Acknowledging message {} failed", msg.getTopicName(), correlationId, - msg.getMessageId(), ex); + /** + * Handles the reception of messages from reply-topic. This method is called whenever a message is + * received on the subscribed topic. It processes the message based on its correlation ID and + * manages successful or erroneous consumption through callbacks. + * + * @param consumer The consumer that received the message. Provides context for the message such + * as subscription and topic information. + * @param msg The message received from the topic. Contains data including the payload and + * metadata like the correlation ID and potential error messages. + */ + @Override + public void received(Consumer consumer, Message msg) { + String correlationId = msg.getKey(); + try { + if (!pendingRequestsMap.containsKey(correlationId) + && !msg.hasProperty(REQUEST_DELIVER_AT_TIME)) { + log.warn( + "[{}] [{}] No pending request found for correlationId {}." + + " This may indicate the message has already been processed or timed out.", + consumer.getTopic(), + consumer.getConsumerName(), + correlationId); + } else { + CompletableFuture future = + pendingRequestsMap.computeIfAbsent(correlationId, key -> new CompletableFuture<>()); + String errorMessage = msg.getProperty(ERROR_MESSAGE); + String serverSub = msg.getProperty(SERVER_SUB); + if (errorMessage != null) { + callBack.onReplyError(correlationId, serverSub, errorMessage, future); + } else { + callBack.onReplySuccess(correlationId, serverSub, msg.getValue(), future); + } + } + } finally { + consumer + .acknowledgeAsync(msg) + .exceptionally( + ex -> { + log.warn( + "[{}] [{}] Acknowledging message {} failed", + msg.getTopicName(), + correlationId, + msg.getMessageId(), + ex); callBack.onReplyMessageAckFailed(correlationId, consumer, msg, ex); return null; - }); - } + }); } + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestCallBack.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestCallBack.java index a0ce89a..bf97f31 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestCallBack.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestCallBack.java @@ -19,86 +19,92 @@ import org.apache.pulsar.client.api.MessageId; /** - * Provides callback methods for various asynchronous events in Pulsar RPC communications. - * This interface is used to define custom behaviors that occur in response to different stages - * of message handling, such as request message successful send, send error, successful reply from server, - * reply error from server, timeouts, and errors in reply message acknowledgment. + * Provides callback methods for various asynchronous events in Pulsar RPC communications. This + * interface is used to define custom behaviors that occur in response to different stages of + * message handling, such as request message successful send, send error, successful reply from + * server, reply error from server, timeouts, and errors in reply message acknowledgment. * *

Implementations of this interface can be used to handle callbacks in a way that integrates - * seamlessly with business logic, including error handling, logging, or retry mechanisms.

+ * seamlessly with business logic, including error handling, logging, or retry mechanisms. * * @param the type of reply message */ public interface RequestCallBack { - /** - * Invoked after successfully sending a request to the server. - * - * @param correlationId A unique identifier for the request to correlate the response. - * @param messageId The message ID of the request message sent to server. - */ - void onSendRequestSuccess(String correlationId, MessageId messageId); + /** + * Invoked after successfully sending a request to the server. + * + * @param correlationId A unique identifier for the request to correlate the response. + * @param messageId The message ID of the request message sent to server. + */ + void onSendRequestSuccess(String correlationId, MessageId messageId); - /** - * Invoked when an error occurs during the sending of a request message. - * - *

Please note that {@code replyFuture.completeExceptionally(t)} must be executed at the end. - * - * @param correlationId The correlation ID of the request. - * @param t The throwable error that occurred during sending. - * @param replyFuture The future where the error will be reported. - */ - void onSendRequestError(String correlationId, Throwable t, CompletableFuture replyFuture); + /** + * Invoked when an error occurs during the sending of a request message. + * + *

Please note that {@code replyFuture.completeExceptionally(t)} must be executed at the end. + * + * @param correlationId The correlation ID of the request. + * @param t The throwable error that occurred during sending. + * @param replyFuture The future where the error will be reported. + */ + void onSendRequestError(String correlationId, Throwable t, CompletableFuture replyFuture); - /** - * Invoked after receiving a reply from the server successfully. - * - *

Please note that {@code replyFuture.complete(value)} must be executed at the end. - * - * @param correlationId The correlation ID associated with the reply. - * @param subscription The subscription name the reply was received on. - * @param value The value of the reply. - * @param replyFuture The future to be completed with the received value. - */ - void onReplySuccess(String correlationId, String subscription, V value, CompletableFuture replyFuture); + /** + * Invoked after receiving a reply from the server successfully. + * + *

Please note that {@code replyFuture.complete(value)} must be executed at the end. + * + * @param correlationId The correlation ID associated with the reply. + * @param subscription The subscription name the reply was received on. + * @param value The value of the reply. + * @param replyFuture The future to be completed with the received value. + */ + void onReplySuccess( + String correlationId, String subscription, V value, CompletableFuture replyFuture); - /** - * Invoked when an error occurs upon receiving a reply from the server. - * - *

Please note that {@code replyFuture.completeExceptionally(new Exception(errorMessage))} must be executed - * at the end. - * - * @param correlationId The correlation ID of the request. - * @param subscription The subscription name the error occurred on. - * @param errorMessage The error message associated with the reply. - * @param replyFuture The future to be completed exceptionally due to the error. - */ - void onReplyError(String correlationId, String subscription, String errorMessage, CompletableFuture replyFuture); + /** + * Invoked when an error occurs upon receiving a reply from the server. + * + *

Please note that {@code replyFuture.completeExceptionally(new Exception(errorMessage))} must + * be executed at the end. + * + * @param correlationId The correlation ID of the request. + * @param subscription The subscription name the error occurred on. + * @param errorMessage The error message associated with the reply. + * @param replyFuture The future to be completed exceptionally due to the error. + */ + void onReplyError( + String correlationId, + String subscription, + String errorMessage, + CompletableFuture replyFuture); - /** - * Invoked when receive reply message times out. - * - * @param correlationId The correlation ID associated with the request that timed out. - * @param t The timeout exception or relevant throwable. - */ - void onTimeout(String correlationId, Throwable t); + /** + * Invoked when receive reply message times out. + * + * @param correlationId The correlation ID associated with the request that timed out. + * @param t The timeout exception or relevant throwable. + */ + void onTimeout(String correlationId, Throwable t); - /** - * Invoked when acknowledging reply message fails. - * - *

You can retry or record the messageId of the reply message for subsequent processing separately. - *

- * This piece does not affect the current function. Because the reply message has been processed by - * onReplySuccess or onReplyError. When the user-defined request success condition is met, - * the user removes the request through the removeRequest method of rpc client. - * Even if you receive the reply message corresponding to this request in the future. - * But if there is no request in the pendingRequestMap, it will not be processed. - *

- * - * @param correlationId The correlation ID of the message. - * @param consumer The consumer that is acknowledging the message. - * @param msg The message that failed to be acknowledged. - * @param t The throwable error encountered during acknowledgment. - */ - void onReplyMessageAckFailed(String correlationId, Consumer consumer, Message msg, Throwable t); + /** + * Invoked when acknowledging reply message fails. + * + *

You can retry or record the messageId of the reply message for subsequent processing + * separately. + * + *

This piece does not affect the current function. Because the reply message has been + * processed by onReplySuccess or onReplyError. When the user-defined request success condition is + * met, the user removes the request through the removeRequest method of rpc client. Even if you + * receive the reply message corresponding to this request in the future. But if there is no + * request in the pendingRequestMap, it will not be processed. + * + * @param correlationId The correlation ID of the message. + * @param consumer The consumer that is acknowledging the message. + * @param msg The message that failed to be acknowledged. + * @param t The throwable error encountered during acknowledgment. + */ + void onReplyMessageAckFailed( + String correlationId, Consumer consumer, Message msg, Throwable t); } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestSender.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestSender.java index 9721239..7ab9b3f 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestSender.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestSender.java @@ -22,31 +22,33 @@ import org.apache.pulsar.client.api.TypedMessageBuilder; /** - * Handles the sending of request messages to a specified Pulsar topic. This class encapsulates the details of - * setting message properties related to the reply handling and timeout management before sending the messages - * asynchronously. + * Handles the sending of request messages to a specified Pulsar topic. This class encapsulates the + * details of setting message properties related to the reply handling and timeout management before + * sending the messages asynchronously. * * @param The type of the payload of the request messages that this sender will handle. */ @RequiredArgsConstructor(access = AccessLevel.PACKAGE) class RequestSender { - private final String replyTopic; + private final String replyTopic; - /** - * Sends a request message asynchronously to request-topic specified during this object's construction. - * This method adds necessary properties to the message such as the reply topic and the request timeout before - * sending it. - * - * @param message The {@link TypedMessageBuilder} for building the message to be sent, allowing additional - * properties to be set before dispatch. - * @param millis The timeout in milliseconds after which the request should be considered failed if no reply - * is received. - * @return A {@link CompletableFuture} that will complete with the {@link MessageId} of the sent message once - * it has been successfully dispatched or will complete exceptionally if the send fails. - */ - CompletableFuture sendRequest(TypedMessageBuilder message, long millis) { - return message.property(REPLY_TOPIC, replyTopic) - .property(REQUEST_TIMEOUT_MILLIS, String.valueOf(millis)) - .sendAsync(); - } + /** + * Sends a request message asynchronously to request-topic specified during this object's + * construction. This method adds necessary properties to the message such as the reply topic and + * the request timeout before sending it. + * + * @param message The {@link TypedMessageBuilder} for building the message to be sent, allowing + * additional properties to be set before dispatch. + * @param millis The timeout in milliseconds after which the request should be considered failed + * if no reply is received. + * @return A {@link CompletableFuture} that will complete with the {@link MessageId} of the sent + * message once it has been successfully dispatched or will complete exceptionally if the send + * fails. + */ + CompletableFuture sendRequest(TypedMessageBuilder message, long millis) { + return message + .property(REPLY_TOPIC, replyTopic) + .property(REQUEST_TIMEOUT_MILLIS, String.valueOf(millis)) + .sendAsync(); + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/package-info.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/package-info.java index 3cd8c6c..971ed7b 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/package-info.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/package-info.java @@ -14,27 +14,30 @@ /** * Client-side implementation of a distributed RPC framework using Apache Pulsar * - *

This package contains classes that are responsible for establishing and managing RPC interactions with the - * Pulsar service. - * It includes facilities for sending requests, receiving responses, and processing related callbacks. - * These classes allow users to easily implement the RPC communication model, supporting both asynchronous and - * synchronous message handling. + *

This package contains classes that are responsible for establishing and managing RPC + * interactions with the Pulsar service. It includes facilities for sending requests, receiving + * responses, and processing related callbacks. These classes allow users to easily implement the + * RPC communication model, supporting both asynchronous and synchronous message handling. * *

Key classes and interfaces include: + * *

    - *
  • {@link org.apache.pulsar.rpc.contrib.client.PulsarRpcClient} - The RPC client used for sending requests - * and processing replies. - *
  • {@link org.apache.pulsar.rpc.contrib.client.PulsarRpcClientImpl} - PulsarRpcClient implementation classes. - *
  • {@link org.apache.pulsar.rpc.contrib.client.PulsarRpcClientBuilder} - PulsarRpcClient builder. - *
  • {@link org.apache.pulsar.rpc.contrib.client.PulsarRpcClientBuilderImpl} - PulsarRpcClient builder - * implementation classes. - *
  • {@link org.apache.pulsar.rpc.contrib.client.RequestSender} - A class for sending requests to the Pulsar server. - *
  • {@link org.apache.pulsar.rpc.contrib.client.RequestCallBack} - Defines callback methods to handle - * successful request transmissions, errors, and response successes or failures. - *
  • {@link org.apache.pulsar.rpc.contrib.client.DefaultRequestCallBack} - Default implementation of - * RequestCallBack. - *
  • {@link org.apache.pulsar.rpc.contrib.client.ReplyListener} - A message listener that receives and - * processes reply message from the server. + *
  • {@link org.apache.pulsar.rpc.contrib.client.PulsarRpcClient} - The RPC client used for + * sending requests and processing replies. + *
  • {@link org.apache.pulsar.rpc.contrib.client.PulsarRpcClientImpl} - PulsarRpcClient + * implementation classes. + *
  • {@link org.apache.pulsar.rpc.contrib.client.PulsarRpcClientBuilder} - PulsarRpcClient + * builder. + *
  • {@link org.apache.pulsar.rpc.contrib.client.PulsarRpcClientBuilderImpl} - PulsarRpcClient + * builder implementation classes. + *
  • {@link org.apache.pulsar.rpc.contrib.client.RequestSender} - A class for sending requests + * to the Pulsar server. + *
  • {@link org.apache.pulsar.rpc.contrib.client.RequestCallBack} - Defines callback methods to + * handle successful request transmissions, errors, and response successes or failures. + *
  • {@link org.apache.pulsar.rpc.contrib.client.DefaultRequestCallBack} - Default + * implementation of RequestCallBack. + *
  • {@link org.apache.pulsar.rpc.contrib.client.ReplyListener} - A message listener that + * receives and processes reply message from the server. *
* *

The implementation in this package relies on the Apache Pulsar client library. diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/Constants.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/Constants.java index b51d943..64115cc 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/Constants.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/Constants.java @@ -13,13 +13,11 @@ */ package org.apache.pulsar.rpc.contrib.common; -/** - * Definition of constants. - */ +/** Definition of constants. */ public class Constants { - public static final String REQUEST_TIMEOUT_MILLIS = "requestTimeoutInMillis"; - public static final String REPLY_TOPIC = "replyTopic"; - public static final String ERROR_MESSAGE = "errorMessage"; - public static final String SERVER_SUB = "serverSub"; - public static final String REQUEST_DELIVER_AT_TIME = "deliverAt"; + public static final String REQUEST_TIMEOUT_MILLIS = "requestTimeoutInMillis"; + public static final String REPLY_TOPIC = "replyTopic"; + public static final String ERROR_MESSAGE = "errorMessage"; + public static final String SERVER_SUB = "serverSub"; + public static final String REQUEST_DELIVER_AT_TIME = "deliverAt"; } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/MessageDispatcherFactory.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/MessageDispatcherFactory.java index 56c5b79..8d1d74b 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/MessageDispatcherFactory.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/MessageDispatcherFactory.java @@ -30,98 +30,110 @@ import org.apache.pulsar.client.api.SubscriptionType; /** - * Facilitates the creation of producers and consumers for Pulsar based on specified schemas and configurations. - * This factory simplifies the setup of Pulsar clients for sending and receiving messages in RPC context. + * Facilitates the creation of producers and consumers for Pulsar based on specified schemas and + * configurations. This factory simplifies the setup of Pulsar clients for sending and receiving + * messages in RPC context. * * @param the type parameter for the request messages. * @param the type parameter for the reply messages. */ @RequiredArgsConstructor public class MessageDispatcherFactory { - private final PulsarClient client; - private final Schema requestSchema; - private final Schema replySchema; - private final String subscription; + private final PulsarClient client; + private final Schema requestSchema; + private final Schema replySchema; + private final String subscription; - /** - * Creates a Pulsar producer for sending requests. (Pulsar RPC Client side) - * - * @param topic request topic name. - * @param requestProducerConfig the configuration map for request producer. - * @return the created request message producer. - * @throws IOException if there is an error creating the producer. - */ - public Producer requestProducer(String topic, Map requestProducerConfig) throws IOException { - return client.newProducer(requestSchema) - .topic(topic) - // allow only one client - .accessMode(ProducerAccessMode.Exclusive) - .loadConf(requestProducerConfig) - .create(); - } + /** + * Creates a Pulsar producer for sending requests. (Pulsar RPC Client side) + * + * @param topic request topic name. + * @param requestProducerConfig the configuration map for request producer. + * @return the created request message producer. + * @throws IOException if there is an error creating the producer. + */ + public Producer requestProducer(String topic, Map requestProducerConfig) + throws IOException { + return client + .newProducer(requestSchema) + .topic(topic) + // allow only one client + .accessMode(ProducerAccessMode.Exclusive) + .loadConf(requestProducerConfig) + .create(); + } - /** - * Creates a Pulsar consumer for receiving replies. (Pulsar RPC Client side) - * - * @param topic the topic from which to consume messages. - * @param listener the message listener that handles incoming messages. - * @param topicsPattern the pattern matching multiple topics for the consumer. - * @param patternAutoDiscoveryInterval the interval for topic auto-discovery. - * @return the created reply message consumer. - * @throws IOException if there is an error creating the consumer. - */ - public Consumer replyConsumer(String topic, - MessageListener listener, - Pattern topicsPattern, - Duration patternAutoDiscoveryInterval) throws IOException { - ConsumerBuilder replyConsumerBuilder = client - .newConsumer(replySchema) - .patternAutoDiscoveryPeriod((int) patternAutoDiscoveryInterval.toMillis(), MILLISECONDS) - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .subscriptionName(subscription) - // allow only one client - .subscriptionType(SubscriptionType.Exclusive) - .messageListener(listener); - return topicsPattern == null ? replyConsumerBuilder.topic(topic).subscribe() - : replyConsumerBuilder.topicsPattern(topicsPattern).subscribe(); - } + /** + * Creates a Pulsar consumer for receiving replies. (Pulsar RPC Client side) + * + * @param topic the topic from which to consume messages. + * @param listener the message listener that handles incoming messages. + * @param topicsPattern the pattern matching multiple topics for the consumer. + * @param patternAutoDiscoveryInterval the interval for topic auto-discovery. + * @return the created reply message consumer. + * @throws IOException if there is an error creating the consumer. + */ + public Consumer replyConsumer( + String topic, + MessageListener listener, + Pattern topicsPattern, + Duration patternAutoDiscoveryInterval) + throws IOException { + ConsumerBuilder replyConsumerBuilder = + client + .newConsumer(replySchema) + .patternAutoDiscoveryPeriod((int) patternAutoDiscoveryInterval.toMillis(), MILLISECONDS) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscriptionName(subscription) + // allow only one client + .subscriptionType(SubscriptionType.Exclusive) + .messageListener(listener); + return topicsPattern == null + ? replyConsumerBuilder.topic(topic).subscribe() + : replyConsumerBuilder.topicsPattern(topicsPattern).subscribe(); + } - /** - * Creates a Pulsar consumer for receiving requests. (Pulsar RPC Server side) - * - * @param topic the topic from which to consume messages. - * @param patternAutoDiscoveryInterval the interval for topic auto-discovery. - * @param listener the message listener that handles incoming messages. - * @param topicsPattern the pattern matching multiple topics for the consumer. - * @return the created Consumer. - * @throws IOException if there is an error creating the consumer. - */ - public Consumer requestConsumer( - String topic, Duration patternAutoDiscoveryInterval, - MessageListener listener, Pattern topicsPattern) throws IOException { - ConsumerBuilder consumerBuilder = client - .newConsumer(requestSchema) - .patternAutoDiscoveryPeriod((int) patternAutoDiscoveryInterval.toMillis(), MILLISECONDS) - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .subscriptionName(subscription) - .subscriptionType(SubscriptionType.Key_Shared) - .messageListener(listener); - return topicsPattern == null ? consumerBuilder.topic(topic).subscribe() - : consumerBuilder.topicsPattern(topicsPattern).subscribe(); - } + /** + * Creates a Pulsar consumer for receiving requests. (Pulsar RPC Server side) + * + * @param topic the topic from which to consume messages. + * @param patternAutoDiscoveryInterval the interval for topic auto-discovery. + * @param listener the message listener that handles incoming messages. + * @param topicsPattern the pattern matching multiple topics for the consumer. + * @return the created Consumer. + * @throws IOException if there is an error creating the consumer. + */ + public Consumer requestConsumer( + String topic, + Duration patternAutoDiscoveryInterval, + MessageListener listener, + Pattern topicsPattern) + throws IOException { + ConsumerBuilder consumerBuilder = + client + .newConsumer(requestSchema) + .patternAutoDiscoveryPeriod((int) patternAutoDiscoveryInterval.toMillis(), MILLISECONDS) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscriptionName(subscription) + .subscriptionType(SubscriptionType.Key_Shared) + .messageListener(listener); + return topicsPattern == null + ? consumerBuilder.topic(topic).subscribe() + : consumerBuilder.topicsPattern(topicsPattern).subscribe(); + } - /** - * Creates a Pulsar producer for sending replies. (Pulsar RPC Server side) - * - * @param topic the topic to which the producer will send messages. - * @return the created Producer. - * @throws IOException if there is an error creating the producer. - */ - public Producer replyProducer(String topic) throws IOException { - return client - .newProducer(replySchema) - .topic(topic) - .accessMode(ProducerAccessMode.Shared) - .create(); - } + /** + * Creates a Pulsar producer for sending replies. (Pulsar RPC Server side) + * + * @param topic the topic to which the producer will send messages. + * @return the created Producer. + * @throws IOException if there is an error creating the producer. + */ + public Producer replyProducer(String topic) throws IOException { + return client + .newProducer(replySchema) + .topic(topic) + .accessMode(ProducerAccessMode.Shared) + .create(); + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/PulsarRpcClientException.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/PulsarRpcClientException.java index e2cea8c..e141e21 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/PulsarRpcClientException.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/PulsarRpcClientException.java @@ -15,26 +15,23 @@ public class PulsarRpcClientException extends Exception { - /** - * Constructs an {@code PulsarRpcClientException} with the specified detail message. - * - * @param message - * The detail message (which is saved for later retrieval - * by the {@link #getMessage()} method) - */ - public PulsarRpcClientException(String message) { - super(message); - } + /** + * Constructs an {@code PulsarRpcClientException} with the specified detail message. + * + * @param message The detail message (which is saved for later retrieval by the {@link + * #getMessage()} method) + */ + public PulsarRpcClientException(String message) { + super(message); + } - /** - * Constructs an {@code PulsarRpcClientException} with the specified cause. - * - * @param cause - * The cause (which is saved for later retrieval by the - * {@link #getCause()} method). (A null value is permitted, - * and indicates that the cause is nonexistent or unknown.) - */ - public PulsarRpcClientException(Throwable cause) { - super(cause); - } + /** + * Constructs an {@code PulsarRpcClientException} with the specified cause. + * + * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method). + * (A null value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + public PulsarRpcClientException(Throwable cause) { + super(cause); + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/PulsarRpcServerException.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/PulsarRpcServerException.java index 0ac89a3..b742370 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/PulsarRpcServerException.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/PulsarRpcServerException.java @@ -15,26 +15,23 @@ public class PulsarRpcServerException extends Exception { - /** - * Constructs an {@code PulsarRpcServerException} with the specified detail message. - * - * @param message - * The detail message (which is saved for later retrieval - * by the {@link #getMessage()} method) - */ - public PulsarRpcServerException(String message) { - super(message); - } + /** + * Constructs an {@code PulsarRpcServerException} with the specified detail message. + * + * @param message The detail message (which is saved for later retrieval by the {@link + * #getMessage()} method) + */ + public PulsarRpcServerException(String message) { + super(message); + } - /** - * Constructs an {@code PulsarRpcServerException} with the specified cause. - * - * @param cause - * The cause (which is saved for later retrieval by the - * {@link #getCause()} method). (A null value is permitted, - * and indicates that the cause is nonexistent or unknown.) - */ - public PulsarRpcServerException(Throwable cause) { - super(cause); - } + /** + * Constructs an {@code PulsarRpcServerException} with the specified cause. + * + * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method). + * (A null value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + public PulsarRpcServerException(Throwable cause) { + super(cause); + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/package-info.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/package-info.java index c7d2540..7bbdefe 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/package-info.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/package-info.java @@ -12,17 +12,20 @@ * limitations under the License. */ /** - * This package provides common utilities and constants used across the Apache Pulsar RPC extensions. - * It includes helper classes for managing RPC message flows and configurations. + * This package provides common utilities and constants used across the Apache Pulsar RPC + * extensions. It includes helper classes for managing RPC message flows and configurations. + * + *

Features include: * - *

Features include:

*
    - *
  • {@link org.apache.pulsar.rpc.contrib.common.Constants} - Constants for message properties and - * configurations. - *
  • {@link org.apache.pulsar.rpc.contrib.common.MessageDispatcherFactory} - Factory methods for configuring - * Pulsar producers and consumers tailored for RPC operations. - *
  • {@link org.apache.pulsar.rpc.contrib.common.PulsarRpcClientException} - Exception on PulsarRpc client side. - *
  • {@link org.apache.pulsar.rpc.contrib.common.PulsarRpcServerException} - Exception on PulsarRpc server side. + *
  • {@link org.apache.pulsar.rpc.contrib.common.Constants} - Constants for message properties + * and configurations. + *
  • {@link org.apache.pulsar.rpc.contrib.common.MessageDispatcherFactory} - Factory methods for + * configuring Pulsar producers and consumers tailored for RPC operations. + *
  • {@link org.apache.pulsar.rpc.contrib.common.PulsarRpcClientException} - Exception on + * PulsarRpc client side. + *
  • {@link org.apache.pulsar.rpc.contrib.common.PulsarRpcServerException} - Exception on + * PulsarRpc server side. *
*/ package org.apache.pulsar.rpc.contrib.common; diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServer.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServer.java index b42526f..bec698d 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServer.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServer.java @@ -20,38 +20,37 @@ /** * Represents an RPC server that utilizes Apache Pulsar as the messaging layer to handle - * request-response cycles in a distributed environment. This server is responsible for - * receiving RPC requests, processing them, and sending the corresponding responses back - * to the client. + * request-response cycles in a distributed environment. This server is responsible for receiving + * RPC requests, processing them, and sending the corresponding responses back to the client. * - *

This class integrates tightly with Apache Pulsar's consumer and producer APIs to - * receive messages and send replies. It uses a {@link GenericKeyedObjectPool} to manage - * a pool of Pulsar producers optimized for sending replies efficiently across different topics.

+ *

This class integrates tightly with Apache Pulsar's consumer and producer APIs to receive + * messages and send replies. It uses a {@link GenericKeyedObjectPool} to manage a pool of Pulsar + * producers optimized for sending replies efficiently across different topics. * * @param the type of request message this server handles * @param the type of response message this server sends */ public interface PulsarRpcServer extends AutoCloseable { - /** - * Provides a builder to configure and create instances of {@link PulsarRpcServer}. - * - * @param requestSchema the schema for serializing and deserializing request messages - * @param replySchema the schema for serializing and deserializing reply messages - * @return a builder to configure and instantiate a {@link PulsarRpcServer} - */ - static PulsarRpcServerBuilder builder(@NonNull Schema requestSchema, - @NonNull Schema replySchema) { - return new PulsarRpcServerBuilderImpl<>(requestSchema, replySchema); - } + /** + * Provides a builder to configure and create instances of {@link PulsarRpcServer}. + * + * @param requestSchema the schema for serializing and deserializing request messages + * @param replySchema the schema for serializing and deserializing reply messages + * @return a builder to configure and instantiate a {@link PulsarRpcServer} + */ + static PulsarRpcServerBuilder builder( + @NonNull Schema requestSchema, @NonNull Schema replySchema) { + return new PulsarRpcServerBuilderImpl<>(requestSchema, replySchema); + } - /** - * Closes the RPC server, releasing all resources such as the request consumer and reply producer pool. - * This method ensures that all underlying Pulsar clients are properly closed to free up network resources and - * prevent memory leaks. - * - * @throws PulsarRpcServerException if an error occurs during the closing of server resources - */ - @Override - void close() throws PulsarRpcServerException; + /** + * Closes the RPC server, releasing all resources such as the request consumer and reply producer + * pool. This method ensures that all underlying Pulsar clients are properly closed to free up + * network resources and prevent memory leaks. + * + * @throws PulsarRpcServerException if an error occurs during the closing of server resources + */ + @Override + void close() throws PulsarRpcServerException; } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerBuilder.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerBuilder.java index af5e60a..19114dd 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerBuilder.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerBuilder.java @@ -23,64 +23,68 @@ import org.apache.pulsar.rpc.contrib.common.PulsarRpcServerException; /** - * Builder class for creating instances of {@link PulsarRpcServer}. - * This class provides a fluent API to configure the Pulsar RPC server with necessary schemas, - * topics, subscriptions, and other configuration parameters related to Pulsar clients. + * Builder class for creating instances of {@link PulsarRpcServer}. This class provides a fluent API + * to configure the Pulsar RPC server with necessary schemas, topics, subscriptions, and other + * configuration parameters related to Pulsar clients. * - *

Instances of {@link PulsarRpcServer} are configured to handle RPC requests and replies - * using Apache Pulsar as the messaging system. This builder allows you to specify the request - * and reply topics, schemas for serialization and deserialization, and other relevant settings.

+ *

Instances of {@link PulsarRpcServer} are configured to handle RPC requests and replies using + * Apache Pulsar as the messaging system. This builder allows you to specify the request and reply + * topics, schemas for serialization and deserialization, and other relevant settings. * * @param the type of the request message * @param the type of the reply message */ public interface PulsarRpcServerBuilder { - /** - * Specifies the Pulsar topic that this server will listen to for receiving requests. - * - * @param requestTopic the Pulsar topic name - * @return this builder instance - */ - PulsarRpcServerBuilder requestTopic(@NonNull String requestTopic); + /** + * Specifies the Pulsar topic that this server will listen to for receiving requests. + * + * @param requestTopic the Pulsar topic name + * @return this builder instance + */ + PulsarRpcServerBuilder requestTopic(@NonNull String requestTopic); - /** - * Specifies a pattern for topics that this server will listen to. This is useful for subscribing - * to multiple topics that match the given pattern. - * - * @param requestTopicsPattern the pattern to match topics against - * @return this builder instance - */ - PulsarRpcServerBuilder requestTopicsPattern(@NonNull Pattern requestTopicsPattern); + /** + * Specifies a pattern for topics that this server will listen to. This is useful for subscribing + * to multiple topics that match the given pattern. + * + * @param requestTopicsPattern the pattern to match topics against + * @return this builder instance + */ + PulsarRpcServerBuilder requestTopicsPattern(@NonNull Pattern requestTopicsPattern); - /** - * Sets the subscription name for this server to use when subscribing to the request topic. - * - * @param requestSubscription the subscription name - * @return this builder instance - */ - PulsarRpcServerBuilder requestSubscription(@NonNull String requestSubscription); + /** + * Sets the subscription name for this server to use when subscribing to the request topic. + * + * @param requestSubscription the subscription name + * @return this builder instance + */ + PulsarRpcServerBuilder requestSubscription(@NonNull String requestSubscription); - /** - * Sets the auto-discovery interval for topics. This setting helps in automatically discovering - * topics that match the set pattern at the specified interval. - * - * @param patternAutoDiscoveryInterval the duration to set for auto-discovery - * @return this builder instance - */ - PulsarRpcServerBuilder patternAutoDiscoveryInterval(@NonNull Duration patternAutoDiscoveryInterval); - - /** - * Builds and returns a {@link PulsarRpcServer} instance configured with the current settings of this builder. - * The server uses provided functional parameters to handle requests and manage rollbacks. - * - * @param pulsarClient the client to connect to Pulsar - * @param requestFunction a function to process incoming requests and generate replies - * @param rollBackFunction a consumer to handle rollback operations in case of errors - * @return a new {@link PulsarRpcServer} instance - * @throws PulsarRpcServerException if an error occurs during server initialization - */ - PulsarRpcServer build(PulsarClient pulsarClient, Function> requestFunction, - BiConsumer rollBackFunction) throws PulsarRpcServerException; + /** + * Sets the auto-discovery interval for topics. This setting helps in automatically discovering + * topics that match the set pattern at the specified interval. + * + * @param patternAutoDiscoveryInterval the duration to set for auto-discovery + * @return this builder instance + */ + PulsarRpcServerBuilder patternAutoDiscoveryInterval( + @NonNull Duration patternAutoDiscoveryInterval); + /** + * Builds and returns a {@link PulsarRpcServer} instance configured with the current settings of + * this builder. The server uses provided functional parameters to handle requests and manage + * rollbacks. + * + * @param pulsarClient the client to connect to Pulsar + * @param requestFunction a function to process incoming requests and generate replies + * @param rollBackFunction a consumer to handle rollback operations in case of errors + * @return a new {@link PulsarRpcServer} instance + * @throws PulsarRpcServerException if an error occurs during server initialization + */ + PulsarRpcServer build( + PulsarClient pulsarClient, + Function> requestFunction, + BiConsumer rollBackFunction) + throws PulsarRpcServerException; } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerBuilderImpl.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerBuilderImpl.java index ba315ea..4a3c3c0 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerBuilderImpl.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerBuilderImpl.java @@ -27,84 +27,90 @@ @Getter(AccessLevel.PACKAGE) class PulsarRpcServerBuilderImpl implements PulsarRpcServerBuilder { - private final Schema requestSchema; - private final Schema replySchema; - private String requestTopic; - private Pattern requestTopicsPattern; - private String requestSubscription; - private Duration patternAutoDiscoveryInterval; + private final Schema requestSchema; + private final Schema replySchema; + private String requestTopic; + private Pattern requestTopicsPattern; + private String requestSubscription; + private Duration patternAutoDiscoveryInterval; - /** - * Constructs a new {@link PulsarRpcServerBuilderImpl} with the given request and reply message schemas. - * - * @param requestSchema the schema used to serialize request messages - * @param replySchema the schema used to serialize reply messages - */ - public PulsarRpcServerBuilderImpl(@NonNull Schema requestSchema, @NonNull Schema replySchema) { - this.requestSchema = requestSchema; - this.replySchema = replySchema; - } + /** + * Constructs a new {@link PulsarRpcServerBuilderImpl} with the given request and reply message + * schemas. + * + * @param requestSchema the schema used to serialize request messages + * @param replySchema the schema used to serialize reply messages + */ + public PulsarRpcServerBuilderImpl( + @NonNull Schema requestSchema, @NonNull Schema replySchema) { + this.requestSchema = requestSchema; + this.replySchema = replySchema; + } - /** - * Specifies the Pulsar topic that this server will listen to for receiving requests. - * - * @param requestTopic the Pulsar topic name - * @return this builder instance - */ - public PulsarRpcServerBuilderImpl requestTopic(@NonNull String requestTopic) { - this.requestTopic = requestTopic; - return this; - } + /** + * Specifies the Pulsar topic that this server will listen to for receiving requests. + * + * @param requestTopic the Pulsar topic name + * @return this builder instance + */ + public PulsarRpcServerBuilderImpl requestTopic(@NonNull String requestTopic) { + this.requestTopic = requestTopic; + return this; + } - /** - * Specifies a pattern for topics that this server will listen to. This is useful for subscribing - * to multiple topics that match the given pattern. - * - * @param requestTopicsPattern the pattern to match topics against - * @return this builder instance - */ - public PulsarRpcServerBuilderImpl requestTopicsPattern(@NonNull Pattern requestTopicsPattern) { - this.requestTopicsPattern = requestTopicsPattern; - return this; - } + /** + * Specifies a pattern for topics that this server will listen to. This is useful for subscribing + * to multiple topics that match the given pattern. + * + * @param requestTopicsPattern the pattern to match topics against + * @return this builder instance + */ + public PulsarRpcServerBuilderImpl requestTopicsPattern( + @NonNull Pattern requestTopicsPattern) { + this.requestTopicsPattern = requestTopicsPattern; + return this; + } - /** - * Sets the subscription name for this server to use when subscribing to the request topic. - * - * @param requestSubscription the subscription name - * @return this builder instance - */ - public PulsarRpcServerBuilderImpl requestSubscription(@NonNull String requestSubscription) { - this.requestSubscription = requestSubscription; - return this; - } + /** + * Sets the subscription name for this server to use when subscribing to the request topic. + * + * @param requestSubscription the subscription name + * @return this builder instance + */ + public PulsarRpcServerBuilderImpl requestSubscription(@NonNull String requestSubscription) { + this.requestSubscription = requestSubscription; + return this; + } - /** - * Sets the auto-discovery interval for topics. This setting helps in automatically discovering - * topics that match the set pattern at the specified interval. - * - * @param patternAutoDiscoveryInterval the duration to set for auto-discovery - * @return this builder instance - */ - public PulsarRpcServerBuilderImpl patternAutoDiscoveryInterval( - @NonNull Duration patternAutoDiscoveryInterval) { - this.patternAutoDiscoveryInterval = patternAutoDiscoveryInterval; - return this; - } + /** + * Sets the auto-discovery interval for topics. This setting helps in automatically discovering + * topics that match the set pattern at the specified interval. + * + * @param patternAutoDiscoveryInterval the duration to set for auto-discovery + * @return this builder instance + */ + public PulsarRpcServerBuilderImpl patternAutoDiscoveryInterval( + @NonNull Duration patternAutoDiscoveryInterval) { + this.patternAutoDiscoveryInterval = patternAutoDiscoveryInterval; + return this; + } - /** - * Builds and returns a {@link PulsarRpcServerImpl} instance configured with the current settings of this builder. - * The server uses provided functional parameters to handle requests and manage rollbacks. - * - * @param pulsarClient the client to connect to Pulsar - * @param requestFunction a function to process incoming requests and generate replies - * @param rollBackFunction a consumer to handle rollback operations in case of errors - * @return a new {@link PulsarRpcServerImpl} instance - * @throws PulsarRpcServerException if an error occurs during server initialization - */ - public PulsarRpcServer build( - PulsarClient pulsarClient, Function> requestFunction, - BiConsumer rollBackFunction) throws PulsarRpcServerException { - return PulsarRpcServerImpl.create(pulsarClient, requestFunction, rollBackFunction, this); - } + /** + * Builds and returns a {@link PulsarRpcServerImpl} instance configured with the current settings + * of this builder. The server uses provided functional parameters to handle requests and manage + * rollbacks. + * + * @param pulsarClient the client to connect to Pulsar + * @param requestFunction a function to process incoming requests and generate replies + * @param rollBackFunction a consumer to handle rollback operations in case of errors + * @return a new {@link PulsarRpcServerImpl} instance + * @throws PulsarRpcServerException if an error occurs during server initialization + */ + public PulsarRpcServer build( + PulsarClient pulsarClient, + Function> requestFunction, + BiConsumer rollBackFunction) + throws PulsarRpcServerException { + return PulsarRpcServerImpl.create(pulsarClient, requestFunction, rollBackFunction, this); + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerImpl.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerImpl.java index 26f539a..79f2cea 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerImpl.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerImpl.java @@ -30,64 +30,62 @@ @RequiredArgsConstructor(access = PACKAGE) class PulsarRpcServerImpl implements PulsarRpcServer { - private final Consumer requestConsumer; - private final GenericKeyedObjectPool replyProducerPool; + private final Consumer requestConsumer; + private final GenericKeyedObjectPool replyProducerPool; - /** - * Creates a new instance of {@link PulsarRpcServerImpl} using the specified Pulsar client and configuration. - * It sets up the necessary consumer and producer resources according to the - * provided {@link PulsarRpcServerBuilderImpl} settings. - * - * @param pulsarClient the Pulsar client to use for connecting to the Pulsar instance - * @param requestFunction the function to process incoming requests and produce replies - * @param rollBackFunction the function to handle rollback in case of processing errors - * @param builder the configuration builder with all necessary settings - * @return a new instance of {@link PulsarRpcServerImpl} - * @throws PulsarRpcServerException if there is an error during the server initialization. - */ - static PulsarRpcServerImpl create( - @NonNull PulsarClient pulsarClient, - @NonNull Function> requestFunction, - @NonNull BiConsumer rollBackFunction, - @NonNull PulsarRpcServerBuilderImpl builder) throws PulsarRpcServerException { - MessageDispatcherFactory dispatcherFactory = new MessageDispatcherFactory<>( - pulsarClient, - builder.getRequestSchema(), - builder.getReplySchema(), - builder.getRequestSubscription()); - ReplyProducerPoolFactory poolFactory = new ReplyProducerPoolFactory<>(dispatcherFactory); - GenericKeyedObjectPool> pool = new GenericKeyedObjectPool<>(poolFactory); - ReplySender replySender = new ReplySender<>(pool, rollBackFunction); - RequestListener requestListener = new RequestListener<>( - requestFunction, - replySender, - rollBackFunction); + /** + * Creates a new instance of {@link PulsarRpcServerImpl} using the specified Pulsar client and + * configuration. It sets up the necessary consumer and producer resources according to the + * provided {@link PulsarRpcServerBuilderImpl} settings. + * + * @param pulsarClient the Pulsar client to use for connecting to the Pulsar instance + * @param requestFunction the function to process incoming requests and produce replies + * @param rollBackFunction the function to handle rollback in case of processing errors + * @param builder the configuration builder with all necessary settings + * @return a new instance of {@link PulsarRpcServerImpl} + * @throws PulsarRpcServerException if there is an error during the server initialization. + */ + static PulsarRpcServerImpl create( + @NonNull PulsarClient pulsarClient, + @NonNull Function> requestFunction, + @NonNull BiConsumer rollBackFunction, + @NonNull PulsarRpcServerBuilderImpl builder) + throws PulsarRpcServerException { + MessageDispatcherFactory dispatcherFactory = + new MessageDispatcherFactory<>( + pulsarClient, + builder.getRequestSchema(), + builder.getReplySchema(), + builder.getRequestSubscription()); + ReplyProducerPoolFactory poolFactory = new ReplyProducerPoolFactory<>(dispatcherFactory); + GenericKeyedObjectPool> pool = new GenericKeyedObjectPool<>(poolFactory); + ReplySender replySender = new ReplySender<>(pool, rollBackFunction); + RequestListener requestListener = + new RequestListener<>(requestFunction, replySender, rollBackFunction); - Consumer requestConsumer; - try { - requestConsumer = dispatcherFactory.requestConsumer( - builder.getRequestTopic(), - builder.getPatternAutoDiscoveryInterval(), - requestListener, - builder.getRequestTopicsPattern()); - } catch (IOException e) { - throw new PulsarRpcServerException(e); - } - - return new PulsarRpcServerImpl<>( - requestConsumer, - pool - ); + Consumer requestConsumer; + try { + requestConsumer = + dispatcherFactory.requestConsumer( + builder.getRequestTopic(), + builder.getPatternAutoDiscoveryInterval(), + requestListener, + builder.getRequestTopicsPattern()); + } catch (IOException e) { + throw new PulsarRpcServerException(e); } - @Override - public void close() throws PulsarRpcServerException { - try { - requestConsumer.close(); - } catch (PulsarClientException e) { - throw new PulsarRpcServerException(e); - } finally { - replyProducerPool.close(); - } + return new PulsarRpcServerImpl<>(requestConsumer, pool); + } + + @Override + public void close() throws PulsarRpcServerException { + try { + requestConsumer.close(); + } catch (PulsarClientException e) { + throw new PulsarRpcServerException(e); + } finally { + replyProducerPool.close(); } + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplyProducerPoolFactory.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplyProducerPoolFactory.java index f4ebd51..29b0ff9 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplyProducerPoolFactory.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplyProducerPoolFactory.java @@ -23,58 +23,60 @@ import org.apache.pulsar.rpc.contrib.common.MessageDispatcherFactory; /** - * A factory for creating and managing pooled {@link Producer} instances associated with specific topics. - * This factory extends {@link BaseKeyedPooledObjectFactory}, customizing the creation, wrapping, and destruction - * of Pulsar {@link Producer} instances for use in a {@link org.apache.commons.pool2.KeyedObjectPool}. + * A factory for creating and managing pooled {@link Producer} instances associated with specific + * topics. This factory extends {@link BaseKeyedPooledObjectFactory}, customizing the creation, + * wrapping, and destruction of Pulsar {@link Producer} instances for use in a {@link + * org.apache.commons.pool2.KeyedObjectPool}. * - *

This factory leverages the {@link MessageDispatcherFactory} to create {@link Producer} instances, - * ensuring that each producer is correctly configured according to the dispatcher settings.

+ *

This factory leverages the {@link MessageDispatcherFactory} to create {@link Producer} + * instances, ensuring that each producer is correctly configured according to the dispatcher + * settings. * * @param the type of messages the producers will send */ @RequiredArgsConstructor class ReplyProducerPoolFactory extends BaseKeyedPooledObjectFactory> { - private final MessageDispatcherFactory dispatcherFactory; + private final MessageDispatcherFactory dispatcherFactory; - /** - * Creates a new {@link Producer} for the specified topic. This method is called internally by the pool - * when a new producer is needed and not available in the pool. - * - * @param topic The topic for which the producer is to be created. - * @return A new {@link Producer} instance configured for the specified topic. - * @throws UncheckedIOException if an I/O error occurs when creating the producer. - */ - @Override - public Producer create(String topic) { - try { - return dispatcherFactory.replyProducer(topic); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + /** + * Creates a new {@link Producer} for the specified topic. This method is called internally by the + * pool when a new producer is needed and not available in the pool. + * + * @param topic The topic for which the producer is to be created. + * @return A new {@link Producer} instance configured for the specified topic. + * @throws UncheckedIOException if an I/O error occurs when creating the producer. + */ + @Override + public Producer create(String topic) { + try { + return dispatcherFactory.replyProducer(topic); + } catch (IOException e) { + throw new UncheckedIOException(e); } + } - /** - * Wraps a {@link Producer} instance inside a {@link PooledObject} to manage pool operations such as - * borrow and return. - * - * @param producer The {@link Producer} instance to wrap. - * @return The {@link PooledObject} wrapping the provided {@link Producer}. - */ - @Override - public PooledObject> wrap(Producer producer) { - return new DefaultPooledObject<>(producer); - } + /** + * Wraps a {@link Producer} instance inside a {@link PooledObject} to manage pool operations such + * as borrow and return. + * + * @param producer The {@link Producer} instance to wrap. + * @return The {@link PooledObject} wrapping the provided {@link Producer}. + */ + @Override + public PooledObject> wrap(Producer producer) { + return new DefaultPooledObject<>(producer); + } - /** - * Destroys a {@link Producer} instance when it is no longer needed by the pool, ensuring that - * resources are released and the producer is properly closed. - * - * @param topic The topic associated with the producer to be destroyed. - * @param pooledObject The pooled object wrapping the producer that needs to be destroyed. - * @throws Exception if an error occurs during the closing of the producer. - */ - @Override - public void destroyObject(String topic, PooledObject> pooledObject) throws Exception { - pooledObject.getObject().close(); - } + /** + * Destroys a {@link Producer} instance when it is no longer needed by the pool, ensuring that + * resources are released and the producer is properly closed. + * + * @param topic The topic associated with the producer to be destroyed. + * @param pooledObject The pooled object wrapping the producer that needs to be destroyed. + * @throws Exception if an error occurs during the closing of the producer. + */ + @Override + public void destroyObject(String topic, PooledObject> pooledObject) throws Exception { + pooledObject.getObject().close(); + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplySender.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplySender.java index efb44e3..614c42c 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplySender.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplySender.java @@ -27,10 +27,11 @@ /** * A utility class for sending reply messages back to clients or reporting errors encountered during - * the processing of requests. This class manages a pool of {@link Producer} instances for sending messages. + * the processing of requests. This class manages a pool of {@link Producer} instances for sending + * messages. * - *

The {@link ReplySender} utilizes a {@link KeyedObjectPool} to manage {@link Producer} instances - * for different topics to optimize resource usage and manage producer lifecycle.

+ *

The {@link ReplySender} utilizes a {@link KeyedObjectPool} to manage {@link Producer} + * instances for different topics to optimize resource usage and manage producer lifecycle. * * @param The type of the request payload. * @param The type of the reply payload. @@ -38,71 +39,89 @@ @Slf4j @RequiredArgsConstructor(access = AccessLevel.PACKAGE) class ReplySender { - private final KeyedObjectPool> pool; - private final BiConsumer rollBackFunction; + private final KeyedObjectPool> pool; + private final BiConsumer rollBackFunction; - /** - * Sends a reply message to a given topic with specified correlation ID and value. - * - * @param topic The topic to which the reply is sent. - * @param correlationId The unique identifier of the request to which this reply corresponds. - * @param reply The reply content to send. - * @param value The original value received with the request, used for rollback purposes if needed. - * @param sub The subscriber name involved in this interaction. - */ - @SneakyThrows - void sendReply(String topic, String correlationId, V reply, T value, String sub, - long delayedAt) { - onSend(topic, correlationId, msg -> msg.value(reply), value, sub, delayedAt); - } + /** + * Sends a reply message to a given topic with specified correlation ID and value. + * + * @param topic The topic to which the reply is sent. + * @param correlationId The unique identifier of the request to which this reply corresponds. + * @param reply The reply content to send. + * @param value The original value received with the request, used for rollback purposes if + * needed. + * @param sub The subscriber name involved in this interaction. + */ + @SneakyThrows + void sendReply(String topic, String correlationId, V reply, T value, String sub, long delayedAt) { + onSend(topic, correlationId, msg -> msg.value(reply), value, sub, delayedAt); + } - /** - * Sends an error reply to a given topic indicating that an error occurred during processing of the request. - * - * @param topic The topic to which the error reply is sent. - * @param correlationId The unique identifier of the request for which this error reply is sent. - * @param errorMessage The error message to include in the reply. - * @param value The original value received with the request, used for rollback purposes if needed. - * @param sub The subscriber name involved in this interaction. - */ - @SneakyThrows - void sendErrorReply(String topic, String correlationId, String errorMessage, T value, String sub, - long delayedAt) { - onSend(topic, correlationId, msg -> msg.property(ERROR_MESSAGE, errorMessage).value(null), - value, sub, delayedAt); - } + /** + * Sends an error reply to a given topic indicating that an error occurred during processing of + * the request. + * + * @param topic The topic to which the error reply is sent. + * @param correlationId The unique identifier of the request for which this error reply is sent. + * @param errorMessage The error message to include in the reply. + * @param value The original value received with the request, used for rollback purposes if + * needed. + * @param sub The subscriber name involved in this interaction. + */ + @SneakyThrows + void sendErrorReply( + String topic, + String correlationId, + String errorMessage, + T value, + String sub, + long delayedAt) { + onSend( + topic, + correlationId, + msg -> msg.property(ERROR_MESSAGE, errorMessage).value(null), + value, + sub, + delayedAt); + } - /** - * Internal method to handle the mechanics of sending replies or error messages. - * It manages the acquisition and return of {@link Producer} instances from a pool. - * - * @param topic The topic to which the message is sent. - * @param correlationId The correlation ID associated with the message. - * @param consumer A consumer that sets the properties of the message to be sent. - * @param value The original value received with the request. - * @param sub The subscriber name to be included in the message metadata. - */ - @SneakyThrows - void onSend(String topic, String correlationId, java.util.function.Consumer> consumer, - T value, String sub, long delayedAt) { - log.debug("Sending {}", correlationId); - Producer producer = pool.borrowObject(topic); - try { - TypedMessageBuilder builder = producer.newMessage() - .key(correlationId) - .property(SERVER_SUB, sub); - if (delayedAt > 0) { - builder.property(REQUEST_DELIVER_AT_TIME, String.valueOf(delayedAt)); - } - consumer.accept(builder); - builder.sendAsync() - .exceptionally(e -> { - log.error("Failed to send reply", e); - rollBackFunction.accept(correlationId, value); - return null; - }); - } finally { - pool.returnObject(topic, producer); - } + /** + * Internal method to handle the mechanics of sending replies or error messages. It manages the + * acquisition and return of {@link Producer} instances from a pool. + * + * @param topic The topic to which the message is sent. + * @param correlationId The correlation ID associated with the message. + * @param consumer A consumer that sets the properties of the message to be sent. + * @param value The original value received with the request. + * @param sub The subscriber name to be included in the message metadata. + */ + @SneakyThrows + void onSend( + String topic, + String correlationId, + java.util.function.Consumer> consumer, + T value, + String sub, + long delayedAt) { + log.debug("Sending {}", correlationId); + Producer producer = pool.borrowObject(topic); + try { + TypedMessageBuilder builder = + producer.newMessage().key(correlationId).property(SERVER_SUB, sub); + if (delayedAt > 0) { + builder.property(REQUEST_DELIVER_AT_TIME, String.valueOf(delayedAt)); + } + consumer.accept(builder); + builder + .sendAsync() + .exceptionally( + e -> { + log.error("Failed to send reply", e); + rollBackFunction.accept(correlationId, value); + return null; + }); + } finally { + pool.returnObject(topic, producer); } + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/RequestListener.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/RequestListener.java index 3f0f0f7..11c71ca 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/RequestListener.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/RequestListener.java @@ -30,8 +30,9 @@ import org.apache.pulsar.client.api.MessageListener; /** - * Handles incoming Pulsar messages, processes them using a specified function, and sends replies using a - * {@link ReplySender}. This listener is typically used on the server side of a Pulsar RPC implementation. + * Handles incoming Pulsar messages, processes them using a specified function, and sends replies + * using a {@link ReplySender}. This listener is typically used on the server side of a Pulsar RPC + * implementation. * * @param the type of the request messages * @param the type of the response messages @@ -39,55 +40,64 @@ @Slf4j @RequiredArgsConstructor(access = AccessLevel.PACKAGE) class RequestListener implements MessageListener { - private final Function> requestFunction; - private final ReplySender sender; - private final BiConsumer rollBackFunction; + private final Function> requestFunction; + private final ReplySender sender; + private final BiConsumer rollBackFunction; - /** - * Processes received messages by applying a function to generate replies, which are then sent back - * to the client. Handles request timeouts and errors during processing. - * - * @param consumer The consumer that received the message. - * @param msg The message received from the client. - */ - @Override - public void received(Consumer consumer, Message msg) { - long replyTimeout = Long.parseLong(msg.getProperty(REQUEST_TIMEOUT_MILLIS)) - - (System.currentTimeMillis() - msg.getPublishTime()); - if (replyTimeout <= 0 && !msg.hasProperty(REQUEST_DELIVER_AT_TIME)) { - consumer.acknowledgeAsync(msg); - return; - } + /** + * Processes received messages by applying a function to generate replies, which are then sent + * back to the client. Handles request timeouts and errors during processing. + * + * @param consumer The consumer that received the message. + * @param msg The message received from the client. + */ + @Override + public void received(Consumer consumer, Message msg) { + long replyTimeout = + Long.parseLong(msg.getProperty(REQUEST_TIMEOUT_MILLIS)) + - (System.currentTimeMillis() - msg.getPublishTime()); + if (replyTimeout <= 0 && !msg.hasProperty(REQUEST_DELIVER_AT_TIME)) { + consumer.acknowledgeAsync(msg); + return; + } - String correlationId = msg.getKey(); - String requestSubscription = consumer.getSubscription(); - String replyTopic = msg.getProperty(REPLY_TOPIC); - T value = msg.getValue(); - long delayedAt = msg.hasProperty(REQUEST_DELIVER_AT_TIME) - ? Long.parseLong(msg.getProperty(REQUEST_DELIVER_AT_TIME)) + String correlationId = msg.getKey(); + String requestSubscription = consumer.getSubscription(); + String replyTopic = msg.getProperty(REPLY_TOPIC); + T value = msg.getValue(); + long delayedAt = + msg.hasProperty(REQUEST_DELIVER_AT_TIME) + ? Long.parseLong(msg.getProperty(REQUEST_DELIVER_AT_TIME)) + Long.parseLong(msg.getProperty(REQUEST_TIMEOUT_MILLIS)) - : 0; - try { - requestFunction.apply(value) - .orTimeout(replyTimeout, TimeUnit.MILLISECONDS) - .thenAccept(reply -> { - sender.sendReply(replyTopic, correlationId, reply, value, requestSubscription, delayedAt); - }) - .get(); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof TimeoutException) { - log.error("[{}] Timeout and rollback", correlationId, e); - rollBackFunction.accept(correlationId, value); - } else { - log.error("[{}] Error processing request", correlationId, e); - sender.sendErrorReply(replyTopic, correlationId, - cause.getClass().getName() + ": " + cause.getMessage(), - value, requestSubscription, delayedAt); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } + : 0; + try { + requestFunction + .apply(value) + .orTimeout(replyTimeout, TimeUnit.MILLISECONDS) + .thenAccept( + reply -> { + sender.sendReply( + replyTopic, correlationId, reply, value, requestSubscription, delayedAt); + }) + .get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof TimeoutException) { + log.error("[{}] Timeout and rollback", correlationId, e); + rollBackFunction.accept(correlationId, value); + } else { + log.error("[{}] Error processing request", correlationId, e); + sender.sendErrorReply( + replyTopic, + correlationId, + cause.getClass().getName() + ": " + cause.getMessage(), + value, + requestSubscription, + delayedAt); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); } + } } diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/package-info.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/package-info.java index b833cc9..d79f2ab 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/package-info.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/package-info.java @@ -13,25 +13,28 @@ */ /** * Provides the server-side components necessary for building a RPC system based on Apache Pulsar. - * This package includes classes that handle server-side request processing and response dispatching, - * leveraging Pulsar's messaging capabilities. + * This package includes classes that handle server-side request processing and response + * dispatching, leveraging Pulsar's messaging capabilities. * *

Key components include: + * *

    - *
  • {@link org.apache.pulsar.rpc.contrib.server.PulsarRpcServer} - The RPC client used for sending replies - * and processing requests.
  • - *
  • {@link org.apache.pulsar.rpc.contrib.server.PulsarRpcServerImpl} - PulsarRpcServer implementation classes. - *
  • {@link org.apache.pulsar.rpc.contrib.server.PulsarRpcServerBuilder} - Aids in constructing a - * {@link org.apache.pulsar.rpc.contrib.server.PulsarRpcServer} instance with customized settings.
  • - *
  • {@link org.apache.pulsar.rpc.contrib.server.PulsarRpcServerBuilderImpl} - PulsarRpcServerBuilder - * implementation classes.
  • - *
  • {@link org.apache.pulsar.rpc.contrib.server.RequestListener} - Implements the Pulsar - * {@link org.apache.pulsar.client.api.MessageListener} interface to process messages as RPC requests.
  • - *
  • {@link org.apache.pulsar.rpc.contrib.server.ReplySender} - Manages sending responses back to clients through - * Pulsar producers.
  • - *
  • {@link org.apache.pulsar.rpc.contrib.server.ReplyProducerPoolFactory} - Manages a pool of Pulsar producers - * for efficient response dispatching.
  • + *
  • {@link org.apache.pulsar.rpc.contrib.server.PulsarRpcServer} - The RPC client used for + * sending replies and processing requests. + *
  • {@link org.apache.pulsar.rpc.contrib.server.PulsarRpcServerImpl} - PulsarRpcServer + * implementation classes. + *
  • {@link org.apache.pulsar.rpc.contrib.server.PulsarRpcServerBuilder} - Aids in constructing + * a {@link org.apache.pulsar.rpc.contrib.server.PulsarRpcServer} instance with customized + * settings. + *
  • {@link org.apache.pulsar.rpc.contrib.server.PulsarRpcServerBuilderImpl} - + * PulsarRpcServerBuilder implementation classes. + *
  • {@link org.apache.pulsar.rpc.contrib.server.RequestListener} - Implements the Pulsar {@link + * org.apache.pulsar.client.api.MessageListener} interface to process messages as RPC + * requests. + *
  • {@link org.apache.pulsar.rpc.contrib.server.ReplySender} - Manages sending responses back + * to clients through Pulsar producers. + *
  • {@link org.apache.pulsar.rpc.contrib.server.ReplyProducerPoolFactory} - Manages a pool of + * Pulsar producers for efficient response dispatching. *
- * */ package org.apache.pulsar.rpc.contrib.server; diff --git a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/PulsarRpcClientTest.java b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/PulsarRpcClientTest.java index f8de3ef..e94683a 100644 --- a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/PulsarRpcClientTest.java +++ b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/PulsarRpcClientTest.java @@ -37,164 +37,203 @@ @Slf4j public class PulsarRpcClientTest extends PulsarRpcBase { - @BeforeMethod(alwaysRun = true) - protected void setup() throws Exception { - super.internalSetup(); - } - - @AfterMethod(alwaysRun = true) - protected void cleanup() throws Exception { - super.internalCleanup(); - } - - @Test - public void testPulsarRpcClient() throws Exception { - setupTopic("testPulsarRpcClient"); - Map requestProducerConfigMap = new HashMap<>(); - requestProducerConfigMap.put("producerName", "requestProducer"); - requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); - rpcClient = PulsarRpcClient.builder(requestSchema, replySchema) - .requestTopic(requestTopic) - .requestProducerConfig(requestProducerConfigMap) - .replyTopic(replyTopic) - .replySubscription(replySubBase) - .replyTimeout(replyTimeout) - .patternAutoDiscoveryInterval(Duration.ofSeconds(1)) - .build(pulsarClient); - - sendRequest(); - Awaitility.await().atMost(30, TimeUnit.SECONDS) - .until(() -> rpcClient.pendingRequestSize() == messageNum); - } - - @Test - public void testPulsarRpcClientWithReplyTopicsPattern() throws Exception { - setupTopic("pattern"); - Map requestProducerConfigMap = new HashMap<>(); - requestProducerConfigMap.put("producerName", "requestProducer"); - requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); - rpcClient = PulsarRpcClient.builder(requestSchema, replySchema) - .requestTopic(requestTopic) - .requestProducerConfig(requestProducerConfigMap) - .replyTopicsPattern(replyTopicPattern) - .replySubscription(replySubBase) - .replyTimeout(replyTimeout) - .patternAutoDiscoveryInterval(Duration.ofSeconds(1)) - .build(pulsarClient); - - sendRequest(); - Awaitility.await().atMost(30, TimeUnit.SECONDS) - .until(() -> rpcClient.pendingRequestSize() == messageNum); - } - - @Test - public void testPulsarRpcClientWithRequestCallBack() throws Exception { - setupTopic("testPulsarRpcClientWithRequestCallBack"); - Map requestProducerConfigMap = new HashMap<>(); - requestProducerConfigMap.put("producerName", "requestProducer"); - requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); - - AtomicInteger counter = new AtomicInteger(); - - RequestCallBack callBack = new RequestCallBack<>() { - @Override - public void onSendRequestSuccess(String correlationId, MessageId messageId) { - log.info(" CorrelationId[{}] Send request message success. MessageId: {}", - correlationId, messageId); - } - - @Override - public void onSendRequestError(String correlationId, Throwable t, - CompletableFuture replyFuture) { - log.warn(" CorrelationId[{}] Send request message failed. {}", - correlationId, t.getMessage()); - replyFuture.completeExceptionally(t); - } - - @Override - public void onReplySuccess(String correlationId, String subscription, - TestReply value, CompletableFuture replyFuture) { - log.info(" CorrelationId[{}] Subscription[{}] Receive reply message success. Value: {}", - correlationId, subscription, value); - replyFuture.complete(value); - } - - @Override - public void onReplyError(String correlationId, String subscription, - String errorMessage, CompletableFuture replyFuture) { - log.warn(" CorrelationId[{}] Subscription[{}] Receive reply message failed. {}", - correlationId, subscription, errorMessage); - replyFuture.completeExceptionally(new Exception(errorMessage)); - } - - @Override - public void onTimeout(String correlationId, Throwable t) { - log.warn(" CorrelationId[{}] Receive reply message timed out. {}", - correlationId, t.getMessage()); - counter.incrementAndGet(); - } - - @Override - public void onReplyMessageAckFailed(String correlationId, Consumer consumer, - Message msg, Throwable t) { - consumer.acknowledgeAsync(msg.getMessageId()).exceptionally(ex -> { - log.warn(" [{}] [{}] Acknowledging message {} failed again.", - msg.getTopicName(), correlationId, msg.getMessageId(), ex); - return null; - }); - } + @BeforeMethod(alwaysRun = true) + protected void setup() throws Exception { + super.internalSetup(); + } + + @AfterMethod(alwaysRun = true) + protected void cleanup() throws Exception { + super.internalCleanup(); + } + + @Test + public void testPulsarRpcClient() throws Exception { + setupTopic("testPulsarRpcClient"); + Map requestProducerConfigMap = new HashMap<>(); + requestProducerConfigMap.put("producerName", "requestProducer"); + requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); + rpcClient = + PulsarRpcClient.builder(requestSchema, replySchema) + .requestTopic(requestTopic) + .requestProducerConfig(requestProducerConfigMap) + .replyTopic(replyTopic) + .replySubscription(replySubBase) + .replyTimeout(replyTimeout) + .patternAutoDiscoveryInterval(Duration.ofSeconds(1)) + .build(pulsarClient); + + sendRequest(); + Awaitility.await() + .atMost(30, TimeUnit.SECONDS) + .until(() -> rpcClient.pendingRequestSize() == messageNum); + } + + @Test + public void testPulsarRpcClientWithReplyTopicsPattern() throws Exception { + setupTopic("pattern"); + Map requestProducerConfigMap = new HashMap<>(); + requestProducerConfigMap.put("producerName", "requestProducer"); + requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); + rpcClient = + PulsarRpcClient.builder(requestSchema, replySchema) + .requestTopic(requestTopic) + .requestProducerConfig(requestProducerConfigMap) + .replyTopicsPattern(replyTopicPattern) + .replySubscription(replySubBase) + .replyTimeout(replyTimeout) + .patternAutoDiscoveryInterval(Duration.ofSeconds(1)) + .build(pulsarClient); + + sendRequest(); + Awaitility.await() + .atMost(30, TimeUnit.SECONDS) + .until(() -> rpcClient.pendingRequestSize() == messageNum); + } + + @Test + public void testPulsarRpcClientWithRequestCallBack() throws Exception { + setupTopic("testPulsarRpcClientWithRequestCallBack"); + Map requestProducerConfigMap = new HashMap<>(); + requestProducerConfigMap.put("producerName", "requestProducer"); + requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); + + AtomicInteger counter = new AtomicInteger(); + + RequestCallBack callBack = + new RequestCallBack<>() { + @Override + public void onSendRequestSuccess(String correlationId, MessageId messageId) { + log.info( + " CorrelationId[{}] Send request message success. MessageId: {}", + correlationId, + messageId); + } + + @Override + public void onSendRequestError( + String correlationId, Throwable t, CompletableFuture replyFuture) { + log.warn( + " CorrelationId[{}] Send request message failed. {}", + correlationId, + t.getMessage()); + replyFuture.completeExceptionally(t); + } + + @Override + public void onReplySuccess( + String correlationId, + String subscription, + TestReply value, + CompletableFuture replyFuture) { + log.info( + " CorrelationId[{}] Subscription[{}] Receive reply message success. Value: {}", + correlationId, + subscription, + value); + replyFuture.complete(value); + } + + @Override + public void onReplyError( + String correlationId, + String subscription, + String errorMessage, + CompletableFuture replyFuture) { + log.warn( + " CorrelationId[{}] Subscription[{}] Receive reply message failed. {}", + correlationId, + subscription, + errorMessage); + replyFuture.completeExceptionally(new Exception(errorMessage)); + } + + @Override + public void onTimeout(String correlationId, Throwable t) { + log.warn( + " CorrelationId[{}] Receive reply message timed out. {}", + correlationId, + t.getMessage()); + counter.incrementAndGet(); + } + + @Override + public void onReplyMessageAckFailed( + String correlationId, + Consumer consumer, + Message msg, + Throwable t) { + consumer + .acknowledgeAsync(msg.getMessageId()) + .exceptionally( + ex -> { + log.warn( + " [{}] [{}] Acknowledging message {} failed again.", + msg.getTopicName(), + correlationId, + msg.getMessageId(), + ex); + return null; + }); + } }; - rpcClient = PulsarRpcClient.builder(requestSchema, replySchema) - .requestProducerConfig(requestProducerConfigMap) - .requestTopic(requestTopic) - .replyTopic(replyTopic) - .replySubscription(replySubBase) - .replyTimeout(replyTimeout) - .patternAutoDiscoveryInterval(Duration.ofSeconds(1)) - .requestCallBack(callBack) - .build(pulsarClient); - - sendRequest(); - Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() -> counter.get() == messageNum + 1); + rpcClient = + PulsarRpcClient.builder(requestSchema, replySchema) + .requestProducerConfig(requestProducerConfigMap) + .requestTopic(requestTopic) + .replyTopic(replyTopic) + .replySubscription(replySubBase) + .replyTimeout(replyTimeout) + .patternAutoDiscoveryInterval(Duration.ofSeconds(1)) + .requestCallBack(callBack) + .build(pulsarClient); + + sendRequest(); + Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() -> counter.get() == messageNum + 1); + } + + private void sendRequest() { + Map requestMessageConfigMap = new HashMap<>(); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); + + // Synchronous Send + String correlationId = correlationIdSupplier.get(); + TestRequest requestMessage = new TestRequest(synchronousMessage); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + try { + rpcClient.request(correlationId, requestMessage, requestMessageConfigMap); + log.info("[Synchronous] Reply message: {}, KEY: {}", requestMessage, correlationId); + Assert.fail("Because we only started the PulsarRpcClient. Request will timed out."); + } catch (Exception e) { + log.error("An unexpected error occurred while sending the request: " + e.getMessage(), e); + String expectedMessage = "java.util.concurrent.TimeoutException"; + Assert.assertTrue( + e.getMessage().contains(expectedMessage), + "The exception message should contain '" + expectedMessage + "'."); } - private void sendRequest() { - Map requestMessageConfigMap = new HashMap<>(); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); - - // Synchronous Send - String correlationId = correlationIdSupplier.get(); - TestRequest requestMessage = new TestRequest(synchronousMessage); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - try { - rpcClient.request(correlationId, requestMessage, requestMessageConfigMap); - log.info("[Synchronous] Reply message: {}, KEY: {}", requestMessage, correlationId); - Assert.fail("Because we only started the PulsarRpcClient. Request will timed out."); - } catch (Exception e) { - log.error("An unexpected error occurred while sending the request: " + e.getMessage(), e); - String expectedMessage = "java.util.concurrent.TimeoutException"; - Assert.assertTrue(e.getMessage().contains(expectedMessage), - "The exception message should contain '" + expectedMessage + "'."); - } - - // Asynchronous Send - for (int i = 0; i < messageNum; i++) { - String asyncCorrelationId = correlationIdSupplier.get(); - TestRequest asyncRequestMessage = new TestRequest(asynchronousMessage + i); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - - rpcClient.requestAsync(asyncCorrelationId, asyncRequestMessage, requestMessageConfigMap) - .whenComplete((replyMessage, e) -> { - if (e != null) { - log.error("error", e); - Assert.fail("Request timed out."); - } else { - log.info("[Asynchronous] Reply message: {}, KEY: {}", - replyMessage.value(), asyncCorrelationId); - } - rpcClient.removeRequest(asyncCorrelationId); - }); - } + // Asynchronous Send + for (int i = 0; i < messageNum; i++) { + String asyncCorrelationId = correlationIdSupplier.get(); + TestRequest asyncRequestMessage = new TestRequest(asynchronousMessage + i); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + + rpcClient + .requestAsync(asyncCorrelationId, asyncRequestMessage, requestMessageConfigMap) + .whenComplete( + (replyMessage, e) -> { + if (e != null) { + log.error("error", e); + Assert.fail("Request timed out."); + } else { + log.info( + "[Asynchronous] Reply message: {}, KEY: {}", + replyMessage.value(), + asyncCorrelationId); + } + rpcClient.removeRequest(asyncCorrelationId); + }); } + } } diff --git a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/PulsarRpcServerTest.java b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/PulsarRpcServerTest.java index e970f57..610a6e0 100644 --- a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/PulsarRpcServerTest.java +++ b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/PulsarRpcServerTest.java @@ -29,71 +29,79 @@ @Slf4j public class PulsarRpcServerTest extends PulsarRpcBase { - @BeforeMethod(alwaysRun = true) - protected void setup() throws Exception { - super.internalSetup(); - } + @BeforeMethod(alwaysRun = true) + protected void setup() throws Exception { + super.internalSetup(); + } - @AfterMethod(alwaysRun = true) - protected void cleanup() throws Exception { - super.internalCleanup(); - } + @AfterMethod(alwaysRun = true) + protected void cleanup() throws Exception { + super.internalCleanup(); + } - @Test - public void testPulsarRpcServer() throws Exception { - setupTopic("testPulsarRpcServer"); - final int defaultEpoch = 1; - AtomicInteger epoch = new AtomicInteger(defaultEpoch); + @Test + public void testPulsarRpcServer() throws Exception { + setupTopic("testPulsarRpcServer"); + final int defaultEpoch = 1; + AtomicInteger epoch = new AtomicInteger(defaultEpoch); - // What do we do when we receive the request message - Function> requestFunction = request -> { - epoch.getAndIncrement(); - return CompletableFuture.completedFuture(new TestReply(request.value() + "-----------done")); + // What do we do when we receive the request message + Function> requestFunction = + request -> { + epoch.getAndIncrement(); + return CompletableFuture.completedFuture( + new TestReply(request.value() + "-----------done")); }; - // If the server side is stateful, an error occurs after the server side executes 3-1, and a mechanism for - // checking and rolling back needs to be provided. - BiConsumer rollbackFunction = (id, request) -> { - if (epoch.get() != defaultEpoch) { - epoch.set(defaultEpoch); - } + // If the server side is stateful, an error occurs after the server side executes 3-1, and a + // mechanism for + // checking and rolling back needs to be provided. + BiConsumer rollbackFunction = + (id, request) -> { + if (epoch.get() != defaultEpoch) { + epoch.set(defaultEpoch); + } }; - PulsarRpcServerBuilder rpcServerBuilder = - PulsarRpcServer.builder(requestSchema, replySchema) - .requestSubscription(requestSubBase) - .patternAutoDiscoveryInterval(Duration.ofSeconds(1)); - rpcServerBuilder.requestTopic(requestTopic); - rpcServer = rpcServerBuilder.build(pulsarClient, requestFunction, rollbackFunction); - Thread.sleep(3000); - } + PulsarRpcServerBuilder rpcServerBuilder = + PulsarRpcServer.builder(requestSchema, replySchema) + .requestSubscription(requestSubBase) + .patternAutoDiscoveryInterval(Duration.ofSeconds(1)); + rpcServerBuilder.requestTopic(requestTopic); + rpcServer = rpcServerBuilder.build(pulsarClient, requestFunction, rollbackFunction); + Thread.sleep(3000); + } - @Test - public void testPulsarRpcServerWithPattern() throws Exception { - setupTopic("pattern"); - final int defaultEpoch = 1; - AtomicInteger epoch = new AtomicInteger(defaultEpoch); + @Test + public void testPulsarRpcServerWithPattern() throws Exception { + setupTopic("pattern"); + final int defaultEpoch = 1; + AtomicInteger epoch = new AtomicInteger(defaultEpoch); - // What do we do when we receive the request message - Function> requestFunction = request -> { - epoch.getAndIncrement(); - return CompletableFuture.completedFuture(new TestReply(request.value() + "-----------done")); + // What do we do when we receive the request message + Function> requestFunction = + request -> { + epoch.getAndIncrement(); + return CompletableFuture.completedFuture( + new TestReply(request.value() + "-----------done")); }; - // If the server side is stateful, an error occurs after the server side executes 3-1, and a mechanism for - // checking and rolling back needs to be provided. - BiConsumer rollbackFunction = (id, request) -> { - if (epoch.get() != defaultEpoch) { - epoch.set(defaultEpoch); - } + // If the server side is stateful, an error occurs after the server side executes 3-1, and a + // mechanism for + // checking and rolling back needs to be provided. + BiConsumer rollbackFunction = + (id, request) -> { + if (epoch.get() != defaultEpoch) { + epoch.set(defaultEpoch); + } }; - PulsarRpcServerBuilder rpcServerBuilder = - PulsarRpcServer.builder(requestSchema, replySchema) - .requestSubscription(requestSubBase) - .patternAutoDiscoveryInterval(Duration.ofSeconds(1)); - rpcServerBuilder.requestTopicsPattern(requestTopicPattern); - rpcServer = rpcServerBuilder.build(pulsarClient, requestFunction, rollbackFunction); - Thread.sleep(3000); - } + PulsarRpcServerBuilder rpcServerBuilder = + PulsarRpcServer.builder(requestSchema, replySchema) + .requestSubscription(requestSubBase) + .patternAutoDiscoveryInterval(Duration.ofSeconds(1)); + rpcServerBuilder.requestTopicsPattern(requestTopicPattern); + rpcServer = rpcServerBuilder.build(pulsarClient, requestFunction, rollbackFunction); + Thread.sleep(3000); + } } diff --git a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/SimpleRpcCallTest.java b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/SimpleRpcCallTest.java index b8839de..81426d4 100644 --- a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/SimpleRpcCallTest.java +++ b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/SimpleRpcCallTest.java @@ -46,540 +46,677 @@ @Slf4j public class SimpleRpcCallTest extends PulsarRpcBase { - private Function> requestFunction; - private BiConsumer rollbackFunction; - - @BeforeMethod(alwaysRun = true) - protected void setup() throws Exception { - super.internalSetup(); - } - - @AfterMethod(alwaysRun = true) - protected void cleanup() throws Exception { - super.internalCleanup(); - } - - @Test - public void testRpcCall() throws Exception { - setupTopic("testRpcCall"); - Map requestProducerConfigMap = new HashMap<>(); - requestProducerConfigMap.put("producerName", "requestProducer"); - requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); - - // 1.Create PulsarRpcClient - rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, null, null); - - // 2.Create PulsarRpcServerImpl - final int defaultEpoch = 1; - AtomicInteger epoch = new AtomicInteger(defaultEpoch); - // What do we do when we receive the request message - requestFunction = request -> { - epoch.getAndIncrement(); - return CompletableFuture.completedFuture(new TestReply(request.value() + "-----------done")); + private Function> + requestFunction; + private BiConsumer rollbackFunction; + + @BeforeMethod(alwaysRun = true) + protected void setup() throws Exception { + super.internalSetup(); + } + + @AfterMethod(alwaysRun = true) + protected void cleanup() throws Exception { + super.internalCleanup(); + } + + @Test + public void testRpcCall() throws Exception { + setupTopic("testRpcCall"); + Map requestProducerConfigMap = new HashMap<>(); + requestProducerConfigMap.put("producerName", "requestProducer"); + requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); + + // 1.Create PulsarRpcClient + rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, null, null); + + // 2.Create PulsarRpcServerImpl + final int defaultEpoch = 1; + AtomicInteger epoch = new AtomicInteger(defaultEpoch); + // What do we do when we receive the request message + requestFunction = + request -> { + epoch.getAndIncrement(); + return CompletableFuture.completedFuture( + new TestReply(request.value() + "-----------done")); }; - // If the server side is stateful, an error occurs after the server side executes 3-1, and a mechanism for - // checking and rolling back needs to be provided. - rollbackFunction = (id, request) -> { - if (epoch.get() != defaultEpoch) { - epoch.set(defaultEpoch); - } + // If the server side is stateful, an error occurs after the server side executes 3-1, and a + // mechanism for + // checking and rolling back needs to be provided. + rollbackFunction = + (id, request) -> { + if (epoch.get() != defaultEpoch) { + epoch.set(defaultEpoch); + } }; - rpcServer = createPulsarRpcServer(pulsarClient, requestSubBase, requestFunction, rollbackFunction, null); + rpcServer = + createPulsarRpcServer( + pulsarClient, requestSubBase, requestFunction, rollbackFunction, null); - ConcurrentHashMap resultMap = new ConcurrentHashMap<>(); + ConcurrentHashMap resultMap = new ConcurrentHashMap<>(); - Map requestMessageConfigMap = new HashMap<>(); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); + Map requestMessageConfigMap = new HashMap<>(); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); - // 3-1.Synchronous Send - for (int i = 0; i < messageNum; i++) { - String correlationId = correlationIdSupplier.get(); - TestRequest message = new TestRequest(synchronousMessage + i); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + // 3-1.Synchronous Send + for (int i = 0; i < messageNum; i++) { + String correlationId = correlationIdSupplier.get(); + TestRequest message = new TestRequest(synchronousMessage + i); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - TestReply reply = rpcClient.request(correlationId, message, requestMessageConfigMap); - resultMap.put(correlationId, reply); - log.info("[Synchronous] Reply message: {}, KEY: {}", reply.value(), correlationId); - rpcClient.removeRequest(correlationId); - } + TestReply reply = rpcClient.request(correlationId, message, requestMessageConfigMap); + resultMap.put(correlationId, reply); + log.info("[Synchronous] Reply message: {}, KEY: {}", reply.value(), correlationId); + rpcClient.removeRequest(correlationId); + } - // 3-2.Asynchronous Send - for (int i = 0; i < messageNum; i++) { - String asyncCorrelationId = correlationIdSupplier.get(); - TestRequest message = new TestRequest(asynchronousMessage + i); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + // 3-2.Asynchronous Send + for (int i = 0; i < messageNum; i++) { + String asyncCorrelationId = correlationIdSupplier.get(); + TestRequest message = new TestRequest(asynchronousMessage + i); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - rpcClient.requestAsync(asyncCorrelationId, message).whenComplete((replyMessage, e) -> { + rpcClient + .requestAsync(asyncCorrelationId, message) + .whenComplete( + (replyMessage, e) -> { if (e != null) { - log.error("error", e); + log.error("error", e); } else { - resultMap.put(asyncCorrelationId, replyMessage); - log.info("[Asynchronous] Reply message: {}, KEY: {}", replyMessage.value(), asyncCorrelationId); + resultMap.put(asyncCorrelationId, replyMessage); + log.info( + "[Asynchronous] Reply message: {}, KEY: {}", + replyMessage.value(), + asyncCorrelationId); } rpcClient.removeRequest(asyncCorrelationId); - }); - } - Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() -> resultMap.size() == messageNum * 2); + }); } - - @Test - public void testRpcCallWithCallBack() throws Exception { - setupTopic("testRpcCallWithCallBack"); - Map requestProducerConfigMap = new HashMap<>(); - requestProducerConfigMap.put("producerName", "requestProducer"); - requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); - - Map resultMap = new ConcurrentHashMap<>(); - final int ackNums = 2; - - RequestCallBack callBack = new RequestCallBack<>() { - @Override - public void onSendRequestSuccess(String correlationId, MessageId messageId) { - log.info(" CorrelationId[{}] Send request message success. MessageId: {}", - correlationId, messageId); - } - - @Override - public void onSendRequestError(String correlationId, Throwable t, - CompletableFuture replyFuture) { - log.warn(" CorrelationId[{}] Send request message failed. {}", - correlationId, t.getMessage()); - replyFuture.completeExceptionally(t); - } - - @Override - public void onReplySuccess(String correlationId, String subscription, - TestReply value, CompletableFuture replyFuture) { - log.info(" CorrelationId[{}] Subscription[{}] Receive reply message success. Value: {}", - correlationId, subscription, value); - if (resultMap.get(correlationId).getAndIncrement() == ackNums - 1) { - rpcClient.removeRequest(correlationId); - } - replyFuture.complete(value); - } - - @Override - public void onReplyError(String correlationId, String subscription, - String errorMessage, CompletableFuture replyFuture) { - log.warn(" CorrelationId[{}] Subscription[{}] Receive reply message failed. {}", - correlationId, subscription, errorMessage); - replyFuture.completeExceptionally(new Exception(errorMessage)); - } - - @Override - public void onTimeout(String correlationId, Throwable t) { - log.warn(" CorrelationId[{}] Receive reply message timed out. {}", - correlationId, t.getMessage()); - } - - @Override - public void onReplyMessageAckFailed(String correlationId, Consumer consumer, - Message msg, Throwable t) { - consumer.acknowledgeAsync(msg.getMessageId()).exceptionally(ex -> { - log.warn(" [{}] [{}] Acknowledging message {} failed again.", - msg.getTopicName(), correlationId, msg.getMessageId(), ex); - return null; - }); + Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() -> resultMap.size() == messageNum * 2); + } + + @Test + public void testRpcCallWithCallBack() throws Exception { + setupTopic("testRpcCallWithCallBack"); + Map requestProducerConfigMap = new HashMap<>(); + requestProducerConfigMap.put("producerName", "requestProducer"); + requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); + + Map resultMap = new ConcurrentHashMap<>(); + final int ackNums = 2; + + RequestCallBack callBack = + new RequestCallBack<>() { + @Override + public void onSendRequestSuccess(String correlationId, MessageId messageId) { + log.info( + " CorrelationId[{}] Send request message success. MessageId: {}", + correlationId, + messageId); + } + + @Override + public void onSendRequestError( + String correlationId, Throwable t, CompletableFuture replyFuture) { + log.warn( + " CorrelationId[{}] Send request message failed. {}", + correlationId, + t.getMessage()); + replyFuture.completeExceptionally(t); + } + + @Override + public void onReplySuccess( + String correlationId, + String subscription, + TestReply value, + CompletableFuture replyFuture) { + log.info( + " CorrelationId[{}] Subscription[{}] Receive reply message success. Value: {}", + correlationId, + subscription, + value); + if (resultMap.get(correlationId).getAndIncrement() == ackNums - 1) { + rpcClient.removeRequest(correlationId); } + replyFuture.complete(value); + } + + @Override + public void onReplyError( + String correlationId, + String subscription, + String errorMessage, + CompletableFuture replyFuture) { + log.warn( + " CorrelationId[{}] Subscription[{}] Receive reply message failed. {}", + correlationId, + subscription, + errorMessage); + replyFuture.completeExceptionally(new Exception(errorMessage)); + } + + @Override + public void onTimeout(String correlationId, Throwable t) { + log.warn( + " CorrelationId[{}] Receive reply message timed out. {}", + correlationId, + t.getMessage()); + } + + @Override + public void onReplyMessageAckFailed( + String correlationId, + Consumer consumer, + Message msg, + Throwable t) { + consumer + .acknowledgeAsync(msg.getMessageId()) + .exceptionally( + ex -> { + log.warn( + " [{}] [{}] Acknowledging message {} failed again.", + msg.getTopicName(), + correlationId, + msg.getMessageId(), + ex); + return null; + }); + } }; - rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, null, callBack); + rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, null, callBack); - final int defaultEpoch = 1; - AtomicInteger epoch = new AtomicInteger(defaultEpoch); - // What do we do when we receive the request message - requestFunction = request -> { - epoch.getAndIncrement(); - return CompletableFuture.completedFuture(new TestReply(request.value() + "-----------done")); + final int defaultEpoch = 1; + AtomicInteger epoch = new AtomicInteger(defaultEpoch); + // What do we do when we receive the request message + requestFunction = + request -> { + epoch.getAndIncrement(); + return CompletableFuture.completedFuture( + new TestReply(request.value() + "-----------done")); }; - // If the server side is stateful, an error occurs after the server side executes 3-1, and a mechanism for - // checking and rolling back needs to be provided. - rollbackFunction = (id, request) -> { - if (epoch.get() != defaultEpoch) { - epoch.set(defaultEpoch); - } + // If the server side is stateful, an error occurs after the server side executes 3-1, and a + // mechanism for + // checking and rolling back needs to be provided. + rollbackFunction = + (id, request) -> { + if (epoch.get() != defaultEpoch) { + epoch.set(defaultEpoch); + } }; - PulsarRpcServer rpcServer1 = createPulsarRpcServer(pulsarClient, requestSubBase + "-1", - requestFunction, rollbackFunction, null); - PulsarRpcServer rpcServer2 = createPulsarRpcServer(pulsarClient, requestSubBase + "-2", - requestFunction, rollbackFunction, null); - PulsarRpcServer rpcServer3 = createPulsarRpcServer(pulsarClient, requestSubBase + "-3", - requestFunction, rollbackFunction, null); - - Map requestMessageConfigMap = new HashMap<>(); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); - for (int i = 0; i < messageNum; i++) { - String correlationId = correlationIdSupplier.get(); - TestRequest message = new TestRequest(asynchronousMessage + i); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - resultMap.put(correlationId, new AtomicInteger()); - rpcClient.requestAsync(correlationId, message, requestMessageConfigMap); - } - Awaitility.await().atMost(20, TimeUnit.SECONDS).until(() -> { - AtomicInteger success = new AtomicInteger(); - resultMap.forEach((__, count) -> success.getAndAdd(count.get())); - return success.get() == messageNum * ackNums; - }); - rpcServer1.close(); - rpcServer2.close(); - rpcServer3.close(); + PulsarRpcServer rpcServer1 = + createPulsarRpcServer( + pulsarClient, requestSubBase + "-1", requestFunction, rollbackFunction, null); + PulsarRpcServer rpcServer2 = + createPulsarRpcServer( + pulsarClient, requestSubBase + "-2", requestFunction, rollbackFunction, null); + PulsarRpcServer rpcServer3 = + createPulsarRpcServer( + pulsarClient, requestSubBase + "-3", requestFunction, rollbackFunction, null); + + Map requestMessageConfigMap = new HashMap<>(); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); + for (int i = 0; i < messageNum; i++) { + String correlationId = correlationIdSupplier.get(); + TestRequest message = new TestRequest(asynchronousMessage + i); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + resultMap.put(correlationId, new AtomicInteger()); + rpcClient.requestAsync(correlationId, message, requestMessageConfigMap); } - - @Test - public void testRpcCallWithPattern() throws Exception { - setupTopic("pattern"); - Map requestProducerConfigMap = new HashMap<>(); - requestProducerConfigMap.put("producerName", "requestProducer"); - requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); - - // 1.Create PulsarRpcClient - rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, replyTopicPattern, null); - - // 2.Create PulsarRpcServerImpl - final int defaultEpoch = 1; - AtomicInteger epoch = new AtomicInteger(defaultEpoch); - // What do we do when we receive the request message - requestFunction = request -> { - epoch.getAndIncrement(); - return CompletableFuture.completedFuture(new TestReply(request.value() + "-----------done")); + Awaitility.await() + .atMost(20, TimeUnit.SECONDS) + .until( + () -> { + AtomicInteger success = new AtomicInteger(); + resultMap.forEach((__, count) -> success.getAndAdd(count.get())); + return success.get() == messageNum * ackNums; + }); + rpcServer1.close(); + rpcServer2.close(); + rpcServer3.close(); + } + + @Test + public void testRpcCallWithPattern() throws Exception { + setupTopic("pattern"); + Map requestProducerConfigMap = new HashMap<>(); + requestProducerConfigMap.put("producerName", "requestProducer"); + requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); + + // 1.Create PulsarRpcClient + rpcClient = + createPulsarRpcClient(pulsarClient, requestProducerConfigMap, replyTopicPattern, null); + + // 2.Create PulsarRpcServerImpl + final int defaultEpoch = 1; + AtomicInteger epoch = new AtomicInteger(defaultEpoch); + // What do we do when we receive the request message + requestFunction = + request -> { + epoch.getAndIncrement(); + return CompletableFuture.completedFuture( + new TestReply(request.value() + "-----------done")); }; - // If the server side is stateful, an error occurs after the server side executes 3-1, and a mechanism for - // checking and rolling back needs to be provided. - rollbackFunction = (id, request) -> { - if (epoch.get() != defaultEpoch) { - epoch.set(defaultEpoch); - } + // If the server side is stateful, an error occurs after the server side executes 3-1, and a + // mechanism for + // checking and rolling back needs to be provided. + rollbackFunction = + (id, request) -> { + if (epoch.get() != defaultEpoch) { + epoch.set(defaultEpoch); + } }; - rpcServer = createPulsarRpcServer(pulsarClient, requestSubBase, requestFunction, rollbackFunction, - requestTopicPattern); - - ConcurrentHashMap resultMap = new ConcurrentHashMap<>(); - Map requestMessageConfigMap = new HashMap<>(); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); - - // 3-1.Synchronous Send - for (int i = 0; i < messageNum; i++) { - String correlationId = correlationIdSupplier.get(); - TestRequest message = new TestRequest(synchronousMessage + i); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - TestReply reply = rpcClient.request(correlationId, message, requestMessageConfigMap); - resultMap.put(correlationId, reply); - log.info("[Synchronous] Reply message: {}, KEY: {}", reply.value(), correlationId); - rpcClient.removeRequest(correlationId); - } - - // 3-2.Asynchronous Send - for (int i = 0; i < messageNum; i++) { - String correlationId = correlationIdSupplier.get(); - TestRequest message = new TestRequest(asynchronousMessage + i); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - rpcClient.requestAsync(correlationId, message, requestMessageConfigMap).whenComplete((replyMessage, e) -> { + rpcServer = + createPulsarRpcServer( + pulsarClient, requestSubBase, requestFunction, rollbackFunction, requestTopicPattern); + + ConcurrentHashMap resultMap = new ConcurrentHashMap<>(); + Map requestMessageConfigMap = new HashMap<>(); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); + + // 3-1.Synchronous Send + for (int i = 0; i < messageNum; i++) { + String correlationId = correlationIdSupplier.get(); + TestRequest message = new TestRequest(synchronousMessage + i); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + TestReply reply = rpcClient.request(correlationId, message, requestMessageConfigMap); + resultMap.put(correlationId, reply); + log.info("[Synchronous] Reply message: {}, KEY: {}", reply.value(), correlationId); + rpcClient.removeRequest(correlationId); + } + + // 3-2.Asynchronous Send + for (int i = 0; i < messageNum; i++) { + String correlationId = correlationIdSupplier.get(); + TestRequest message = new TestRequest(asynchronousMessage + i); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + rpcClient + .requestAsync(correlationId, message, requestMessageConfigMap) + .whenComplete( + (replyMessage, e) -> { if (e != null) { - log.error("error", e); + log.error("error", e); } else { - resultMap.put(correlationId, replyMessage); - log.info("[Asynchronous] Reply message: {}, KEY: {}", replyMessage.value(), correlationId); + resultMap.put(correlationId, replyMessage); + log.info( + "[Asynchronous] Reply message: {}, KEY: {}", + replyMessage.value(), + correlationId); } rpcClient.removeRequest(correlationId); - }); - } - Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() -> resultMap.size() == messageNum * 2); + }); } - - @Test - public void testRpcCallTimeout() throws Exception { - setupTopic("testRpcCallTimeout"); - Map requestProducerConfigMap = new HashMap<>(); - requestProducerConfigMap.put("producerName", "requestProducer"); - requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); - - // 1.Create PulsarRpcClient - rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, null, null); - - // 2.Create PulsarRpcServerImpl - final int defaultEpoch = 1; - AtomicInteger epoch = new AtomicInteger(defaultEpoch); - // What do we do when we receive the request message - requestFunction = request -> { - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - epoch.getAndIncrement(); - return CompletableFuture.completedFuture(new TestReply(request.value() + "-----------done")); + Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() -> resultMap.size() == messageNum * 2); + } + + @Test + public void testRpcCallTimeout() throws Exception { + setupTopic("testRpcCallTimeout"); + Map requestProducerConfigMap = new HashMap<>(); + requestProducerConfigMap.put("producerName", "requestProducer"); + requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); + + // 1.Create PulsarRpcClient + rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, null, null); + + // 2.Create PulsarRpcServerImpl + final int defaultEpoch = 1; + AtomicInteger epoch = new AtomicInteger(defaultEpoch); + // What do we do when we receive the request message + requestFunction = + request -> { + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + epoch.getAndIncrement(); + return CompletableFuture.completedFuture( + new TestReply(request.value() + "-----------done")); }; - // If the server side is stateful, an error occurs after the server side executes 3-1, and a mechanism for - // checking and rolling back needs to be provided. - rollbackFunction = (id, request) -> { - if (epoch.get() != defaultEpoch) { - epoch.set(defaultEpoch); - } + // If the server side is stateful, an error occurs after the server side executes 3-1, and a + // mechanism for + // checking and rolling back needs to be provided. + rollbackFunction = + (id, request) -> { + if (epoch.get() != defaultEpoch) { + epoch.set(defaultEpoch); + } }; - rpcServer = createPulsarRpcServer(pulsarClient, requestSubBase, requestFunction, rollbackFunction, null); - - ConcurrentHashMap resultMap = new ConcurrentHashMap<>(); - int messageNum = 1; - Map requestMessageConfigMap = new HashMap<>(); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); - - // 3-1.Synchronous Send - for (int i = 0; i < messageNum; i++) { - String correlationId = correlationIdSupplier.get(); - try { - TestRequest message = new TestRequest(synchronousMessage + i); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - TestReply reply = rpcClient.request(correlationId, message, requestMessageConfigMap); - resultMap.put(correlationId, reply); - log.info("[Synchronous] Reply message: {}, KEY: {}", reply.value(), correlationId); - Assert.fail("Request timed out."); - } catch (Exception e) { - log.error("An unexpected error occurred while sending the request: " + e.getMessage(), e); - String expectedMessage = "java.util.concurrent.TimeoutException"; - Assert.assertTrue(e.getMessage().contains(expectedMessage), - "The exception message should contain '" + expectedMessage + "'."); - resultMap.put(correlationId, new TestReply(e.getMessage())); - } - } - - // 3-2.Asynchronous Send - for (int i = 0; i < messageNum; i++) { - String correlationId = correlationIdSupplier.get(); - TestRequest message = new TestRequest(asynchronousMessage + i); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - rpcClient.requestAsync(correlationId, message, requestMessageConfigMap).whenComplete((replyMessage, e) -> { + rpcServer = + createPulsarRpcServer( + pulsarClient, requestSubBase, requestFunction, rollbackFunction, null); + + ConcurrentHashMap resultMap = new ConcurrentHashMap<>(); + int messageNum = 1; + Map requestMessageConfigMap = new HashMap<>(); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); + + // 3-1.Synchronous Send + for (int i = 0; i < messageNum; i++) { + String correlationId = correlationIdSupplier.get(); + try { + TestRequest message = new TestRequest(synchronousMessage + i); + requestMessageConfigMap.put( + TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + TestReply reply = rpcClient.request(correlationId, message, requestMessageConfigMap); + resultMap.put(correlationId, reply); + log.info("[Synchronous] Reply message: {}, KEY: {}", reply.value(), correlationId); + Assert.fail("Request timed out."); + } catch (Exception e) { + log.error("An unexpected error occurred while sending the request: " + e.getMessage(), e); + String expectedMessage = "java.util.concurrent.TimeoutException"; + Assert.assertTrue( + e.getMessage().contains(expectedMessage), + "The exception message should contain '" + expectedMessage + "'."); + resultMap.put(correlationId, new TestReply(e.getMessage())); + } + } + + // 3-2.Asynchronous Send + for (int i = 0; i < messageNum; i++) { + String correlationId = correlationIdSupplier.get(); + TestRequest message = new TestRequest(asynchronousMessage + i); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + rpcClient + .requestAsync(correlationId, message, requestMessageConfigMap) + .whenComplete( + (replyMessage, e) -> { if (e != null) { - log.error("error", e); - resultMap.put(correlationId, new TestReply(e.getMessage())); - Assert.fail("Request timed out."); + log.error("error", e); + resultMap.put(correlationId, new TestReply(e.getMessage())); + Assert.fail("Request timed out."); } else { - resultMap.put(correlationId, replyMessage); - log.info("[Asynchronous] Reply message: {}, KEY: {}", replyMessage.value(), correlationId); + resultMap.put(correlationId, replyMessage); + log.info( + "[Asynchronous] Reply message: {}, KEY: {}", + replyMessage.value(), + correlationId); } - }); - } - Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() -> resultMap.size() == messageNum * 2); + }); } - - @Test - public void testRpcCallProcessFailedOnServerSide() throws Exception { - setupTopic("testRpcCallProcessFailedOnServerSide"); - Map requestProducerConfigMap = new HashMap<>(); - requestProducerConfigMap.put("producerName", "requestProducer"); - requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); - - // 1.Create PulsarRpcClient - rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, null, null); - - // 2.Create PulsarRpcServer - final int defaultEpoch = 1; - AtomicInteger epoch = new AtomicInteger(defaultEpoch); - // What do we do when we receive the request message - requestFunction = request -> { - try { - epoch.getAndIncrement(); - int i = epoch.get() / 0; - return CompletableFuture.completedFuture(new TestReply(request.value() + "-----------done")); - } catch (Exception e) { - return CompletableFuture.failedFuture(e); - } + Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() -> resultMap.size() == messageNum * 2); + } + + @Test + public void testRpcCallProcessFailedOnServerSide() throws Exception { + setupTopic("testRpcCallProcessFailedOnServerSide"); + Map requestProducerConfigMap = new HashMap<>(); + requestProducerConfigMap.put("producerName", "requestProducer"); + requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); + + // 1.Create PulsarRpcClient + rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, null, null); + + // 2.Create PulsarRpcServer + final int defaultEpoch = 1; + AtomicInteger epoch = new AtomicInteger(defaultEpoch); + // What do we do when we receive the request message + requestFunction = + request -> { + try { + epoch.getAndIncrement(); + int i = epoch.get() / 0; + return CompletableFuture.completedFuture( + new TestReply(request.value() + "-----------done")); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } }; - // If the server side is stateful, an error occurs after the server side executes 3-1, and a mechanism for - // checking and rolling back needs to be provided. - rollbackFunction = (id, request) -> { - if (epoch.get() != defaultEpoch) { - epoch.set(defaultEpoch); - } + // If the server side is stateful, an error occurs after the server side executes 3-1, and a + // mechanism for + // checking and rolling back needs to be provided. + rollbackFunction = + (id, request) -> { + if (epoch.get() != defaultEpoch) { + epoch.set(defaultEpoch); + } }; - rpcServer = createPulsarRpcServer(pulsarClient, requestSubBase, requestFunction, rollbackFunction, null); - - ConcurrentHashMap resultMap = new ConcurrentHashMap<>(); - int messageNum = 1; - Map requestMessageConfigMap = new HashMap<>(); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); - - // 3-1.Synchronous Send - for (int i = 0; i < messageNum; i++) { - String correlationId = correlationIdSupplier.get(); - try { - TestRequest message = new TestRequest(synchronousMessage + i); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - TestReply reply = rpcClient.request(correlationId, message, requestMessageConfigMap); - resultMap.put(correlationId, reply); - log.info("[Synchronous] Reply message: {}, KEY: {}", reply.value(), correlationId); - Assert.fail("Server process failed."); - } catch (Exception e) { - log.error("An unexpected error occurred while sending the request: " + e.getMessage(), e); - resultMap.put(correlationId, new TestReply(e.getMessage())); - } - } - - // 3-2.Asynchronous Send - for (int i = 0; i < messageNum; i++) { - String correlationId = correlationIdSupplier.get(); - TestRequest message = new TestRequest(asynchronousMessage + i); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); - rpcClient.requestAsync(correlationId, message, requestMessageConfigMap).whenComplete((replyMessage, e) -> { + rpcServer = + createPulsarRpcServer( + pulsarClient, requestSubBase, requestFunction, rollbackFunction, null); + + ConcurrentHashMap resultMap = new ConcurrentHashMap<>(); + int messageNum = 1; + Map requestMessageConfigMap = new HashMap<>(); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); + + // 3-1.Synchronous Send + for (int i = 0; i < messageNum; i++) { + String correlationId = correlationIdSupplier.get(); + try { + TestRequest message = new TestRequest(synchronousMessage + i); + requestMessageConfigMap.put( + TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + TestReply reply = rpcClient.request(correlationId, message, requestMessageConfigMap); + resultMap.put(correlationId, reply); + log.info("[Synchronous] Reply message: {}, KEY: {}", reply.value(), correlationId); + Assert.fail("Server process failed."); + } catch (Exception e) { + log.error("An unexpected error occurred while sending the request: " + e.getMessage(), e); + resultMap.put(correlationId, new TestReply(e.getMessage())); + } + } + + // 3-2.Asynchronous Send + for (int i = 0; i < messageNum; i++) { + String correlationId = correlationIdSupplier.get(); + TestRequest message = new TestRequest(asynchronousMessage + i); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, System.currentTimeMillis()); + rpcClient + .requestAsync(correlationId, message, requestMessageConfigMap) + .whenComplete( + (replyMessage, e) -> { if (e != null) { - log.error("error", e); - resultMap.put(correlationId, new TestReply(e.getMessage())); - Assert.fail("Server process failed."); + log.error("error", e); + resultMap.put(correlationId, new TestReply(e.getMessage())); + Assert.fail("Server process failed."); } else { - resultMap.put(correlationId, replyMessage); - log.info("[Asynchronous] Reply message: {}, KEY: {}", replyMessage.value(), correlationId); - String expectedMessage = "java.lang.ArithmeticException"; - Assert.assertTrue(e.getMessage().contains(expectedMessage), - "The exception message should contain '" + expectedMessage + "'."); + resultMap.put(correlationId, replyMessage); + log.info( + "[Asynchronous] Reply message: {}, KEY: {}", + replyMessage.value(), + correlationId); + String expectedMessage = "java.lang.ArithmeticException"; + Assert.assertTrue( + e.getMessage().contains(expectedMessage), + "The exception message should contain '" + expectedMessage + "'."); } - }); - } - Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() -> resultMap.size() == messageNum * 2); + }); } - - @Test - public void testDelayedRpcAt() throws Exception { - setupTopic("testDelayedRpcAt"); - Map requestProducerConfigMap = new HashMap<>(); - requestProducerConfigMap.put("producerName", "requestProducer"); - requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); - - Map resultMap = new ConcurrentHashMap<>(); - final int ackNums = 2; - - RequestCallBack callBack = new RequestCallBack<>() { - @Override - public void onSendRequestSuccess(String correlationId, MessageId messageId) { - log.info(" CorrelationId[{}] Send request message success. MessageId: {}", - correlationId, messageId); - } - - @Override - public void onSendRequestError(String correlationId, Throwable t, - CompletableFuture replyFuture) { - log.warn(" CorrelationId[{}] Send request message failed. {}", - correlationId, t.getMessage()); - replyFuture.completeExceptionally(t); - } - - @Override - public void onReplySuccess(String correlationId, String subscription, - TestReply value, CompletableFuture replyFuture) { - log.info(" CorrelationId[{}] Subscription[{}] Receive reply message success. Value: {}", - correlationId, subscription, value); - if (resultMap.get(correlationId).getAndIncrement() == ackNums - 1) { - rpcClient.removeRequest(correlationId); - } - replyFuture.complete(value); - } - - @Override - public void onReplyError(String correlationId, String subscription, - String errorMessage, CompletableFuture replyFuture) { - log.warn(" CorrelationId[{}] Subscription[{}] Receive reply message failed. {}", - correlationId, subscription, errorMessage); - replyFuture.completeExceptionally(new Exception(errorMessage)); - } - - @Override - public void onTimeout(String correlationId, Throwable t) { - log.warn(" CorrelationId[{}] Receive reply message timed out. {}", - correlationId, t.getMessage()); - } - - @Override - public void onReplyMessageAckFailed(String correlationId, Consumer consumer, - Message msg, Throwable t) { - consumer.acknowledgeAsync(msg.getMessageId()).exceptionally(ex -> { - log.warn(" [{}] [{}] Acknowledging message {} failed again.", - msg.getTopicName(), correlationId, msg.getMessageId(), ex); - return null; - }); + Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() -> resultMap.size() == messageNum * 2); + } + + @Test + public void testDelayedRpcAt() throws Exception { + setupTopic("testDelayedRpcAt"); + Map requestProducerConfigMap = new HashMap<>(); + requestProducerConfigMap.put("producerName", "requestProducer"); + requestProducerConfigMap.put("messageRoutingMode", MessageRoutingMode.RoundRobinPartition); + + Map resultMap = new ConcurrentHashMap<>(); + final int ackNums = 2; + + RequestCallBack callBack = + new RequestCallBack<>() { + @Override + public void onSendRequestSuccess(String correlationId, MessageId messageId) { + log.info( + " CorrelationId[{}] Send request message success. MessageId: {}", + correlationId, + messageId); + } + + @Override + public void onSendRequestError( + String correlationId, Throwable t, CompletableFuture replyFuture) { + log.warn( + " CorrelationId[{}] Send request message failed. {}", + correlationId, + t.getMessage()); + replyFuture.completeExceptionally(t); + } + + @Override + public void onReplySuccess( + String correlationId, + String subscription, + TestReply value, + CompletableFuture replyFuture) { + log.info( + " CorrelationId[{}] Subscription[{}] Receive reply message success. Value: {}", + correlationId, + subscription, + value); + if (resultMap.get(correlationId).getAndIncrement() == ackNums - 1) { + rpcClient.removeRequest(correlationId); } + replyFuture.complete(value); + } + + @Override + public void onReplyError( + String correlationId, + String subscription, + String errorMessage, + CompletableFuture replyFuture) { + log.warn( + " CorrelationId[{}] Subscription[{}] Receive reply message failed. {}", + correlationId, + subscription, + errorMessage); + replyFuture.completeExceptionally(new Exception(errorMessage)); + } + + @Override + public void onTimeout(String correlationId, Throwable t) { + log.warn( + " CorrelationId[{}] Receive reply message timed out. {}", + correlationId, + t.getMessage()); + } + + @Override + public void onReplyMessageAckFailed( + String correlationId, + Consumer consumer, + Message msg, + Throwable t) { + consumer + .acknowledgeAsync(msg.getMessageId()) + .exceptionally( + ex -> { + log.warn( + " [{}] [{}] Acknowledging message {} failed again.", + msg.getTopicName(), + correlationId, + msg.getMessageId(), + ex); + return null; + }); + } }; - rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, null, callBack); + rpcClient = createPulsarRpcClient(pulsarClient, requestProducerConfigMap, null, callBack); - final int defaultEpoch = 1; - AtomicInteger epoch = new AtomicInteger(defaultEpoch); - // What do we do when we receive the request message - requestFunction = request -> { - epoch.getAndIncrement(); - return CompletableFuture.completedFuture(new TestReply(request.value() + "-----------done")); + final int defaultEpoch = 1; + AtomicInteger epoch = new AtomicInteger(defaultEpoch); + // What do we do when we receive the request message + requestFunction = + request -> { + epoch.getAndIncrement(); + return CompletableFuture.completedFuture( + new TestReply(request.value() + "-----------done")); }; - // If the server side is stateful, an error occurs after the server side executes 3-1, and a mechanism for - // checking and rolling back needs to be provided. - rollbackFunction = (id, request) -> { - if (epoch.get() != defaultEpoch) { - epoch.set(defaultEpoch); - } + // If the server side is stateful, an error occurs after the server side executes 3-1, and a + // mechanism for + // checking and rolling back needs to be provided. + rollbackFunction = + (id, request) -> { + if (epoch.get() != defaultEpoch) { + epoch.set(defaultEpoch); + } }; - PulsarRpcServer rpcServer1 = createPulsarRpcServer(pulsarClient, requestSubBase + "-1", - requestFunction, rollbackFunction, null); - PulsarRpcServer rpcServer2 = createPulsarRpcServer(pulsarClient, requestSubBase + "-2", - requestFunction, rollbackFunction, null); - PulsarRpcServer rpcServer3 = createPulsarRpcServer(pulsarClient, requestSubBase + "-3", - requestFunction, rollbackFunction, null); - - long delayedTime = 5000; - - Map requestMessageConfigMap = new HashMap<>(); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); - for (int i = 0; i < messageNum; i++) { - String correlationId = correlationIdSupplier.get(); - TestRequest message = new TestRequest(asynchronousMessage + i); - long eventTime = System.currentTimeMillis(); - requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, eventTime); - resultMap.put(correlationId, new AtomicInteger()); - rpcClient.requestAfterAsync(correlationId, message, requestMessageConfigMap, - delayedTime, TimeUnit.MILLISECONDS); - } - long current = System.currentTimeMillis(); - - Awaitility.await().atMost(20, TimeUnit.SECONDS).until(() -> { - AtomicInteger success = new AtomicInteger(); - resultMap.forEach((__, count) -> success.getAndAdd(count.get())); - if (System.currentTimeMillis() - current < delayedTime && success.get() > 0) { - return false; - } - return success.get() >= messageNum * ackNums && System.currentTimeMillis() - current >= delayedTime; - }); - rpcServer1.close(); - rpcServer2.close(); - rpcServer3.close(); + PulsarRpcServer rpcServer1 = + createPulsarRpcServer( + pulsarClient, requestSubBase + "-1", requestFunction, rollbackFunction, null); + PulsarRpcServer rpcServer2 = + createPulsarRpcServer( + pulsarClient, requestSubBase + "-2", requestFunction, rollbackFunction, null); + PulsarRpcServer rpcServer3 = + createPulsarRpcServer( + pulsarClient, requestSubBase + "-3", requestFunction, rollbackFunction, null); + + long delayedTime = 5000; + + Map requestMessageConfigMap = new HashMap<>(); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_DISABLE_REPLICATION, true); + for (int i = 0; i < messageNum; i++) { + String correlationId = correlationIdSupplier.get(); + TestRequest message = new TestRequest(asynchronousMessage + i); + long eventTime = System.currentTimeMillis(); + requestMessageConfigMap.put(TypedMessageBuilder.CONF_EVENT_TIME, eventTime); + resultMap.put(correlationId, new AtomicInteger()); + rpcClient.requestAfterAsync( + correlationId, message, requestMessageConfigMap, delayedTime, TimeUnit.MILLISECONDS); } - - private PulsarRpcClient createPulsarRpcClient( - PulsarClient pulsarClient, Map requestProducerConfigMap, - Pattern replyTopicsPattern, RequestCallBack callBack) throws PulsarRpcClientException { - PulsarRpcClientBuilder rpcClientBuilder = - PulsarRpcClient.builder(requestSchema, replySchema) - .requestTopic(requestTopic) - .requestProducerConfig(requestProducerConfigMap) - .replySubscription(replySubBase) - .replyTimeout(replyTimeout) - .patternAutoDiscoveryInterval(Duration.ofSeconds(1)); - if (callBack != null) { - rpcClientBuilder.requestCallBack(callBack); - } - return replyTopicsPattern == null ? rpcClientBuilder.replyTopic(replyTopic).build(pulsarClient) - : rpcClientBuilder.replyTopicsPattern(replyTopicsPattern).build(pulsarClient); + long current = System.currentTimeMillis(); + + Awaitility.await() + .atMost(20, TimeUnit.SECONDS) + .until( + () -> { + AtomicInteger success = new AtomicInteger(); + resultMap.forEach((__, count) -> success.getAndAdd(count.get())); + if (System.currentTimeMillis() - current < delayedTime && success.get() > 0) { + return false; + } + return success.get() >= messageNum * ackNums + && System.currentTimeMillis() - current >= delayedTime; + }); + rpcServer1.close(); + rpcServer2.close(); + rpcServer3.close(); + } + + private PulsarRpcClient createPulsarRpcClient( + PulsarClient pulsarClient, + Map requestProducerConfigMap, + Pattern replyTopicsPattern, + RequestCallBack callBack) + throws PulsarRpcClientException { + PulsarRpcClientBuilder rpcClientBuilder = + PulsarRpcClient.builder(requestSchema, replySchema) + .requestTopic(requestTopic) + .requestProducerConfig(requestProducerConfigMap) + .replySubscription(replySubBase) + .replyTimeout(replyTimeout) + .patternAutoDiscoveryInterval(Duration.ofSeconds(1)); + if (callBack != null) { + rpcClientBuilder.requestCallBack(callBack); } - - private PulsarRpcServer createPulsarRpcServer( - PulsarClient pulsarClient, String requestSubscription, - Function> requestFunction, - BiConsumer rollbackFunction, - Pattern requestTopicsPattern) throws PulsarRpcServerException { - PulsarRpcServerBuilder rpcServerBuilder = - PulsarRpcServer.builder(requestSchema, replySchema) - .requestSubscription(requestSubscription) - .patternAutoDiscoveryInterval(Duration.ofSeconds(1)); - if (requestTopicsPattern == null) { - rpcServerBuilder.requestTopic(requestTopic); - } else { - rpcServerBuilder.requestTopicsPattern(requestTopicsPattern); - } - return rpcServerBuilder.build(pulsarClient, requestFunction, rollbackFunction); + return replyTopicsPattern == null + ? rpcClientBuilder.replyTopic(replyTopic).build(pulsarClient) + : rpcClientBuilder.replyTopicsPattern(replyTopicsPattern).build(pulsarClient); + } + + private PulsarRpcServer createPulsarRpcServer( + PulsarClient pulsarClient, + String requestSubscription, + Function> requestFunction, + BiConsumer rollbackFunction, + Pattern requestTopicsPattern) + throws PulsarRpcServerException { + PulsarRpcServerBuilder rpcServerBuilder = + PulsarRpcServer.builder(requestSchema, replySchema) + .requestSubscription(requestSubscription) + .patternAutoDiscoveryInterval(Duration.ofSeconds(1)); + if (requestTopicsPattern == null) { + rpcServerBuilder.requestTopic(requestTopic); + } else { + rpcServerBuilder.requestTopicsPattern(requestTopicsPattern); } - + return rpcServerBuilder.build(pulsarClient, requestFunction, rollbackFunction); + } } diff --git a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/PulsarRpcBase.java b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/PulsarRpcBase.java index b70fcca..f2caeb8 100644 --- a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/PulsarRpcBase.java +++ b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/PulsarRpcBase.java @@ -26,59 +26,57 @@ @Setter public abstract class PulsarRpcBase { - protected final Supplier correlationIdSupplier = () -> randomUUID().toString(); - protected final String topicPrefix = "persistent://public/default/"; - // protected final String topicPrefix = "public/default/"; - protected String requestTopic; - protected String replyTopic; - protected Pattern requestTopicPattern; - protected Pattern replyTopicPattern; - protected String requestSubBase; - protected String replySubBase; - protected Duration replyTimeout = Duration.ofSeconds(3); - protected final String synchronousMessage = "SynchronousRequest"; - protected final String asynchronousMessage = "AsynchronousRequest"; - protected final Schema requestSchema = Schema.JSON(TestRequest.class); - protected final Schema replySchema = Schema.JSON(TestReply.class); - protected final int messageNum = 10; + protected final Supplier correlationIdSupplier = () -> randomUUID().toString(); + protected final String topicPrefix = "persistent://public/default/"; + // protected final String topicPrefix = "public/default/"; + protected String requestTopic; + protected String replyTopic; + protected Pattern requestTopicPattern; + protected Pattern replyTopicPattern; + protected String requestSubBase; + protected String replySubBase; + protected Duration replyTimeout = Duration.ofSeconds(3); + protected final String synchronousMessage = "SynchronousRequest"; + protected final String asynchronousMessage = "AsynchronousRequest"; + protected final Schema requestSchema = Schema.JSON(TestRequest.class); + protected final Schema replySchema = Schema.JSON(TestReply.class); + protected final int messageNum = 10; - protected PulsarAdmin pulsarAdmin; - protected PulsarClient pulsarClient; - protected PulsarRpcClient rpcClient; - protected PulsarRpcServer rpcServer; + protected PulsarAdmin pulsarAdmin; + protected PulsarClient pulsarClient; + protected PulsarRpcClient rpcClient; + protected PulsarRpcServer rpcServer; - protected final void internalSetup() throws Exception { - pulsarAdmin = SingletonPulsarContainer.createPulsarAdmin(); - pulsarClient = SingletonPulsarContainer.createPulsarClient(); - } + protected final void internalSetup() throws Exception { + pulsarAdmin = SingletonPulsarContainer.createPulsarAdmin(); + pulsarClient = SingletonPulsarContainer.createPulsarClient(); + } - protected final void internalCleanup() throws Exception { - pulsarClient.close(); - pulsarAdmin.topics().deletePartitionedTopic(requestTopic); - pulsarAdmin.topics().deletePartitionedTopic(replyTopic); - pulsarAdmin.close(); - if (rpcServer != null) { - rpcServer.close(); - } - if (rpcClient != null) { - rpcClient.close(); - } + protected final void internalCleanup() throws Exception { + pulsarClient.close(); + pulsarAdmin.topics().deletePartitionedTopic(requestTopic); + pulsarAdmin.topics().deletePartitionedTopic(replyTopic); + pulsarAdmin.close(); + if (rpcServer != null) { + rpcServer.close(); } - - protected void setupTopic(String topicBase) throws Exception { - this.requestTopic = topicBase + "-request"; - this.replyTopic = topicBase + "-reply"; - this.requestTopicPattern = Pattern.compile(topicPrefix + requestTopic + ".*"); - this.replyTopicPattern = Pattern.compile(topicPrefix + replyTopic + ".*"); - this.requestSubBase = requestTopic + "-sub"; - this.replySubBase = replyTopic + "-sub"; - pulsarAdmin.topics().createPartitionedTopic(requestTopic, 10); - pulsarAdmin.topics().createPartitionedTopic(replyTopic, 10); + if (rpcClient != null) { + rpcClient.close(); } + } - public record TestRequest(String value) { - } + protected void setupTopic(String topicBase) throws Exception { + this.requestTopic = topicBase + "-request"; + this.replyTopic = topicBase + "-reply"; + this.requestTopicPattern = Pattern.compile(topicPrefix + requestTopic + ".*"); + this.replyTopicPattern = Pattern.compile(topicPrefix + replyTopic + ".*"); + this.requestSubBase = requestTopic + "-sub"; + this.replySubBase = replyTopic + "-sub"; + pulsarAdmin.topics().createPartitionedTopic(requestTopic, 10); + pulsarAdmin.topics().createPartitionedTopic(replyTopic, 10); + } - public record TestReply(String value) { - } + public record TestRequest(String value) {} + + public record TestReply(String value) {} } diff --git a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/SingletonPulsarContainer.java b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/SingletonPulsarContainer.java index c418ab9..71c6f83 100644 --- a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/SingletonPulsarContainer.java +++ b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/SingletonPulsarContainer.java @@ -26,44 +26,47 @@ @Slf4j public class SingletonPulsarContainer { - private static final PulsarContainer PULSAR_CONTAINER; + private static final PulsarContainer PULSAR_CONTAINER; - static { - PULSAR_CONTAINER = new PulsarContainer(getPulsarImage()) - .withEnv("PULSAR_PREFIX_acknowledgmentAtBatchIndexLevelEnabled", "true") - .withEnv("PULSAR_PREFIX_delayedDeliveryEnabled", "true") - .withStartupTimeout(Duration.ofMinutes(3)); - PULSAR_CONTAINER.start(); - } + static { + PULSAR_CONTAINER = + new PulsarContainer(getPulsarImage()) + .withEnv("PULSAR_PREFIX_acknowledgmentAtBatchIndexLevelEnabled", "true") + .withEnv("PULSAR_PREFIX_delayedDeliveryEnabled", "true") + .withStartupTimeout(Duration.ofMinutes(3)); + PULSAR_CONTAINER.start(); + } - private static DockerImageName getPulsarImage() { - return DockerImageName.parse("apachepulsar/pulsar:" + getPulsarImageVersion()); - } + private static DockerImageName getPulsarImage() { + return DockerImageName.parse("apachepulsar/pulsar:" + getPulsarImageVersion()); + } - private static String getPulsarImageVersion() { - String pulsarVersion = ""; - Properties properties = new Properties(); - try { - properties.load(SingletonPulsarContainer.class.getClassLoader() - .getResourceAsStream("pulsar-container.properties")); - if (!properties.isEmpty()) { - pulsarVersion = properties.getProperty("pulsar.version"); - } - } catch (IOException e) { - log.error("Failed to load pulsar version. " + e.getCause()); - } - return pulsarVersion; + private static String getPulsarImageVersion() { + String pulsarVersion = ""; + Properties properties = new Properties(); + try { + properties.load( + SingletonPulsarContainer.class + .getClassLoader() + .getResourceAsStream("pulsar-container.properties")); + if (!properties.isEmpty()) { + pulsarVersion = properties.getProperty("pulsar.version"); + } + } catch (IOException e) { + log.error("Failed to load pulsar version. " + e.getCause()); } + return pulsarVersion; + } - static PulsarClient createPulsarClient() throws PulsarClientException { - return PulsarClient.builder() - .serviceUrl(SingletonPulsarContainer.PULSAR_CONTAINER.getPulsarBrokerUrl()) - .build(); - } + static PulsarClient createPulsarClient() throws PulsarClientException { + return PulsarClient.builder() + .serviceUrl(SingletonPulsarContainer.PULSAR_CONTAINER.getPulsarBrokerUrl()) + .build(); + } - static PulsarAdmin createPulsarAdmin() throws PulsarClientException { - return PulsarAdmin.builder() - .serviceHttpUrl(SingletonPulsarContainer.PULSAR_CONTAINER.getHttpServiceUrl()) - .build(); - } + static PulsarAdmin createPulsarAdmin() throws PulsarClientException { + return PulsarAdmin.builder() + .serviceHttpUrl(SingletonPulsarContainer.PULSAR_CONTAINER.getHttpServiceUrl()) + .build(); + } } diff --git a/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/SingletonPulsarContainer.java b/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/SingletonPulsarContainer.java index afd3bdc..5187a8d 100644 --- a/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/SingletonPulsarContainer.java +++ b/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/SingletonPulsarContainer.java @@ -13,6 +13,7 @@ */ package org.apache.pulsar.txn; + import java.io.IOException; import java.time.Duration; import java.util.Properties; @@ -26,45 +27,48 @@ @Slf4j public class SingletonPulsarContainer { - private static final PulsarContainer PULSAR_CONTAINER; + private static final PulsarContainer PULSAR_CONTAINER; - static { - PULSAR_CONTAINER = new PulsarContainer(getPulsarImage()) - .withEnv("PULSAR_PREFIX_acknowledgmentAtBatchIndexLevelEnabled", "true") - .withEnv("PULSAR_PREFIX_transactionCoordinatorEnabled", "true") - .withStartupTimeout(Duration.ofMinutes(3)); - PULSAR_CONTAINER.start(); - } + static { + PULSAR_CONTAINER = + new PulsarContainer(getPulsarImage()) + .withEnv("PULSAR_PREFIX_acknowledgmentAtBatchIndexLevelEnabled", "true") + .withEnv("PULSAR_PREFIX_transactionCoordinatorEnabled", "true") + .withStartupTimeout(Duration.ofMinutes(3)); + PULSAR_CONTAINER.start(); + } - private static DockerImageName getPulsarImage() { - return DockerImageName.parse("apachepulsar/pulsar:" + getPulsarImageVersion()); - } + private static DockerImageName getPulsarImage() { + return DockerImageName.parse("apachepulsar/pulsar:" + getPulsarImageVersion()); + } - private static String getPulsarImageVersion() { - String pulsarVersion = ""; - Properties properties = new Properties(); - try { - properties.load(SingletonPulsarContainer.class.getClassLoader() - .getResourceAsStream("pulsar-container.properties")); - if (!properties.isEmpty()) { - pulsarVersion = properties.getProperty("pulsar.version"); - } - } catch (IOException e) { - log.error("Failed to load pulsar version. " + e.getCause()); - } - return pulsarVersion; + private static String getPulsarImageVersion() { + String pulsarVersion = ""; + Properties properties = new Properties(); + try { + properties.load( + SingletonPulsarContainer.class + .getClassLoader() + .getResourceAsStream("pulsar-container.properties")); + if (!properties.isEmpty()) { + pulsarVersion = properties.getProperty("pulsar.version"); + } + } catch (IOException e) { + log.error("Failed to load pulsar version. " + e.getCause()); } + return pulsarVersion; + } - static PulsarClient createPulsarClient() throws PulsarClientException { - return PulsarClient.builder() - .serviceUrl(SingletonPulsarContainer.PULSAR_CONTAINER.getPulsarBrokerUrl()) - .enableTransaction(true) - .build(); - } + static PulsarClient createPulsarClient() throws PulsarClientException { + return PulsarClient.builder() + .serviceUrl(SingletonPulsarContainer.PULSAR_CONTAINER.getPulsarBrokerUrl()) + .enableTransaction(true) + .build(); + } - static PulsarAdmin createPulsarAdmin() throws PulsarClientException { - return PulsarAdmin.builder() - .serviceHttpUrl(SingletonPulsarContainer.PULSAR_CONTAINER.getHttpServiceUrl()) - .build(); - } + static PulsarAdmin createPulsarAdmin() throws PulsarClientException { + return PulsarAdmin.builder() + .serviceHttpUrl(SingletonPulsarContainer.PULSAR_CONTAINER.getHttpServiceUrl()) + .build(); + } } diff --git a/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/TransactionDemo.java b/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/TransactionDemo.java index 308c521..c398e22 100644 --- a/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/TransactionDemo.java +++ b/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/TransactionDemo.java @@ -38,15 +38,18 @@ public void transactionDemo() throws Exception { // Create a Transaction object // Use TransactionFactory to create a transaction object with a timeout of 5 seconds Transaction transaction = - TransactionFactory.createTransaction(client, 5, TimeUnit.SECONDS).get(); + TransactionFactory.createTransaction(client, 5, TimeUnit.SECONDS).get(); // Create producers and a consumer // Create two producers to send messages to different topics - Producer producerToPubTopic = client.newProducer(Schema.STRING).topic(pubTopic).create(); - Producer producerToSubTopic = client.newProducer(Schema.STRING).topic(subTopic).create(); + Producer producerToPubTopic = + client.newProducer(Schema.STRING).topic(pubTopic).create(); + Producer producerToSubTopic = + client.newProducer(Schema.STRING).topic(subTopic).create(); // Create a consumer to receive messages from the subTopic - Consumer consumerFromSubTopic = client + Consumer consumerFromSubTopic = + client .newConsumer(Schema.STRING) .subscriptionName(subscription) .topic(subTopic) @@ -79,4 +82,4 @@ public void transactionDemo() throws Exception { producerToSubTopic.close(); client.close(); } -} \ No newline at end of file +} From d25930e76ec895ceed12abb872cb4e433c8c9221 Mon Sep 17 00:00:00 2001 From: lushiji Date: Mon, 3 Nov 2025 11:22:55 +0800 Subject: [PATCH 2/2] [fix] change checkstyle.xml --- .../admin/mcp/transport/TransportManagerTest.java | 1 + .../rpc/contrib/client/PulsarRpcClientImpl.java | 1 + .../pulsar/rpc/contrib/client/ReplyListener.java | 1 + .../pulsar/rpc/contrib/client/RequestSender.java | 1 + .../contrib/common/MessageDispatcherFactory.java | 1 + .../rpc/contrib/server/PulsarRpcServerImpl.java | 1 + .../pulsar/rpc/contrib/server/ReplySender.java | 1 + .../pulsar/rpc/contrib/server/RequestListener.java | 1 + .../pulsar/rpc/contrib/base/PulsarRpcBase.java | 1 + .../org/apache/pulsar/txn/TransactionImplTest.java | 13 +++++++++---- src/main/resources/checkstyle.xml | 1 + 11 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/TransportManagerTest.java b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/TransportManagerTest.java index 0ed6fa9..b45f1b0 100644 --- a/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/TransportManagerTest.java +++ b/pulsar-admin-mcp-contrib/src/test/java/org/apache/pulsar/admin/mcp/transport/TransportManagerTest.java @@ -20,6 +20,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; + import org.apache.pulsar.admin.mcp.config.PulsarMCPCliOptions; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientImpl.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientImpl.java index 4c100f9..b0e9b30 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientImpl.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/PulsarRpcClientImpl.java @@ -15,6 +15,7 @@ import static lombok.AccessLevel.PACKAGE; import static org.apache.pulsar.rpc.contrib.common.Constants.REQUEST_DELIVER_AT_TIME; + import java.io.IOException; import java.time.Duration; import java.util.Map; diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/ReplyListener.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/ReplyListener.java index db6a35a..f348c26 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/ReplyListener.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/ReplyListener.java @@ -16,6 +16,7 @@ import static org.apache.pulsar.rpc.contrib.common.Constants.ERROR_MESSAGE; import static org.apache.pulsar.rpc.contrib.common.Constants.REQUEST_DELIVER_AT_TIME; import static org.apache.pulsar.rpc.contrib.common.Constants.SERVER_SUB; + import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import lombok.AccessLevel; diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestSender.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestSender.java index 7ab9b3f..71de825 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestSender.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/client/RequestSender.java @@ -15,6 +15,7 @@ import static org.apache.pulsar.rpc.contrib.common.Constants.REPLY_TOPIC; import static org.apache.pulsar.rpc.contrib.common.Constants.REQUEST_TIMEOUT_MILLIS; + import java.util.concurrent.CompletableFuture; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/MessageDispatcherFactory.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/MessageDispatcherFactory.java index 8d1d74b..2cf3b97 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/MessageDispatcherFactory.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/common/MessageDispatcherFactory.java @@ -14,6 +14,7 @@ package org.apache.pulsar.rpc.contrib.common; import static java.util.concurrent.TimeUnit.MILLISECONDS; + import java.io.IOException; import java.time.Duration; import java.util.Map; diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerImpl.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerImpl.java index 79f2cea..9533096 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerImpl.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/PulsarRpcServerImpl.java @@ -14,6 +14,7 @@ package org.apache.pulsar.rpc.contrib.server; import static lombok.AccessLevel.PACKAGE; + import java.io.IOException; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplySender.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplySender.java index 614c42c..90fe0a1 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplySender.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/ReplySender.java @@ -16,6 +16,7 @@ import static org.apache.pulsar.rpc.contrib.common.Constants.ERROR_MESSAGE; import static org.apache.pulsar.rpc.contrib.common.Constants.REQUEST_DELIVER_AT_TIME; import static org.apache.pulsar.rpc.contrib.common.Constants.SERVER_SUB; + import java.util.function.BiConsumer; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; diff --git a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/RequestListener.java b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/RequestListener.java index 11c71ca..844b9d3 100644 --- a/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/RequestListener.java +++ b/pulsar-rpc-contrib/src/main/java/org/apache/pulsar/rpc/contrib/server/RequestListener.java @@ -16,6 +16,7 @@ import static org.apache.pulsar.rpc.contrib.common.Constants.REPLY_TOPIC; import static org.apache.pulsar.rpc.contrib.common.Constants.REQUEST_DELIVER_AT_TIME; import static org.apache.pulsar.rpc.contrib.common.Constants.REQUEST_TIMEOUT_MILLIS; + import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; diff --git a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/PulsarRpcBase.java b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/PulsarRpcBase.java index f2caeb8..3e30be9 100644 --- a/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/PulsarRpcBase.java +++ b/pulsar-rpc-contrib/src/test/java/org/apache/pulsar/rpc/contrib/base/PulsarRpcBase.java @@ -14,6 +14,7 @@ package org.apache.pulsar.rpc.contrib.base; import static java.util.UUID.randomUUID; + import java.time.Duration; import java.util.function.Supplier; import java.util.regex.Pattern; diff --git a/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/TransactionImplTest.java b/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/TransactionImplTest.java index e4171a6..6871f38 100644 --- a/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/TransactionImplTest.java +++ b/pulsar-transaction-contrib/src/test/java/org/apache/pulsar/txn/TransactionImplTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -87,7 +88,8 @@ public void testAckAllReceivedMsgsAsync() throws ExecutionException, Interrupted CompletableFuture future = transactionImpl.ackAllReceivedMsgsAsync(); future.get(); - // Verify each consumer called the correct acknowledgeAsync method with the correct message IDs + // Verify each consumer called the correct acknowledgeAsync method with the correct message + // IDs for (int i = 0; i < mockConsumers.size(); i++) { Consumer consumer = mockConsumers.get(i); MessageId messageId = messageIds.get(i); @@ -109,7 +111,8 @@ public void testAckAllReceivedMsgs() throws ExecutionException, InterruptedExcep // Call the ackAllReceivedMsgs method transactionImpl.ackAllReceivedMsgs(consumer); - // Verify the consumer called the correct acknowledgeAsync method with the correct message IDs + // Verify the consumer called the correct acknowledgeAsync method with the correct message + // IDs verify(consumer).acknowledgeAsync(eq(List.of(messageId)), eq(mockTransaction)); // Verify the message Ids were removed from the transaction context after acked. assertEquals(transactionImpl.getReceivedMessages().size(), 0); @@ -133,7 +136,8 @@ public void testAckAllReceivedMsgsAll() throws ExecutionException, InterruptedEx // Call the ackAllReceivedMsgs method transactionImpl.ackAllReceivedMsgs(); - // Verify each consumer called the correct acknowledgeAsync method with the correct message IDs + // Verify each consumer called the correct acknowledgeAsync method with the correct message + // IDs for (int i = 0; i < mockConsumers.size(); i++) { Consumer consumer = mockConsumers.get(i); MessageId messageId = messageIds.get(i); @@ -160,7 +164,8 @@ public void testAckAllReceivedMsgsAsyncAll() throws ExecutionException, Interrup CompletableFuture future = transactionImpl.ackAllReceivedMsgsAsync(); future.get(); - // Verify each consumer called the correct acknowledgeAsync method with the correct message IDs + // Verify each consumer called the correct acknowledgeAsync method with the correct message + // IDs for (int i = 0; i < mockConsumers.size(); i++) { Consumer consumer = mockConsumers.get(i); MessageId messageId = messageIds.get(i); diff --git a/src/main/resources/checkstyle.xml b/src/main/resources/checkstyle.xml index c56dfb2..1baa7fe 100644 --- a/src/main/resources/checkstyle.xml +++ b/src/main/resources/checkstyle.xml @@ -122,6 +122,7 @@ page at http://checkstyle.sourceforge.net/config.html --> +