|
50 | 50 | import org.apache.pulsar.client.impl.metrics.InstrumentProvider; |
51 | 51 | import org.apache.pulsar.common.api.proto.BaseCommand; |
52 | 52 | import org.apache.pulsar.common.api.proto.BaseCommand.Type; |
| 53 | +import org.apache.pulsar.common.api.proto.CommandGetTopicsOfNamespace.Mode; |
| 54 | +import org.apache.pulsar.common.lookup.GetTopicsResult; |
| 55 | +import org.apache.pulsar.common.naming.NamespaceName; |
53 | 56 | import org.apache.pulsar.common.naming.TopicName; |
54 | 57 | import org.apache.pulsar.common.partition.PartitionedTopicMetadata; |
55 | 58 | import org.apache.pulsar.common.protocol.Commands; |
@@ -201,6 +204,136 @@ private static LookupDataResult createLookupDataResult(String brokerUrl, boolean |
201 | 204 | return lookupResult; |
202 | 205 | } |
203 | 206 |
|
| 207 | + /** |
| 208 | + * Verifies that getTopicsUnderNamespace() deduplicates concurrent requests and cleans up after completion. |
| 209 | + * |
| 210 | + * First, two concurrent calls with identical parameters should return the same CompletableFuture |
| 211 | + * and trigger only one connection pool request (deduplication). |
| 212 | + * |
| 213 | + * Second, after the future completes, the map entry should be removed so a subsequent call |
| 214 | + * with the same parameters creates a new future (cleanup). |
| 215 | + * |
| 216 | + * This test uses a never-completing connection future to isolate the deduplication logic |
| 217 | + * without executing the network request path. |
| 218 | + */ |
| 219 | + |
| 220 | + @Test(timeOut = 60000) |
| 221 | + public void testGetTopicsUnderNamespaceDeduplication() throws Exception { |
| 222 | + PulsarClientImpl client = mock(PulsarClientImpl.class); |
| 223 | + ConnectionPool cnxPool = mock(ConnectionPool.class); |
| 224 | + |
| 225 | + ClientConfigurationData conf = new ClientConfigurationData(); |
| 226 | + conf.setOperationTimeoutMs(30000); |
| 227 | + when(client.getConfiguration()).thenReturn(conf); |
| 228 | + when(client.instrumentProvider()).thenReturn(InstrumentProvider.NOOP); |
| 229 | + when(client.getCnxPool()).thenReturn(cnxPool); |
| 230 | + |
| 231 | + // Never-completing connection prevents the thenAcceptAsync callback in getTopicsUnderNamespace from executing, |
| 232 | + // isolating only the deduplication logic without network calls. |
| 233 | + CompletableFuture<ClientCnx> neverCompletes = new CompletableFuture<>(); |
| 234 | + when(cnxPool.getConnection(any(ServiceNameResolver.class))).thenReturn(neverCompletes); |
| 235 | + |
| 236 | + ScheduledExecutorService scheduler = |
| 237 | + Executors.newSingleThreadScheduledExecutor(new DefaultThreadFactory("lookup-test-sched")); |
| 238 | + |
| 239 | + try (BinaryProtoLookupService lookup = new BinaryProtoLookupService( |
| 240 | + client, "pulsar://broker:6650", null, false, scheduler, /*lookupPinnedExecutor*/ null)) { |
| 241 | + |
| 242 | + NamespaceName ns = NamespaceName.get("public", "default"); |
| 243 | + Mode mode = Mode.PERSISTENT; |
| 244 | + String pattern = ".*"; |
| 245 | + String topicsHash = null; |
| 246 | + |
| 247 | + CompletableFuture<GetTopicsResult> f1 = lookup.getTopicsUnderNamespace(ns, mode, pattern, topicsHash); |
| 248 | + CompletableFuture<GetTopicsResult> f1b = lookup.getTopicsUnderNamespace(ns, mode, pattern, topicsHash); |
| 249 | + |
| 250 | + assertSame(f1b, f1, "Concurrent requests with identical parameters should return the same future"); |
| 251 | + |
| 252 | + verify(cnxPool, times(1)).getConnection(any(ServiceNameResolver.class)); |
| 253 | + |
| 254 | + GetTopicsResult payload = new GetTopicsResult(java.util.Collections.emptyList(), null, false, true); |
| 255 | + |
| 256 | + // Complete the future. This triggers the whenComplete callback that removes the map entry. |
| 257 | + f1.complete(payload); |
| 258 | + assertTrue(f1.isDone()); |
| 259 | + |
| 260 | + // Verify cleanup: subsequent call with same parameters creates a new future. |
| 261 | + CompletableFuture<GetTopicsResult> f2 = lookup.getTopicsUnderNamespace(ns, mode, pattern, topicsHash); |
| 262 | + assertNotSame(f2, f1, |
| 263 | + "After completion, the deduplication map entry should be removed and a new future created"); |
| 264 | + verify(cnxPool, times(2)).getConnection(any(ServiceNameResolver.class)); |
| 265 | + } finally { |
| 266 | + scheduler.shutdownNow(); |
| 267 | + } |
| 268 | + } |
| 269 | + |
| 270 | + /** |
| 271 | + * Verifies that getTopicsUnderNamespace() treats different topicsHash values as distinct keys for deduplication. |
| 272 | + * |
| 273 | + * Requests with different topicsHash values should create separate futures and trigger separate connection |
| 274 | + * pool requests. Cleanup is per key. Completing one future does not affect another in-flight entry. |
| 275 | + * |
| 276 | + * This test uses a never-completing connection future to isolate the deduplication logic without executing |
| 277 | + * the network request path. |
| 278 | + */ |
| 279 | + @Test(timeOut = 60000) |
| 280 | + public void testGetTopicsUnderNamespaceDeduplicationDifferentHash() throws Exception { |
| 281 | + PulsarClientImpl client = mock(PulsarClientImpl.class); |
| 282 | + ConnectionPool cnxPool = mock(ConnectionPool.class); |
| 283 | + |
| 284 | + ClientConfigurationData conf = new ClientConfigurationData(); |
| 285 | + conf.setOperationTimeoutMs(30000); |
| 286 | + when(client.getConfiguration()).thenReturn(conf); |
| 287 | + when(client.instrumentProvider()).thenReturn(InstrumentProvider.NOOP); |
| 288 | + when(client.getCnxPool()).thenReturn(cnxPool); |
| 289 | + |
| 290 | + // Never-completing connection prevents the thenAcceptAsync callback in getTopicsUnderNamespace from executing, |
| 291 | + // isolating only the deduplication logic without network calls. |
| 292 | + CompletableFuture<ClientCnx> neverCompletes = new CompletableFuture<>(); |
| 293 | + when(cnxPool.getConnection(any(ServiceNameResolver.class))).thenReturn(neverCompletes); |
| 294 | + |
| 295 | + ScheduledExecutorService scheduler = |
| 296 | + Executors.newSingleThreadScheduledExecutor(new DefaultThreadFactory("lookup-test-sched")); |
| 297 | + |
| 298 | + try (BinaryProtoLookupService lookup = new BinaryProtoLookupService( |
| 299 | + client, "pulsar://broker:6650", null, false, scheduler, null)) { |
| 300 | + |
| 301 | + NamespaceName ns = NamespaceName.get("public", "default"); |
| 302 | + Mode mode = Mode.PERSISTENT; |
| 303 | + String pattern = ".*"; |
| 304 | + |
| 305 | + CompletableFuture<GetTopicsResult> futureHashA = lookup.getTopicsUnderNamespace(ns, mode, pattern, "HashA"); |
| 306 | + CompletableFuture<GetTopicsResult> futureHashB = lookup.getTopicsUnderNamespace(ns, mode, pattern, "HashB"); |
| 307 | + |
| 308 | + // Verify different hash values create separate futures. |
| 309 | + assertNotSame(futureHashA, futureHashB, |
| 310 | + "Requests with different topicsHash must not share the same future"); |
| 311 | + |
| 312 | + // Verify connection pool called twice, once for each distinct topicsHash. |
| 313 | + verify(cnxPool, times(2)).getConnection(any(ServiceNameResolver.class)); |
| 314 | + |
| 315 | + GetTopicsResult payload = new GetTopicsResult(java.util.Collections.emptyList(), null, false, true); |
| 316 | + |
| 317 | + futureHashA.complete(payload); |
| 318 | + |
| 319 | + // Verify cleanup for HashA: subsequent call creates a new future. |
| 320 | + CompletableFuture<GetTopicsResult> futureHashA2 = |
| 321 | + lookup.getTopicsUnderNamespace(ns, mode, pattern, "HashA"); |
| 322 | + assertNotSame(futureHashA2, futureHashA, |
| 323 | + "After completion, a call with the same topicsHash must create a new future"); |
| 324 | + verify(cnxPool, times(3)).getConnection(any(ServiceNameResolver.class)); |
| 325 | + |
| 326 | + // Verify HashB still in-flight: subsequent call returns the original future. |
| 327 | + CompletableFuture<GetTopicsResult> futureHashB2 = |
| 328 | + lookup.getTopicsUnderNamespace(ns, mode, pattern, "HashB"); |
| 329 | + assertSame(futureHashB2, futureHashB, |
| 330 | + "An in-flight request for the same topicsHash must return the same future"); |
| 331 | + verify(cnxPool, times(3)).getConnection(any(ServiceNameResolver.class)); |
| 332 | + } finally { |
| 333 | + scheduler.shutdownNow(); |
| 334 | + } |
| 335 | + } |
| 336 | + |
204 | 337 | /** |
205 | 338 | * Verifies that getPartitionedTopicMetadata() deduplicates concurrent requests and cleans up after completion. |
206 | 339 | * |
|
0 commit comments