You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by "shibd (via GitHub)" <gi...@apache.org> on 2025/09/11 08:46:43 UTC

[PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

shibd opened a new pull request, #24730:
URL: https://github.com/apache/pulsar/pull/24730

   ### Motivation
   
   This PR fixes a critical bug where the `KeyShared` sticky mode consumer in Pulsar (reproduced with the Node.js client) would incorrectly consume messages from hash ranges not explicitly assigned to it. This deviates from the expected "sticky" behavior and instead mimics "auto-split" logic.
   
   Specifically, if a single `KeyShared` consumer is configured with non-contiguous sticky ranges, e.g.:
   - `[0, 9999]`
   - `[20000, 29999]`
   - `[40000, 49999]`
   
   The consumer would incorrectly receive messages for keys falling into the gaps (e.g., `15000` which is between `9999` and `20000`). The root cause lies in the `HashRangeExclusiveStickyKeyConsumerSelector`'s `select` method, which previously stored only the start/end points of ranges in its `rangeMap`. This led to an erroneous interpretation where any hash between a range's `end` and the next range's `start` would be assigned to the previous range's consumer, effectively closing the gaps.
   
   This issue is demonstrable with:
   - The `testConsumerSelectWithMultipRanges` unit test in this PR.
   - The `testCustomStickyRange` integration test in this PR.
   
   ### Modifications
   
   This PR refactors the `HashRangeExclusiveStickyKeyConsumerSelector` to ensure strict enforcement of explicitly defined sticky ranges:
   
   1.  **Refactored `rangeMap`**: Changed `rangeMap` from `ConcurrentSkipListMap<Integer, Consumer>` to `ConcurrentSkipListMap<Integer, Pair<Range, Consumer>>`. This now allows the selector to store the complete `Range` object for each assignment, eliminating the need to infer range boundaries and enabling precise range containment checks.
   2.  **Enhanced `select(int hash)` Logic**: The `select` method is updated to directly utilize the stored `Range` objects. It now accurately checks if a given `hash` is explicitly contained within an assigned `Range` using `pair.getLeft().contains(hash)`, rather than relying on potentially ambiguous `floorEntry`/`ceilingEntry` comparisons.
   3.  **Improved `validateKeySharedMeta(Consumer consumer)` for Strict Validation**:
       * **Internal Range Validity**: A new validation step ensures that a consumer's own `KeySharedMeta` ranges are valid. It verifies that `start <= end` for all `IntRange` objects.
       * **Internal Range Overlap**: Critically, it now detects and rejects `KeySharedMeta` where a consumer's own assigned `IntRange`s internally overlap. This prevents consumers from being registered with self-conflicting configurations.
   4.  **Refined `findConflictingConsumer(List<IntRange> newConsumerRanges)`**: This method's logic was updated to leverage the explicit `Pair<Range, Consumer>` storage in `rangeMap`. It now accurately detects overlaps between a new consumer's ranges and ranges already assigned to other active consumers, utilizing `floorEntry`, `tailMap`, and an `checkRangesOverlap` helper for efficient and precise conflict detection.
   
   These modifications collectively guarantee that `HashRangeExclusiveStickyKeyConsumerSelector` strictly enforces range exclusivity and assignment according to the `KeyShared` sticky policy, resolving the unintended message consumption from unassigned ranges.
   
   ### Verifying this change
   
   - [x] Make sure that the change passes the CI checks.
   
   This change added new unit tests and an integration test and can be verified as follows:
   
   -   **`HashRangeExclusiveStickyKeyConsumerSelectorTest` (Unit Tests)**:
       * `testConsumerSelect`: Updated to verify `select(hash)` returns the correct consumer only when `hash` is within an explicitly defined range, and `null` for hashes in gaps.
       * `testConsumerSelectWithMultipRanges`: Added to confirm a single consumer with multiple distinct sticky ranges correctly selects messages only within those ranges and returns `null` for hashes in gaps.
       * `testOneConsumerRangeConflict`: Added to verify that a consumer cannot be added if its own `KeySharedMeta` contains internally conflicting or invalid (`start > end`) ranges.
       * `testSingleRangeConflict` and `testMultipleRangeConflict`: Updated to correctly assert expected conflicts and non-conflicts based on the new strict range overlap detection logic.
   -   **`KeySharedSubscriptionTest.testCustomStickyRange` (Integration Test)**:
       * A new end-to-end integration test has been added. It simulates the reported scenario with a partitioned topic and two consumers assigned non-overlapping sticky ranges. This test verifies that each consumer *only* receives messages for keys falling within its *explicitly assigned ranges*, confirming the fix for the unintended auto-split behavior.
   
   ### Does this pull request potentially affect one of the following parts:
   
   *If the box was checked, please highlight the changes*
   
   - [ ] Dependencies (add or upgrade a dependency)
   - [ ] The public API
   - [ ] The schema
   - [ ] The default values of configurations
   - [ ] The threading model
   - [x] The binary protocol (Changes to `KeySharedMeta` validation might affect how clients sending invalid `IntRange` lists are handled by the broker, potentially rejecting previously accepted invalid configurations.)
   - [ ] The REST endpoints
   - [ ] The admin CLI options
   - [ ] The metrics
   - [x] Anything that affects deployment (The broker's consumer selection logic is fundamentally changed for `KeyShared` sticky subscriptions, enforcing strict range adherence. This alters message distribution behavior for affected subscriptions.)
   
   ### Documentation
   
   - [ ] `doc`
   - [x] `doc-required` - [ ] `doc-not-needed`
   - [ ] `doc-complete`
   
   ### Matching PR in forked repository
   
   PR in forked repository: ```


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343291872


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -73,47 +74,41 @@ private synchronized Optional<ImpactedConsumersResult> internalAddConsumer(Consu
                     + conflictingConsumer);
         }
         for (IntRange intRange : consumer.getKeySharedMeta().getHashRangesList()) {
-            rangeMap.put(intRange.getStart(), consumer);
-            rangeMap.put(intRange.getEnd(), consumer);
+            rangeMap.put(intRange.getStart(), Pair.of(Range.of(intRange.getStart(), intRange.getEnd()), consumer));
         }
         return Optional.empty();
     }
 
     @Override
     public synchronized Optional<ImpactedConsumersResult> removeConsumer(Consumer consumer) {
-        rangeMap.entrySet().removeIf(entry -> entry.getValue().equals(consumer));
+        rangeMap.entrySet().removeIf(entry -> entry.getValue().getRight().equals(consumer));
         return Optional.empty();
     }
 
     @Override
     public synchronized ConsumerHashAssignmentsSnapshot getConsumerHashAssignmentsSnapshot() {
         List<HashRangeAssignment> result = new ArrayList<>();
-        Map.Entry<Integer, Consumer> prev = null;
-        for (Map.Entry<Integer, Consumer> entry: rangeMap.entrySet()) {
-            if (prev == null) {
-                prev = entry;
-            } else {
-                if (prev.getValue().equals(entry.getValue())) {
-                    result.add(new HashRangeAssignment(Range.of(prev.getKey(), entry.getKey()), entry.getValue()));
-                }
-                prev = null;
-            }
+        for (Map.Entry<Integer, Pair<Range, Consumer>> entry : rangeMap.entrySet()) {
+            Range assignedRange = entry.getValue().getLeft();
+            Consumer assignedConsumer = entry.getValue().getRight();
+            result.add(new HashRangeAssignment(assignedRange, assignedConsumer));

Review Comment:
   But we still need to implement the `getConsumerHashAssignmentsSnapshot` method, right?
   
   If we were to remove it from the interface, we would have to modify all three implementing classes. I don't think it's necessary to do that in this PR.
   
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343524838


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   hi,  @lhotari  Let's say we have two ranges.
   
   - range1:   [2, 5]
   - range2:  [5, 10]
   
   ```
       private static boolean checkRangesOverlap(IntRange range1, IntRange range2) {
        
                                     5                                            5
           return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());
       }
   ```
   
   
   5  == 5
   
   So, this method return `true`, that means two ranges is `Overlap`
   
   
   If we change to `<`
   
   ```
       private static boolean checkRangesOverlap(IntRange range1, IntRange range2) {
        
                                                                 5                                                                    5
           return Math.max(range1.getStart(), range2.getStart()) < Math.min(range1.getEnd(), range2.getEnd());
       }
   ```
   
   5 !< 5
   
   return false, This is unexpected.
   
   
   
   I added `testShouldConflictConsumerWithBoundaryRanges` to cover this case.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343529553


##########
pulsar-broker/src/test/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelectorTest.java:
##########
@@ -189,6 +221,32 @@ public void testGetConsumerKeyHashRangesWithSameConsumerName() throws Exception
         }
     }
 
+    @Test
+    public void testOneConsumerRangeConflict() throws ExecutionException, InterruptedException {

Review Comment:
   I changed name to `testShouldConflictConsumerWithSelfOverlappingRanges`
   
   This test is to ensure that a single consumer is not allowed if its own hash ranges conflict with each other. the client-side already prevents this case, but on the broker-side, having an additional layer of validation makes it safer.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343603001


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   Yes that's clear now. 
   
   What I meant is to have a unit test direct for the method. making the method static and default visibility allows testing in isolation to test all cases. For example:
   
   ```java
   @Test
       public void testCheckRangesOverlap() {
           // Overlap in the middle
           Assert.assertTrue(HashRangeExclusiveStickyKeyConsumerSelector.checkRangesOverlap(
                   new IntRange().setStart(1).setEnd(5),
                   new IntRange().setStart(3).setEnd(4)));
   
           // One contains the other
           Assert.assertTrue(HashRangeExclusiveStickyKeyConsumerSelector.checkRangesOverlap(
                   new IntRange().setStart(0).setEnd(10),
                   new IntRange().setStart(2).setEnd(8)));
   
           // Touching at boundary (shared endpoint counts as overlap)
           Assert.assertTrue(HashRangeExclusiveStickyKeyConsumerSelector.checkRangesOverlap(
                   new IntRange().setStart(1).setEnd(2),
                   new IntRange().setStart(2).setEnd(4)));
   
           // Disjoint - gap between ranges
           Assert.assertFalse(HashRangeExclusiveStickyKeyConsumerSelector.checkRangesOverlap(
                   new IntRange().setStart(1).setEnd(2),
                   new IntRange().setStart(3).setEnd(4)));
   
           // Disjoint - larger gap between ranges
           Assert.assertFalse(HashRangeExclusiveStickyKeyConsumerSelector.checkRangesOverlap(
                   new IntRange().setStart(1).setEnd(2),
                   new IntRange().setStart(4).setEnd(5)));
   
           // Disjoint - first after second
           Assert.assertFalse(HashRangeExclusiveStickyKeyConsumerSelector.checkRangesOverlap(
                   new IntRange().setStart(8).setEnd(10),
                   new IntRange().setStart(1).setEnd(7)));
   
           // Touching at upper/lower boundary in reversed order
           Assert.assertTrue(HashRangeExclusiveStickyKeyConsumerSelector.checkRangesOverlap(
                   new IntRange().setStart(8).setEnd(10),
                   new IntRange().setStart(10).setEnd(12)));
       }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2341218462


##########
pulsar-broker/src/test/java/org/apache/pulsar/client/api/KeySharedSubscriptionTest.java:
##########
@@ -2537,4 +2537,70 @@ public void testDeliveryOfRemainingMessagesWithoutDeadlock(KeySharedImplementati
             logTopicStats(topic);
         }
     }
+
+    @Test
+    public void testCustomStickyRange() throws Exception {
+        int messageCount = 100;
+        final String topicName = "persistent://public/default/test-sticky-range-" + System.nanoTime();
+        final String subscriptionName = "sub-sticky-range";
+
+        // 0. Init topic and subscription
+        admin.topics().createPartitionedTopic(topicName, 4);
+        admin.topics().createSubscription(topicName, subscriptionName, MessageId.earliest);
+
+        // 1. Create a producer and send messages
+        try (Producer<byte[]> producer = pulsarClient.newProducer()
+                .topic(topicName)
+                .enableBatching(false)
+                .messageRoutingMode(MessageRoutingMode.RoundRobinPartition)
+                .create()) {
+            for (int i = 0; i < messageCount; i++) {
+                String key = String.valueOf(i);
+                producer.newMessage()
+                        .value(String.valueOf(i).getBytes())
+                        .key(key)
+                        .send();
+            }
+        }
+
+        // 3. One by one create consumers consume message with different sticky hash range

Review Comment:
   The comment has a grammatical error. It should be 'One by one create consumers to consume messages with different sticky hash ranges' or 'Create consumers one by one to consume messages with different sticky hash ranges'.
   ```suggestion
           // 3. Create consumers one by one to consume messages with different sticky hash ranges
   ```



##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());
+    }
+
+    private boolean checkRangesOverlap(IntRange range1, IntRange range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   These two methods have identical logic and only differ in parameter types. Consider consolidating them into a single generic method or having one delegate to the other to reduce code duplication.
   ```suggestion
           return checkRangesOverlap(range1, new Range(range2.getStart(), range2.getEnd()));
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343390785


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   @shibd I think `<=` should be `<` to be consistent in the checkRangesOverlap method with the `Range.intersect` method. Please make the method static and add tests for it too so that you test the overlap rule for that method.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd merged PR #24730:
URL: https://github.com/apache/pulsar/pull/24730


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343524838


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   hi,  @lhotari  Let's say we have two ranges.
   
   - range1:   [2, 5]
   - range2:  [5, 10]
   
   ```
       private static boolean checkRangesOverlap(IntRange range1, IntRange range2) {
        
                                     5                                            5
           return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());
       }
   ```
   
   
   5  == 5
   
   So, this method return `true`, that means two ranges is `Overlap`
   
   
   If we change to `<`
   
   ```
       private static boolean checkRangesOverlap(IntRange range1, IntRange range2) {
        
                                     5                                            5
           return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());
       }
   ```
   
   5 !< 5
   
   return false, This is unexpected.
   
   
   
   I added `testShouldConflictConsumerWithBoundaryRanges` to cover this case.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343390785


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   @shibd I think `<=` should be `<` in the checkRangesOverlap method to be consistent with the `Range.intersect` method logic. Please make the method static and add tests for it too so that you test the overlap rule for that method.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343284025


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -129,10 +124,25 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
             return FutureUtil.failedFuture(new BrokerServiceException.ConsumerAssignException(
                     "Ranges for KeyShared policy must not be empty."));
         }
-        for (IntRange intRange : ranges) {
-            if (intRange.getStart() > intRange.getEnd()) {
+        List<IntRange> sortedRanges = new ArrayList<>(ranges);
+        sortedRanges.sort(Comparator.comparingInt(IntRange::getStart));
+        for (int i = 0; i < sortedRanges.size(); i++) {
+            IntRange currentRange = sortedRanges.get(i);
+            // 1. Validate: check if start > end for the current range
+            if (currentRange.getStart() > currentRange.getEnd()) {
                 return FutureUtil.failedFuture(
-                        new BrokerServiceException.ConsumerAssignException("Fixed hash range start > end"));
+                        new BrokerServiceException.ConsumerAssignException("Fixed hash range start > end for range: "
+                                + "[" + currentRange.getStart() + "," + currentRange.getEnd() + "]"));
+            }
+            // 2. Validate: check for overlaps with the next range in the sorted list
+            if (i < sortedRanges.size() - 1) {
+                IntRange nextRange = sortedRanges.get(i + 1);

Review Comment:
   > When i == 0 and sortedRanges.size == 1
   
   This case will not go this line.
   
   ```
   if (i < sortedRanges.size() - 1) {
   
   i = 0
   sortedRanges.size - 1 = 0
   ```
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2347780513


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   Thanks for all comments, I changed name to `areRangesOverlapping`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "poorbarcode (via GitHub)" <gi...@apache.org>.
poorbarcode commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343417462


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   > Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());
   
   After discussing with @shibd, we consider that this code has no bug 



##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   > Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());
   
   After discussing with @shibd, we consider that this code has no bugs 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343609527


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   The name `checkRangesOverlap` is a bit confusing. I first understood that it's working in a way where `true` means that it doesn't overlap and `false` means that it overlaps. It's the other way around.
   Renaming the method to `doesOverlap` would address that confusion.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "poorbarcode (via GitHub)" <gi...@apache.org>.
poorbarcode commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2341267490


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -129,10 +124,25 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
             return FutureUtil.failedFuture(new BrokerServiceException.ConsumerAssignException(
                     "Ranges for KeyShared policy must not be empty."));
         }
-        for (IntRange intRange : ranges) {
-            if (intRange.getStart() > intRange.getEnd()) {
+        List<IntRange> sortedRanges = new ArrayList<>(ranges);
+        sortedRanges.sort(Comparator.comparingInt(IntRange::getStart));
+        for (int i = 0; i < sortedRanges.size(); i++) {
+            IntRange currentRange = sortedRanges.get(i);
+            // 1. Validate: check if start > end for the current range
+            if (currentRange.getStart() > currentRange.getEnd()) {
                 return FutureUtil.failedFuture(
-                        new BrokerServiceException.ConsumerAssignException("Fixed hash range start > end"));
+                        new BrokerServiceException.ConsumerAssignException("Fixed hash range start > end for range: "
+                                + "[" + currentRange.getStart() + "," + currentRange.getEnd() + "]"));
+            }
+            // 2. Validate: check for overlaps with the next range in the sorted list
+            if (i < sortedRanges.size() - 1) {
+                IntRange nextRange = sortedRanges.get(i + 1);

Review Comment:
   When `i == 0 and sortedRanges.size == 1`, the line may face an `IndexOutOfBoundsException`



##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -73,47 +74,41 @@ private synchronized Optional<ImpactedConsumersResult> internalAddConsumer(Consu
                     + conflictingConsumer);
         }
         for (IntRange intRange : consumer.getKeySharedMeta().getHashRangesList()) {
-            rangeMap.put(intRange.getStart(), consumer);
-            rangeMap.put(intRange.getEnd(), consumer);
+            rangeMap.put(intRange.getStart(), Pair.of(Range.of(intRange.getStart(), intRange.getEnd()), consumer));
         }
         return Optional.empty();
     }
 
     @Override
     public synchronized Optional<ImpactedConsumersResult> removeConsumer(Consumer consumer) {
-        rangeMap.entrySet().removeIf(entry -> entry.getValue().equals(consumer));
+        rangeMap.entrySet().removeIf(entry -> entry.getValue().getRight().equals(consumer));
         return Optional.empty();
     }
 
     @Override
     public synchronized ConsumerHashAssignmentsSnapshot getConsumerHashAssignmentsSnapshot() {
         List<HashRangeAssignment> result = new ArrayList<>();
-        Map.Entry<Integer, Consumer> prev = null;
-        for (Map.Entry<Integer, Consumer> entry: rangeMap.entrySet()) {
-            if (prev == null) {
-                prev = entry;
-            } else {
-                if (prev.getValue().equals(entry.getValue())) {
-                    result.add(new HashRangeAssignment(Range.of(prev.getKey(), entry.getKey()), entry.getValue()));
-                }
-                prev = null;
-            }
+        for (Map.Entry<Integer, Pair<Range, Consumer>> entry : rangeMap.entrySet()) {
+            Range assignedRange = entry.getValue().getLeft();
+            Consumer assignedConsumer = entry.getValue().getRight();
+            result.add(new HashRangeAssignment(assignedRange, assignedConsumer));

Review Comment:
   Since the method `getConsumerKeyHashRanges()` is the unique caller of the method `getConsumerHashAssignmentsSnapshot()`, we can overwrite the method `getConsumerKeyHashRanges()` in this class, which is simpler: loop consumers and put all `consumer.getKeySharedMeta().getHashRangesList()`



##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   Can we allow the intersection of one point between two ranges like follows?
   - `range 1`: `[0-100]`
   - `range 2`: `[100-200]`
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343888929


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   Well the naming is confusing at least for code maintenance. renaming from `checkRangesOverlap` to `doRangesOverlap` would resolve this confusion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd commented on PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#issuecomment-3279346497

   /pulsarbot rerun-failure-checks


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343422724


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   `end >=  start` is the same as `start <= end`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343677610


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   This is a private static method.
   
   I don't think a separate test is necessary for this. The test coverage within the addConsumer test should be sufficient.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "shibd (via GitHub)" <gi...@apache.org>.
shibd commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343311221


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   I'm wondering if allowing this might create some ambiguity. For instance, which range would a value like 100 fall into?
   
   I tested it, and this wasn't allowed before this PR, and the client-side also prevents this. So, the behavior is consistent with how it was before. 
   
   
   ```
   java.lang.IllegalArgumentException: Ranges for KeyShared policy with overlap between [0, 9999] and [9999, 29999]
   
   	at org.apache.pulsar.client.api.KeySharedPolicy$KeySharedPolicySticky.validate(KeySharedPolicy.java:118)
   	at org.apache.pulsar.client.impl.ConsumerBuilderImpl.keySharedPolicy(ConsumerBuilderImpl.java:525)
   	at org.apache.pulsar.client.api.KeySharedSubscriptionTest.testCustomStickyRange(KeySharedSubscriptionTest.java:2579)
   ```
   
   https://github.com/apache/pulsar/blob/14129e352e4bb4e06cbf2c6c581edcdbaf9f6775/pulsar-client-api/src/main/java/org/apache/pulsar/client/api/KeySharedPolicy.java#L105-L123
   
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343468147


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   @poorbarcode @shibd  the method isn't tested. The point is that `<=` allows the boundary to overlap if I'm not mistaken. That's why I think the method should be tested and `<=` should be changed to `<`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2342198254


##########
pulsar-broker/src/test/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelectorTest.java:
##########
@@ -89,6 +89,38 @@ public void testConsumerSelect() throws ExecutionException, InterruptedException
         Assert.assertNull(selectedConsumer);
     }
 
+
+    @Test
+    public void testConsumerSelectWithMultipRanges() throws ExecutionException, InterruptedException {

Review Comment:
   Multip -> Multiple



##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {

Review Comment:
   make it static since it's not using instance fields



##########
pulsar-broker/src/test/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelectorTest.java:
##########
@@ -189,6 +221,32 @@ public void testGetConsumerKeyHashRangesWithSameConsumerName() throws Exception
         }
     }
 
+    @Test
+    public void testOneConsumerRangeConflict() throws ExecutionException, InterruptedException {

Review Comment:
   It's a bit unclear from the test name what this is testing. Perhaps testAddingConsumerWithMultipleRangeConflicts is what this does and how this differs from following tests?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2344241762


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   Although it's a private method, testing such utility methods in isolation is useful. It improves clarity for code reviews too when you can see what something is intended to do.



##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   Although it's a private method, testing such utility methods in isolation is useful. It improves clarity for code reviews too when you can see from the test code what something is intended to do.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2342192375


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   It shouldn't be allowed unless the old code behaved that way? I guess it would be useful to have a test for this and run it with the old implementation unless it's already tested.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "codecov-commenter (via GitHub)" <gi...@apache.org>.
codecov-commenter commented on PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#issuecomment-3280168993

   ## [Codecov](https://app.codecov.io/gh/apache/pulsar/pull/24730?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
   :white_check_mark: All modified and coverable lines are covered by tests.
   :white_check_mark: Project coverage is 74.20%. Comparing base ([`f1b66ae`](https://app.codecov.io/gh/apache/pulsar/commit/f1b66aed03a3752a8a5df213cfc3115512768a0c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`5ec2655`](https://app.codecov.io/gh/apache/pulsar/commit/5ec2655a9ddc85c8ef5de51d00caeeba2b040faa?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
   :warning: Report is 23 commits behind head on master.
   
   <details><summary>Additional details and impacted files</summary>
   
   
   
   [![Impacted file tree graph](https://app.codecov.io/gh/apache/pulsar/pull/24730/graphs/tree.svg?width=650&height=150&src=pr&token=acYqCpsK9J&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)](https://app.codecov.io/gh/apache/pulsar/pull/24730?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
   
   ```diff
   @@             Coverage Diff              @@
   ##             master   #24730      +/-   ##
   ============================================
   - Coverage     74.21%   74.20%   -0.02%     
   - Complexity    33463    33507      +44     
   ============================================
     Files          1895     1896       +1     
     Lines        147954   148114     +160     
     Branches      17130    17162      +32     
   ============================================
   + Hits         109805   109904      +99     
   - Misses        29387    29420      +33     
   - Partials       8762     8790      +28     
   ```
   
   | [Flag](https://app.codecov.io/gh/apache/pulsar/pull/24730/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
   |---|---|---|
   | [inttests](https://app.codecov.io/gh/apache/pulsar/pull/24730/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `26.40% <0.00%> (+0.11%)` | :arrow_up: |
   | [systests](https://app.codecov.io/gh/apache/pulsar/pull/24730/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `22.63% <0.00%> (-0.06%)` | :arrow_down: |
   | [unittests](https://app.codecov.io/gh/apache/pulsar/pull/24730/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `73.73% <100.00%> (-0.01%)` | :arrow_down: |
   
   Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#carryforward-flags-in-the-pull-request-comment) to find out more.
   
   | [Files with missing lines](https://app.codecov.io/gh/apache/pulsar/pull/24730?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
   |---|---|---|
   | [...e/HashRangeExclusiveStickyKeyConsumerSelector.java](https://app.codecov.io/gh/apache/pulsar/pull/24730?src=pr&el=tree&filepath=pulsar-broker%2Fsrc%2Fmain%2Fjava%2Forg%2Fapache%2Fpulsar%2Fbroker%2Fservice%2FHashRangeExclusiveStickyKeyConsumerSelector.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-cHVsc2FyLWJyb2tlci9zcmMvbWFpbi9qYXZhL29yZy9hcGFjaGUvcHVsc2FyL2Jyb2tlci9zZXJ2aWNlL0hhc2hSYW5nZUV4Y2x1c2l2ZVN0aWNreUtleUNvbnN1bWVyU2VsZWN0b3IuamF2YQ==) | `100.00% <100.00%> (+5.40%)` | :arrow_up: |
   
   ... and [134 files with indirect coverage changes](https://app.codecov.io/gh/apache/pulsar/pull/24730/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
   </details>
   <details><summary> :rocket: New features to boost your workflow: </summary>
   
   - :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
   - :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
   </details>


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "lhotari (via GitHub)" <gi...@apache.org>.
lhotari commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343888929


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   Well the naming is confusing at least for code maintenance. renaming from `checkRangesOverlap` to `doesOverlap` would resolve this confusion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "poorbarcode (via GitHub)" <gi...@apache.org>.
poorbarcode commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2343411712


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -73,47 +74,41 @@ private synchronized Optional<ImpactedConsumersResult> internalAddConsumer(Consu
                     + conflictingConsumer);
         }
         for (IntRange intRange : consumer.getKeySharedMeta().getHashRangesList()) {
-            rangeMap.put(intRange.getStart(), consumer);
-            rangeMap.put(intRange.getEnd(), consumer);
+            rangeMap.put(intRange.getStart(), Pair.of(Range.of(intRange.getStart(), intRange.getEnd()), consumer));
         }
         return Optional.empty();
     }
 
     @Override
     public synchronized Optional<ImpactedConsumersResult> removeConsumer(Consumer consumer) {
-        rangeMap.entrySet().removeIf(entry -> entry.getValue().equals(consumer));
+        rangeMap.entrySet().removeIf(entry -> entry.getValue().getRight().equals(consumer));
         return Optional.empty();
     }
 
     @Override
     public synchronized ConsumerHashAssignmentsSnapshot getConsumerHashAssignmentsSnapshot() {
         List<HashRangeAssignment> result = new ArrayList<>();
-        Map.Entry<Integer, Consumer> prev = null;
-        for (Map.Entry<Integer, Consumer> entry: rangeMap.entrySet()) {
-            if (prev == null) {
-                prev = entry;
-            } else {
-                if (prev.getValue().equals(entry.getValue())) {
-                    result.add(new HashRangeAssignment(Range.of(prev.getKey(), entry.getKey()), entry.getValue()));
-                }
-                prev = null;
-            }
+        for (Map.Entry<Integer, Pair<Range, Consumer>> entry : rangeMap.entrySet()) {
+            Range assignedRange = entry.getValue().getLeft();
+            Consumer assignedConsumer = entry.getValue().getRight();
+            result.add(new HashRangeAssignment(assignedRange, assignedConsumer));

Review Comment:
   I am thinking that maybe we can cache the `hash range assignments` after adding or removing consumers, which may help for GC(to avoid re-creating objects typed `HashRangeAssignment `)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [fix][broker] Ensure KeyShared sticky mode consumer respects assigned ranges [pulsar]

Posted by "BewareMyPower (via GitHub)" <gi...@apache.org>.
BewareMyPower commented on code in PR #24730:
URL: https://github.com/apache/pulsar/pull/24730#discussion_r2347777876


##########
pulsar-broker/src/main/java/org/apache/pulsar/broker/service/HashRangeExclusiveStickyKeyConsumerSelector.java:
##########
@@ -143,34 +153,38 @@ private synchronized CompletableFuture<Void> validateKeySharedMeta(Consumer cons
         }
     }
 
-    private synchronized Consumer findConflictingConsumer(List<IntRange> ranges) {
-        for (IntRange intRange : ranges) {
-            Map.Entry<Integer, Consumer> ceilingEntry = rangeMap.ceilingEntry(intRange.getStart());
-            Map.Entry<Integer, Consumer> floorEntry = rangeMap.floorEntry(intRange.getEnd());
-
-            if (floorEntry != null && floorEntry.getKey() >= intRange.getStart()) {
-                return floorEntry.getValue();
-            }
-
-            if (ceilingEntry != null && ceilingEntry.getKey() <= intRange.getEnd()) {
-                return ceilingEntry.getValue();
+    private synchronized Consumer findConflictingConsumer(List<IntRange> newConsumerRanges) {
+        for (IntRange newRange : newConsumerRanges) {
+            // 1. Check for potential conflicts with existing ranges that start before newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictBeforeStart = rangeMap.floorEntry(newRange.getStart());
+            if (conflictBeforeStart != null) {
+                Range existingRange = conflictBeforeStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictBeforeStart.getValue().getRight();
+                }
             }
-
-            if (ceilingEntry != null && floorEntry != null && ceilingEntry.getValue().equals(floorEntry.getValue())) {
-                KeySharedMeta keySharedMeta = ceilingEntry.getValue().getKeySharedMeta();
-                for (IntRange range : keySharedMeta.getHashRangesList()) {
-                    int start = Math.max(intRange.getStart(), range.getStart());
-                    int end = Math.min(intRange.getEnd(), range.getEnd());
-                    if (end >= start) {
-                        return ceilingEntry.getValue();
-                    }
+            // 2. Check for potential conflicts with existing ranges that start after newRange's start.
+            Map.Entry<Integer, Pair<Range, Consumer>> conflictAfterStart = rangeMap.ceilingEntry(newRange.getStart());
+            if (conflictAfterStart != null) {
+                Range existingRange = conflictAfterStart.getValue().getLeft();
+                if (checkRangesOverlap(newRange, existingRange)) {
+                    return conflictAfterStart.getValue().getRight();
                 }
             }
         }
         return null;
     }
 
-    Map<Integer, Consumer> getRangeConsumer() {
+
+    private boolean checkRangesOverlap(IntRange range1, Range range2) {
+        return Math.max(range1.getStart(), range2.getStart()) <= Math.min(range1.getEnd(), range2.getEnd());

Review Comment:
   I think `areRangesOverlapped` or `rangesOverlapped` might be a better name.  `doRangesOverlap` is still confusing to me.
   
   The `checkXxx` names are also widely used in the code base to return a boolean, e.g. `Consumer#checkAndApplyTopicMigration`.
   
   <img width="670" height="371" alt="image" src="https://github.com/user-attachments/assets/a83684a8-84a8-4a90-8376-155525f22ad4" />



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pulsar.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org