-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
making fixes to grouped message src, so that nextMessages always retu… #145
Changes from all commits
b384a88
c0a5cfe
4438485
1dbc30c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,35 +7,54 @@ | |
import com.flipkart.varadhi.spi.services.PolledMessages; | ||
import lombok.AllArgsConstructor; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.apache.commons.lang3.mutable.MutableBoolean; | ||
|
||
import java.util.*; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.concurrent.ConcurrentLinkedDeque; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
|
||
/** | ||
* Message source that maintains ordering among messages of the same groupId. | ||
*/ | ||
@RequiredArgsConstructor | ||
@Slf4j | ||
public class GroupedMessageSrc<O extends Offset> implements MessageSrc { | ||
|
||
private final ConcurrentHashMap<String, GroupTracker> allGroupedMessages = new ConcurrentHashMap<>(); | ||
|
||
private final ConcurrentLinkedDeque<String> freeGroups = new ConcurrentLinkedDeque<>(); | ||
|
||
private final Consumer<O> consumer; | ||
|
||
/** | ||
* Used to limit the message buffering. Will be driven via consumer configuration. | ||
*/ | ||
private final long maxUnAckedMessages; | ||
|
||
/** | ||
* Maintains the count of total messages read from the consumer so far. | ||
* Required for watermark checks, for when this value runs low we can fetch more messages from the consumer. | ||
* Counter gets decremented when the message is committed/consumed. | ||
*/ | ||
private final AtomicLong totalInFlightMessages = new AtomicLong(0); | ||
private final AtomicLong totalUnAckedMessages = new AtomicLong(0); | ||
|
||
// Used for watermark checks against the totalInFlightMessages. Will be driven via consumer configuration. | ||
private final long maxInFlightMessages = 100; // todo(aayush): make configurable | ||
// Internal states to manage async state | ||
|
||
private final Consumer<O> consumer; | ||
/** | ||
* flag to indicate whether a task to fetch messages from consumer is ongoing. | ||
*/ | ||
private final AtomicBoolean pendingAsyncFetch = new AtomicBoolean(false); | ||
|
||
/** | ||
* holder to keep the incomplete future object while waiting for new messages or groups to get freed up. | ||
*/ | ||
private final AtomicReference<NextMsgsRequest> pendingRequest = new AtomicReference<>(); | ||
|
||
/** | ||
* Attempt to fill the message array with one message from each group. | ||
|
@@ -47,22 +66,65 @@ public class GroupedMessageSrc<O extends Offset> implements MessageSrc { | |
*/ | ||
@Override | ||
public CompletableFuture<Integer> nextMessages(MessageTracker[] messages) { | ||
if (!hasMaxInFlightMessages()) { | ||
return replenishAvailableGroups().thenApply(v -> nextMessagesInternal(messages)); | ||
int count = nextMessagesInternal(messages); | ||
if (count > 0) { | ||
return CompletableFuture.completedFuture(count); | ||
} | ||
|
||
NextMsgsRequest request = new NextMsgsRequest(new CompletableFuture<>(), messages); | ||
if (!pendingRequest.compareAndSet(null, request)) { | ||
throw new IllegalStateException( | ||
"nextMessages method is not supposed to be called concurrently. There seems to be a pending nextMessage call"); | ||
} | ||
|
||
// incomplete result is saved. trigger new message fetch. | ||
optionallyFetchNewMessages(); | ||
|
||
// double check, if any free group is available now. | ||
if (isFreeGroupPresent()) { | ||
tryCompletePendingRequest(); | ||
} | ||
|
||
return request.result; | ||
} | ||
|
||
private void tryCompletePendingRequest() { | ||
NextMsgsRequest request; | ||
if ((request = pendingRequest.getAndSet(null)) != null) { | ||
request.result.complete(nextMessagesInternal(request.messages)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is still a chance, where we still end up completing the request with 0 messages, but this should be much rarer now. |
||
} | ||
} | ||
|
||
private void optionallyFetchNewMessages() { | ||
if (!isMaxUnAckedMessagesBreached() && pendingAsyncFetch.compareAndSet(false, true)) { | ||
// there is more room for new messages. We can initiate a new fetch request, as none is ongoing. | ||
consumer.receiveAsync().whenComplete((polledMessages, ex) -> { | ||
if (ex != null) { | ||
replenishAvailableGroups(polledMessages); | ||
pendingAsyncFetch.set(false); | ||
} else { | ||
log.error("Error while fetching messages from consumer", ex); | ||
throw new IllegalStateException( | ||
"should be unreachable. consumer.receiveAsync() should not throw exception."); | ||
} | ||
}); | ||
} | ||
return CompletableFuture.completedFuture(nextMessagesInternal(messages)); | ||
} | ||
|
||
private int nextMessagesInternal(MessageTracker[] messages) { | ||
int i = 0; | ||
GroupTracker groupTracker; | ||
while (i < messages.length && (groupTracker = getGroupTracker()) != null) { | ||
while (i < messages.length && (groupTracker = pollFreeGroup()) != null) { | ||
messages[i++] = new GroupedMessageTracker(groupTracker.messages.getFirst().nextMessage()); | ||
} | ||
return i; | ||
} | ||
|
||
private GroupTracker getGroupTracker() { | ||
boolean isFreeGroupPresent() { | ||
return !freeGroups.isEmpty(); | ||
} | ||
|
||
private GroupTracker pollFreeGroup() { | ||
String freeGroup = freeGroups.poll(); | ||
if (freeGroup == null) { | ||
return null; | ||
|
@@ -77,13 +139,6 @@ private GroupTracker getGroupTracker() { | |
return tracker; | ||
} | ||
|
||
private CompletableFuture<Void> replenishAvailableGroups() { | ||
return consumer.receiveAsync().thenApply(polledMessages -> { | ||
replenishAvailableGroups(polledMessages); | ||
return null; | ||
}); | ||
} | ||
|
||
private void replenishAvailableGroups(PolledMessages<O> polledMessages) { | ||
Map<String, List<MessageTracker>> groupedMessages = groupMessagesByGroupId(polledMessages); | ||
for (Map.Entry<String, List<MessageTracker>> group : groupedMessages.entrySet()) { | ||
|
@@ -97,11 +152,12 @@ private void replenishAvailableGroups(PolledMessages<O> polledMessages) { | |
tracker.messages.add(newBatch); | ||
return tracker; | ||
}); | ||
totalInFlightMessages.addAndGet(newBatch.count()); | ||
totalUnAckedMessages.addAndGet(newBatch.count()); | ||
if (isNewGroup.isTrue()) { | ||
freeGroups.add(group.getKey()); | ||
} | ||
} | ||
tryCompletePendingRequest(); | ||
} | ||
|
||
private Map<String, List<MessageTracker>> groupMessagesByGroupId(PolledMessages<O> polledMessages) { | ||
|
@@ -117,8 +173,8 @@ private Map<String, List<MessageTracker>> groupMessagesByGroupId(PolledMessages< | |
return groups; | ||
} | ||
|
||
private boolean hasMaxInFlightMessages() { | ||
return totalInFlightMessages.get() >= maxInFlightMessages; | ||
boolean isMaxUnAckedMessagesBreached() { | ||
return totalUnAckedMessages.get() >= maxUnAckedMessages; | ||
} | ||
|
||
enum GroupStatus { | ||
|
@@ -158,7 +214,7 @@ private void free(String groupId, MessageConsumptionStatus status) { | |
throw new IllegalStateException(String.format("Tried to free group %s: %s", gId, tracker)); | ||
} | ||
var messages = tracker.messages; | ||
if (!messages.isEmpty() && messages.getFirst().remaining() == 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using |
||
while (!messages.isEmpty() && messages.getFirst().remaining() == 0) { | ||
messages.removeFirst(); | ||
} | ||
if (!messages.isEmpty()) { | ||
|
@@ -169,10 +225,14 @@ private void free(String groupId, MessageConsumptionStatus status) { | |
return null; | ||
} | ||
}); | ||
totalInFlightMessages.decrementAndGet(); | ||
totalUnAckedMessages.decrementAndGet(); | ||
if (isRemaining.isTrue()) { | ||
freeGroups.addFirst(groupId); | ||
tryCompletePendingRequest(); | ||
} | ||
} | ||
} | ||
|
||
record NextMsgsRequest(CompletableFuture<Integer> result, MessageTracker[] messages) { | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prioritizing the array filling.
If no message present / no free group present. try to fetch fresh messages.
And this is the difference, There are 2 conditions, which can unblock the "array filling". the fetch returning with new messages.
and the group getting freed up.