|
29 | 29 | import lombok.Cleanup; |
30 | 30 | import org.apache.pulsar.client.api.v5.config.SubscriptionInitialPosition; |
31 | 31 | import org.apache.pulsar.client.api.v5.schema.Schema; |
| 32 | +import org.awaitility.Awaitility; |
32 | 33 | import org.testng.annotations.Test; |
33 | 34 |
|
34 | 35 | /** |
@@ -176,6 +177,80 @@ public void cumulativeAckCoversEveryTopicSeenSoFar() throws Exception { |
176 | 177 | + (stale != null ? stale.value() : "")); |
177 | 178 | } |
178 | 179 |
|
| 180 | + @Test |
| 181 | + public void closeWithoutAckDoesNotAcknowledgeAnything() throws Exception { |
| 182 | + // A stream consumer acknowledges only via explicit acknowledgeCumulative. Closing |
| 183 | + // the consumer (or, equivalently, a crash) must NOT acknowledge anything on the |
| 184 | + // application's behalf. Regression test for a close-time flush that used to ack each |
| 185 | + // per-topic consumer up to its prefetch frontier — silently acking messages the |
| 186 | + // application had buffered but never received. We assert it at the broker: after |
| 187 | + // receiving every message but acknowledging none, the subscription backlog must |
| 188 | + // still cover every message. (With the old flush this dropped to 0.) |
| 189 | + String topicA = topicName("a"); |
| 190 | + String topicB = topicName("b"); |
| 191 | + admin.scalableTopics().createScalableTopic(topicA, 1); |
| 192 | + admin.scalableTopics().createScalableTopic(topicB, 1); |
| 193 | + |
| 194 | + @Cleanup |
| 195 | + Producer<String> pa = v5Client.newProducer(Schema.string()).topic(topicA).create(); |
| 196 | + @Cleanup |
| 197 | + Producer<String> pb = v5Client.newProducer(Schema.string()).topic(topicB).create(); |
| 198 | + |
| 199 | + String subscription = "multi-stream-close-no-ack"; |
| 200 | + StreamConsumer<String> first = v5Client.newStreamConsumer(Schema.string()) |
| 201 | + .namespace(getNamespace()) |
| 202 | + .subscriptionName(subscription) |
| 203 | + .subscriptionInitialPosition(SubscriptionInitialPosition.EARLIEST) |
| 204 | + .subscribe(); |
| 205 | + |
| 206 | + int n = 5; |
| 207 | + for (int i = 0; i < n; i++) { |
| 208 | + pa.newMessage().value("a-" + i).send(); |
| 209 | + pb.newMessage().value("b-" + i).send(); |
| 210 | + } |
| 211 | + |
| 212 | + // Receive every message — advancing each per-topic prefetch frontier — but ack |
| 213 | + // NOTHING. If close flushed that frontier, all 2n would be acknowledged here. |
| 214 | + Set<String> receivedFirst = new HashSet<>(); |
| 215 | + long deadline = System.currentTimeMillis() + 20_000L; |
| 216 | + while (receivedFirst.size() < 2 * n && System.currentTimeMillis() < deadline) { |
| 217 | + Message<String> msg = first.receive(Duration.ofSeconds(1)); |
| 218 | + if (msg != null) { |
| 219 | + receivedFirst.add(msg.value()); |
| 220 | + } |
| 221 | + } |
| 222 | + assertEquals(receivedFirst.size(), 2 * n, "first consumer should receive every message"); |
| 223 | + first.close(); |
| 224 | + |
| 225 | + // At the broker, every message must still be in the backlog: close acked nothing. |
| 226 | + Awaitility.await().untilAsserted(() -> |
| 227 | + assertEquals(subscriptionBacklog(subscription, topicA, topicB), 2L * n, |
| 228 | + "close must not acknowledge anything; full backlog must remain")); |
| 229 | + } |
| 230 | + |
| 231 | + /** |
| 232 | + * Total delivered-but-unacked backlog for {@code subscription} across every segment of |
| 233 | + * the given scalable topics. Reads the broker's Topic reference directly — the topics |
| 234 | + * REST admin does not serve the {@code segment://} domain. |
| 235 | + */ |
| 236 | + private long subscriptionBacklog(String subscription, String... scalableTopics) throws Exception { |
| 237 | + long total = 0; |
| 238 | + for (String topic : scalableTopics) { |
| 239 | + var stats = admin.scalableTopics().getStats(topic); |
| 240 | + for (var seg : stats.getSegments().values()) { |
| 241 | + var ref = getTopicReference(seg.name()); |
| 242 | + if (ref.isEmpty()) { |
| 243 | + continue; |
| 244 | + } |
| 245 | + var sub = ref.get().getSubscription(subscription); |
| 246 | + if (sub != null) { |
| 247 | + total += sub.getNumberOfEntriesInBacklog(true); |
| 248 | + } |
| 249 | + } |
| 250 | + } |
| 251 | + return total; |
| 252 | + } |
| 253 | + |
179 | 254 | @Test |
180 | 255 | public void filtersByPropertySoOnlyMatchingTopicsAttach() throws Exception { |
181 | 256 | String aliceTopic = topicName("alice"); |
|
0 commit comments