diff --git a/.idea/runConfigurations/generate_test_sources__e.xml b/.idea/runConfigurations/generate_test_sources__e.xml
new file mode 100644
index 000000000..2456e1d82
--- /dev/null
+++ b/.idea/runConfigurations/generate_test_sources__e.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/parallel-consumer-core/src/main/java/io/confluent/csid/utils/MathUtils.java b/parallel-consumer-core/src/main/java/io/confluent/csid/utils/MathUtils.java
deleted file mode 100644
index 3bcfd0012..000000000
--- a/parallel-consumer-core/src/main/java/io/confluent/csid/utils/MathUtils.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package io.confluent.csid.utils;
-
-/*-
- * Copyright (C) 2020-2022 Confluent, Inc.
- */
-
-public class MathUtils {
-
- /**
- * @deprecated old, was used with AssertJ - not needed with Truth (has fuzzy matching built in)
- */
- @Deprecated
- public static boolean isLessWithin(final int needle, final int target, final int percent) {
- int i = target * (1 - percent / 100);
-
- int diff = target - needle;
- int norm = Math.abs(diff);
- int off = norm / target * 100;
- return off < percent;
- }
-
-}
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/RetriableException.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/RetriableException.java
new file mode 100644
index 000000000..5e6ed1732
--- /dev/null
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/RetriableException.java
@@ -0,0 +1,29 @@
+package io.confluent.parallelconsumer;
+
+/*-
+ * Copyright (C) 2020-2022 Confluent, Inc.
+ */
+
+/**
+ * A user's processing function can throw this exception, which signals to PC that processing of the message has failed,
+ * and that it should be retired at a later time.
+ *
+ * The advantage of throwing this exception explicitly, is that PC will not log an ERROR. If any other type of exception
+ * is thrown by the user's function, that will be logged as an error (but will still be retried later).
+ *
+ * So in short, if this exception is thrown, nothing will be logged (except at DEBUG level), any other exception will be
+ * logged as an error.
+ */
+public class RetriableException extends RuntimeException {
+ public RetriableException(String message) {
+ super(message);
+ }
+
+ public RetriableException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public RetriableException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/AbstractParallelEoSStreamProcessor.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/AbstractParallelEoSStreamProcessor.java
index ff9747d6d..71a6d472a 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/AbstractParallelEoSStreamProcessor.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/AbstractParallelEoSStreamProcessor.java
@@ -8,12 +8,10 @@
import io.confluent.parallelconsumer.ParallelConsumer;
import io.confluent.parallelconsumer.ParallelConsumerOptions;
import io.confluent.parallelconsumer.PollContextInternal;
+import io.confluent.parallelconsumer.RetriableException;
import io.confluent.parallelconsumer.state.WorkContainer;
import io.confluent.parallelconsumer.state.WorkManager;
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
-import lombok.SneakyThrows;
+import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
import org.apache.kafka.clients.consumer.ConsumerRecord;
@@ -46,6 +44,7 @@
import static java.time.Duration.ofMillis;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import static lombok.AccessLevel.PRIVATE;
import static lombok.AccessLevel.PROTECTED;
/**
@@ -55,6 +54,7 @@
public abstract class AbstractParallelEoSStreamProcessor implements ParallelConsumer, ConsumerRebalanceListener, Closeable {
public static final String MDC_INSTANCE_ID = "pcId";
+ public static final String MDC_OFFSET_MARKER = "offset";
@Getter(PROTECTED)
protected final ParallelConsumerOptions options;
@@ -99,7 +99,35 @@ public abstract class AbstractParallelEoSStreamProcessor implements Parall
* Collection of work waiting to be
*/
@Getter(PROTECTED)
- private final BlockingQueue> workMailBox = new LinkedBlockingQueue<>(); // Thread safe, highly performant, non blocking
+ private final BlockingQueue> workMailBox = new LinkedBlockingQueue<>(); // Thread safe, highly performant, non blocking
+
+ /**
+ * An inbound message to the controller.
+ *
+ * Currently, an Either type class, representing either newly polled records to ingest, or a work result.
+ */
+ @Value
+ @RequiredArgsConstructor(access = PRIVATE)
+ private static class ControllerEventMessage {
+ WorkContainer workContainer;
+ EpochAndRecordsMap consumerRecords;
+
+ private boolean isWorkResult() {
+ return workContainer != null;
+ }
+
+ private boolean isNewConsumerRecords() {
+ return !isWorkResult();
+ }
+
+ private static ControllerEventMessage of(EpochAndRecordsMap polledRecords) {
+ return new ControllerEventMessage<>(null, polledRecords);
+ }
+
+ public static ControllerEventMessage of(WorkContainer work) {
+ return new ControllerEventMessage(work, null);
+ }
+ }
private final BrokerPollSystem brokerPollSubsystem;
@@ -496,7 +524,13 @@ private boolean isResponsibleForCommits() {
/**
* Block the calling thread until no more messages are being processed.
+ *
+ * Used for testing.
+ *
+ * @deprecated no longer used, will be removed in next version
*/
+ // TODO delete
+ @Deprecated
@SneakyThrows
public void waitForProcessedNotCommitted(Duration timeout) {
log.debug("Waiting processed but not committed...");
@@ -621,9 +655,8 @@ private void controlLoop(Function, List> userFu
if (state == running) {
if (!wm.isSufficientlyLoaded() & brokerPollSubsystem.isPaused()) {
- // can occur
log.debug("Found Poller paused with not enough front loaded messages, ensuring poller is awake (mail: {} vs target: {})",
- wm.getAmountOfWorkQueuedWaitingIngestion(),
+ wm.getNumberOfWorkQueuedInShardsAwaitingSelection(),
options.getTargetAmountOfRecordsInFlight());
brokerPollSubsystem.wakeupIfPaused();
}
@@ -665,7 +698,7 @@ private void controlLoop(Function, List> userFu
// end of loop
log.trace("End of control loop, waiting processing {}, remaining in partition queues: {}, out for processing: {}. In state: {}",
- wm.getTotalWorkAwaitingIngestion(), wm.getNumberOfEntriesInPartitionQueues(), wm.getNumberRecordsOutForProcessing(), state);
+ wm.getNumberOfWorkQueuedInShardsAwaitingSelection(), wm.getNumberOfEntriesInPartitionQueues(), wm.getNumberRecordsOutForProcessing(), state);
}
private int handleWork(final Function, List> userFunction, final Consumer callback) {
@@ -812,20 +845,16 @@ protected int getQueueTargetLoaded() {
* Checks the system has enough pressure in the pipeline of work, if not attempts to step up the load factor.
*/
protected void checkPipelinePressure() {
- boolean moreWorkInQueuesAvailableThatHaveNotBeenPulled = wm.getAmountOfWorkQueuedWaitingIngestion() > options.getTargetAmountOfRecordsInFlight();
if (log.isTraceEnabled())
log.trace("Queue pressure check: (current size: {}, loaded target: {}, factor: {}) " +
- "if (isPoolQueueLow() {} && moreWorkInQueuesAvailableThatHaveNotBeenPulled {} && lastWorkRequestWasFulfilled {}))",
+ "if (isPoolQueueLow() {} && lastWorkRequestWasFulfilled {}))",
getNumberOfUserFunctionsQueued(),
getQueueTargetLoaded(),
dynamicExtraLoadFactor.getCurrentFactor(),
isPoolQueueLow(),
- moreWorkInQueuesAvailableThatHaveNotBeenPulled,
lastWorkRequestWasFulfilled);
- if (isPoolQueueLow()
- && moreWorkInQueuesAvailableThatHaveNotBeenPulled
- && lastWorkRequestWasFulfilled) {
+ if (isPoolQueueLow() && lastWorkRequestWasFulfilled) {
boolean steppedUp = dynamicExtraLoadFactor.maybeStepUp();
if (steppedUp) {
log.debug("isPoolQueueLow(): Executor pool queue is not loaded with enough work (queue: {} vs target: {}), stepped up loading factor to {}",
@@ -847,10 +876,9 @@ private boolean isPoolQueueLow() {
int queueSize = getNumberOfUserFunctionsQueued();
int queueTarget = getPoolLoadTarget();
boolean workAmountBelowTarget = queueSize <= queueTarget;
- boolean hasWorkInMailboxes = wm.hasWorkAwaitingIngestionToShards();
- log.debug("isPoolQueueLow()? workAmountBelowTarget {} {} vs {} && wm.hasWorkInMailboxes() {};",
- workAmountBelowTarget, queueSize, queueTarget, hasWorkInMailboxes);
- return workAmountBelowTarget && hasWorkInMailboxes;
+ log.debug("isPoolQueueLow()? workAmountBelowTarget {} {} vs {};",
+ workAmountBelowTarget, queueSize, queueTarget);
+ return workAmountBelowTarget;
}
private void drain() {
@@ -880,7 +908,7 @@ private void transitionToClosing() {
*/
private void processWorkCompleteMailBox() {
log.trace("Processing mailbox (might block waiting for results)...");
- Set> results = new HashSet<>();
+ Queue> results = new ArrayDeque<>();
final Duration timeToBlockFor = getTimeToBlockFor();
@@ -893,7 +921,7 @@ private void processWorkCompleteMailBox() {
// wait for work, with a timeToBlockFor for sanity
log.trace("Blocking poll {}", timeToBlockFor);
try {
- WorkContainer firstBlockingPoll = workMailBox.poll(timeToBlockFor.toMillis(), MILLISECONDS);
+ var firstBlockingPoll = workMailBox.poll(timeToBlockFor.toMillis(), MILLISECONDS);
if (firstBlockingPoll == null) {
log.debug("Mailbox results returned null, indicating timeToBlockFor (which was set as {})", timeToBlockFor);
} else {
@@ -915,10 +943,15 @@ private void processWorkCompleteMailBox() {
workMailBox.drainTo(results, size);
log.trace("Processing drained work {}...", results.size());
- for (var work : results) {
- MDC.put("offset", work.toString());
- wm.handleFutureResult(work);
- MDC.clear();
+ for (var action : results) {
+ if (action.isNewConsumerRecords()) {
+ wm.registerWork(action.getConsumerRecords());
+ } else {
+ WorkContainer work = action.getWorkContainer();
+ MDC.put(MDC_OFFSET_MARKER, work.toString());
+ wm.handleFutureResult(work);
+ MDC.remove(MDC_OFFSET_MARKER);
+ }
}
}
@@ -926,19 +959,11 @@ private void processWorkCompleteMailBox() {
* The amount of time to block poll in this cycle
*
* @return either the duration until next commit, or next work retry
- * @see WorkManager#isStarvedForNewWork()
- * @see WorkManager#getTotalWorkAwaitingIngestion()
* @see ParallelConsumerOptions#getTargetAmountOfRecordsInFlight()
*/
private Duration getTimeToBlockFor() {
- // should not block as not enough work is being done, and there's more work to ingest
- boolean ingestionWorkAndStarved = wm.hasWorkAwaitingIngestionToShards() && wm.isStarvedForNewWork();
- if (ingestionWorkAndStarved) {
- log.debug("Work waiting to be ingested, and not enough work in flight - will not block");
- return Duration.ofMillis(0);
- }
// if less than target work already in flight, don't sleep longer than the next retry time for failed work, if it exists - so that we can wake up and maybe retry the failed work
- else if (!wm.isWorkInFlightMeetingTarget()) {
+ if (!wm.isWorkInFlightMeetingTarget()) {
// though check if we have work awaiting retry
var lowestScheduledOpt = wm.getLowestRetryTime();
if (lowestScheduledOpt.isPresent()) {
@@ -1015,7 +1040,7 @@ private boolean lingeringOnCommitWouldBeBeneficial() {
workIsWaitingToBeCompletedSuccessfully, workInFlight, workWaitingInMailbox, !workWaitingToCommit);
boolean result = workIsWaitingToBeCompletedSuccessfully || workInFlight || workWaitingInMailbox || !workWaitingToCommit;
- // disable - commit frequency takes care of lingering? is this outdated?
+ // todo disable - commit frequency takes care of lingering? is this outdated?
return false;
}
@@ -1097,7 +1122,12 @@ protected List, R>> runUserFunct
return intermediateResults;
} catch (Exception e) {
// handle fail
- log.error("Exception caught in user function running stage, registering WC as failed, returning to mailbox", e);
+ String msg = "Exception caught in user function running stage, registering WC as failed, returning to mailbox";
+ if (e instanceof RetriableException) {
+ log.debug("Explicit " + RetriableException.class.getSimpleName() + " caught, logging at DEBUG only. " + msg, e);
+ } else {
+ log.error(msg, e);
+ }
for (var wc : workContainerBatch) {
wc.onUserFunctionFailure(e);
addToMailbox(wc); // always add on error
@@ -1117,8 +1147,13 @@ protected void onUserFunctionSuccess(WorkContainer wc, List> resultsFrom
protected void addToMailbox(WorkContainer wc) {
String state = wc.isUserFunctionSucceeded() ? "succeeded" : "FAILED";
- log.debug("Adding {} {} to mailbox...", state, wc);
- workMailBox.add(wc);
+ log.trace("Adding {} {} to mailbox...", state, wc);
+ workMailBox.add(ControllerEventMessage.of(wc));
+ }
+
+ public void registerWork(EpochAndRecordsMap polledRecords) {
+ log.debug("Adding {} to mailbox...", polledRecords);
+ workMailBox.add(ControllerEventMessage.of(polledRecords));
}
/**
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/BrokerPollSystem.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/BrokerPollSystem.java
index ea7d3826c..3fc470eb7 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/BrokerPollSystem.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/BrokerPollSystem.java
@@ -59,11 +59,12 @@ public class BrokerPollSystem implements OffsetCommitter {
private final WorkManager wm;
- public BrokerPollSystem(ConsumerManager consumerMgr, WorkManager wm, AbstractParallelEoSStreamProcessor pc, final ParallelConsumerOptions options) {
+ public BrokerPollSystem(ConsumerManager consumerMgr, WorkManager wm, AbstractParallelEoSStreamProcessor pc, final ParallelConsumerOptions options) {
this.wm = wm;
this.pc = pc;
this.consumerManager = consumerMgr;
+
switch (options.getCommitMode()) {
case PERIODIC_CONSUMER_SYNC, PERIODIC_CONSUMER_ASYNCHRONOUS -> {
ConsumerOffsetCommitter consumerCommitter = new ConsumerOffsetCommitter<>(consumerMgr, wm, options);
@@ -107,22 +108,7 @@ private boolean controlLoop() {
committer.ifPresent(x -> x.claim());
try {
while (state != closed) {
- log.trace("Loop: Broker poller: ({})", state);
- if (state == running || state == draining) { // if draining - subs will be paused, so use this to just sleep
- ConsumerRecords polledRecords = pollBrokerForRecords();
- log.debug("Got {} records in poll result", polledRecords.count());
-
- if (!polledRecords.isEmpty()) {
- log.trace("Loop: Register work");
- wm.registerWork(polledRecords);
-
- // notify control work has been registered, in case it's sleeping waiting for work that will never come
- if (wm.isStarvedForNewWork()) {
- log.trace("Apparently no work is being done, make sure Control is awake to receive messages");
- pc.notifySomethingToDo();
- }
- }
- }
+ handlePoll();
maybeDoCommit();
@@ -143,6 +129,20 @@ private boolean controlLoop() {
}
}
+ private void handlePoll() {
+ log.trace("Loop: Broker poller: ({})", state);
+ if (state == running || state == draining) { // if draining - subs will be paused, so use this to just sleep
+ var polledRecords = pollBrokerForRecords();
+ int count = polledRecords.count();
+ log.debug("Got {} records in poll result", count);
+
+ if (count > 0) {
+ log.trace("Loop: Register work");
+ pc.registerWork(polledRecords);
+ }
+ }
+ }
+
private void doClose() {
log.debug("Doing close...");
doPause();
@@ -166,7 +166,7 @@ private boolean isResponsibleForCommits() {
return committer.isPresent();
}
- private ConsumerRecords pollBrokerForRecords() {
+ private EpochAndRecordsMap pollBrokerForRecords() {
managePauseOfSubscription();
log.debug("Subscriptions are paused: {}", paused);
@@ -176,8 +176,11 @@ private ConsumerRecords pollBrokerForRecords() {
log.debug("Long polling broker with timeout {}, might appear to sleep here if subs are paused, or no data available on broker. Run state: {}", thisLongPollTimeout, state);
ConsumerRecords poll = consumerManager.poll(thisLongPollTimeout);
+
log.debug("Poll completed");
- return poll;
+
+ // build records map
+ return new EpochAndRecordsMap<>(poll, wm.getPm());
}
/**
@@ -205,10 +208,9 @@ private void doPauseMaybe() {
});
} else {
if (log.isDebugEnabled()) {
- log.debug("Should pause but pause rate limit exceeded {} vs {}. Queued: {}",
+ log.debug("Should pause but pause rate limit exceeded {} vs {}.",
pauseLimiter.getElapsedDuration(),
- pauseLimiter.getRate(),
- wm.getAmountOfWorkQueuedWaitingIngestion());
+ pauseLimiter.getRate());
}
}
}
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/CountingCRLinkedList.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/CountingCRLinkedList.java
deleted file mode 100644
index 28cbcb201..000000000
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/CountingCRLinkedList.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package io.confluent.parallelconsumer.internal;
-
-/*-
- * Copyright (C) 2020-2021 Confluent, Inc.
- */
-
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import org.apache.kafka.clients.consumer.ConsumerRecord;
-import org.apache.kafka.clients.consumer.ConsumerRecords;
-
-import java.util.LinkedList;
-import java.util.Queue;
-
-/**
- * Cached view of the nested number of records in this queue.
- *
- * Also protects against concurrent modification exceptions, as we don't need to traverse the list to count the nested
- * elements. The count won't then be always exact, but it doesn't need to be.
- */
-@EqualsAndHashCode(callSuper = true)
-public class CountingCRLinkedList extends LinkedList> implements Queue> {
-
- /**
- * The number of nested {@link ConsumerRecord} in this collection. As this is a non blocking collection, this won't
- * be exact.
- */
- @Getter
- private int nestedCount = 0;
-
- @Override
- public void add(final int index, final ConsumerRecords element) {
- nestedCount = nestedCount + element.count();
- super.add(index, element);
- }
-
- @Override
- public boolean add(final ConsumerRecords element) {
- nestedCount = nestedCount + element.count();
- return super.add(element);
- }
-
- @Override
- public ConsumerRecords poll() {
- ConsumerRecords poll = super.poll();
- if (poll != null) {
- int numberOfNestedMessages = poll.count();
- nestedCount = nestedCount - numberOfNestedMessages;
- }
- return poll;
- }
-
-}
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/EpochAndRecordsMap.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/EpochAndRecordsMap.java
new file mode 100644
index 000000000..ffd7c0874
--- /dev/null
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/internal/EpochAndRecordsMap.java
@@ -0,0 +1,67 @@
+package io.confluent.parallelconsumer.internal;
+
+/*-
+ * Copyright (C) 2020-2022 Confluent, Inc.
+ */
+
+import io.confluent.parallelconsumer.state.PartitionStateManager;
+import lombok.Value;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.clients.consumer.ConsumerRecords;
+import org.apache.kafka.common.TopicPartition;
+
+import java.util.*;
+
+/**
+ * For tagging polled records with our epoch
+ *
+ * @see BrokerPollSystem#partitionAssignmentEpoch
+ */
+@Value
+public class EpochAndRecordsMap {
+
+ Map recordMap = new HashMap<>();
+
+ public EpochAndRecordsMap(ConsumerRecords poll, PartitionStateManager pm) {
+ poll.partitions().forEach(partition -> {
+ var records = poll.records(partition);
+ Long epochOfPartition = pm.getEpochOfPartition(partition);
+ recordMap.put(partition, new RecordsAndEpoch(epochOfPartition, records));
+ });
+ }
+
+ /**
+ * Get the partitions which have records contained in this record set.
+ *
+ * @return the set of partitions with data in this record set (may be empty if no data was returned)
+ */
+ public Set partitions() {
+ return Collections.unmodifiableSet(recordMap.keySet());
+ }
+
+ /**
+ * Get just the records for the given partition
+ *
+ * @param partition The partition to get records for
+ */
+ public RecordsAndEpoch records(TopicPartition partition) {
+ return this.recordMap.get(partition);
+ }
+
+ /**
+ * The number of records for all topics
+ */
+ public int count() {
+ int count = 0;
+ for (var recs : this.recordMap.values())
+ count += recs.getRecords().size();
+ return count;
+ }
+
+ @Value
+ public class RecordsAndEpoch {
+ Long epochOfPartitionAtPoll;
+ List> records;
+ }
+
+}
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/BitSetEncoder.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/BitSetEncoder.java
index 1ecdc556e..04dcb05dc 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/BitSetEncoder.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/BitSetEncoder.java
@@ -1,7 +1,7 @@
package io.confluent.parallelconsumer.offsets;
/*-
- * Copyright (C) 2020-2021 Confluent, Inc.
+ * Copyright (C) 2020-2022 Confluent, Inc.
*/
import io.confluent.csid.utils.StringUtils;
@@ -108,7 +108,7 @@ private ByteBuffer initV2(int bitsetEntriesRequired) throws BitSetEncodingNotSup
private ByteBuffer initV1(int bitsetEntriesRequired) throws BitSetEncodingNotSupportedException {
if (bitsetEntriesRequired > Short.MAX_VALUE) {
// need to upgrade to using Integer for the bitset length, but can't change serialisation format in-place
- throw new BitSetEncodingNotSupportedException("BitSet V1 too long to encode, bitset length overflows Short.MAX_VALUE: " + bitsetEntriesRequired + ". (max: " + Short.MAX_VALUE + ")");
+ throw new BitSetEncodingNotSupportedException("Input too long to encode for BitSet V1, length overflows Short.MAX_VALUE: " + bitsetEntriesRequired + ". (max: " + Short.MAX_VALUE + ")");
}
// prep bit set buffer
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/EncodedOffsetPair.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/EncodedOffsetPair.java
index 7a9697969..65dd6318e 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/EncodedOffsetPair.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/EncodedOffsetPair.java
@@ -11,6 +11,7 @@
import lombok.extern.slf4j.Slf4j;
import java.nio.ByteBuffer;
+import java.util.Comparator;
import static io.confluent.parallelconsumer.offsets.OffsetBitSet.deserialiseBitSetWrap;
import static io.confluent.parallelconsumer.offsets.OffsetBitSet.deserialiseBitSetWrapToIncompletes;
@@ -27,6 +28,7 @@
@Slf4j
public final class EncodedOffsetPair implements Comparable {
+ public static final Comparator SIZE_COMPARATOR = Comparator.comparingInt(x -> x.data.capacity());
@Getter
OffsetEncoding encoding;
@Getter
@@ -42,7 +44,7 @@ public final class EncodedOffsetPair implements Comparable {
@Override
public int compareTo(EncodedOffsetPair o) {
- return Integer.compare(data.capacity(), o.getData().capacity());
+ return SIZE_COMPARATOR.compare(this, o);
}
/**
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/OffsetMapCodecManager.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/OffsetMapCodecManager.java
index 9054b411b..2149b0231 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/OffsetMapCodecManager.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/OffsetMapCodecManager.java
@@ -189,7 +189,12 @@ String serialiseIncompleteOffsetMapToBase64(long finalOffsetForPartition, Partit
byte[] encodeOffsetsCompressed(long finalOffsetForPartition, PartitionState partitionState) throws NoEncodingPossibleException {
var incompleteOffsets = partitionState.getIncompleteOffsetsBelowHighestSucceeded();
long highestSucceeded = partitionState.getOffsetHighestSucceeded();
- log.debug("Encoding partition {}: highest suceeded {}, incomplete offsets {}, ", partitionState.getTp(), highestSucceeded, incompleteOffsets);
+ if (log.isDebugEnabled()) {
+ log.debug("Encoding partition {}, highest succeeded {}, incomplete offsets to encode {}",
+ partitionState.getTp(),
+ highestSucceeded,
+ partitionState.getIncompleteOffsetsBelowHighestSucceeded());
+ }
OffsetSimultaneousEncoder simultaneousEncoder = new OffsetSimultaneousEncoder(finalOffsetForPartition, highestSucceeded, incompleteOffsets).invoke();
//
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/OffsetSimultaneousEncoder.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/OffsetSimultaneousEncoder.java
index 237bccdad..55471af66 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/OffsetSimultaneousEncoder.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/offsets/OffsetSimultaneousEncoder.java
@@ -61,8 +61,7 @@ public class OffsetSimultaneousEncoder {
* @see #packSmallest()
*/
@Getter
- PriorityQueue sortedEncodings = new PriorityQueue<>();
-
+ SortedSet sortedEncodings = new TreeSet<>();
/**
* Force the encoder to also add the compressed versions. Useful for testing.
@@ -226,7 +225,7 @@ public byte[] packSmallest() throws NoEncodingPossibleException {
if (sortedEncodings.isEmpty()) {
throw new NoEncodingPossibleException("No encodings could be used");
}
- final EncodedOffsetPair best = this.sortedEncodings.poll();
+ final EncodedOffsetPair best = this.sortedEncodings.first();
log.debug("Compression chosen is: {}", best.encoding.name());
return packEncoding(best);
}
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/PartitionState.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/PartitionState.java
index e2ccf4094..ebf33fe81 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/PartitionState.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/PartitionState.java
@@ -9,6 +9,7 @@
import io.confluent.parallelconsumer.offsets.OffsetMapCodecManager;
import lombok.Getter;
import lombok.Setter;
+import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
@@ -27,9 +28,16 @@
import static java.util.Optional.of;
import static lombok.AccessLevel.*;
+@ToString
@Slf4j
public class PartitionState {
+ /**
+ * Symbolic value for a parameter which is initialised as having an offset absent (instead of using Optional or
+ * null)
+ */
+ public static final long KAFKA_OFFSET_ABSENCE = -1L;
+
@Getter
private final TopicPartition tp;
@@ -71,7 +79,7 @@ public class PartitionState {
* Highest offset which has completed successfully ("succeeded").
*/
@Getter(PUBLIC)
- private long offsetHighestSucceeded = -1L;
+ private long offsetHighestSucceeded = KAFKA_OFFSET_ABSENCE;
/**
* If true, more messages are allowed to process for this partition.
@@ -100,6 +108,7 @@ public class PartitionState {
* ({@link #getCommitDataIfDirty()}), or reading upon {@link #onPartitionsRemoved}
*/
// todo doesn't need to be concurrent any more?
+ @ToString.Exclude
private final NavigableMap> commitQueue = new ConcurrentSkipListMap<>();
private NavigableMap> getCommitQueue() {
@@ -108,7 +117,7 @@ private NavigableMap> getCommitQueue() {
public PartitionState(TopicPartition tp, OffsetMapCodecManager.HighestOffsetAndIncompletes offsetData) {
this.tp = tp;
- this.offsetHighestSeen = offsetData.getHighestSeenOffset().orElse(-1L);
+ this.offsetHighestSeen = offsetData.getHighestSeenOffset().orElse(KAFKA_OFFSET_ABSENCE);
this.incompleteOffsets = new ConcurrentSkipListSet<>(offsetData.getIncompleteOffsets());
this.offsetHighestSucceeded = this.offsetHighestSeen;
}
@@ -230,6 +239,7 @@ public Set getAllIncompleteOffsets() {
*/
public Set getIncompleteOffsetsBelowHighestSucceeded() {
long highestSucceeded = getOffsetHighestSucceeded();
+ //noinspection FuseStreamOperations Collectors.toUnmodifiableSet since v10
return Collections.unmodifiableSet(incompleteOffsets.parallelStream()
// todo less than or less than and equal?
.filter(x -> x < highestSucceeded)
@@ -306,10 +316,21 @@ private boolean updateBlockFromEncodingResult(String offsetMapPayload) {
}
private double getPressureThresholdValue() {
- return DefaultMaxMetadataSize * PartitionMonitor.getUSED_PAYLOAD_THRESHOLD_MULTIPLIER();
+ return DefaultMaxMetadataSize * PartitionStateManager.getUSED_PAYLOAD_THRESHOLD_MULTIPLIER();
}
public void onPartitionsRemoved(ShardManager sm) {
sm.removeAnyShardsReferencedBy(getCommitQueue());
}
+
+ /**
+ * Convenience method for readability
+ *
+ * @return true if {@link #isAllowedMoreRecords()} is false
+ * @see #isAllowedMoreRecords()
+ */
+ public boolean isBlocked() {
+ return !isAllowedMoreRecords();
+ }
}
+
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/PartitionMonitor.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/PartitionStateManager.java
similarity index 76%
rename from parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/PartitionMonitor.java
rename to parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/PartitionStateManager.java
index 65c19b2dd..1ec416567 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/PartitionMonitor.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/PartitionStateManager.java
@@ -8,6 +8,7 @@
import io.confluent.parallelconsumer.ParallelConsumerOptions.ProcessingOrder;
import io.confluent.parallelconsumer.internal.AbstractParallelEoSStreamProcessor;
import io.confluent.parallelconsumer.internal.BrokerPollSystem;
+import io.confluent.parallelconsumer.internal.EpochAndRecordsMap;
import io.confluent.parallelconsumer.internal.InternalRuntimeError;
import io.confluent.parallelconsumer.offsets.OffsetMapCodecManager;
import lombok.Getter;
@@ -21,10 +22,7 @@
import org.apache.kafka.common.TopicPartition;
import java.time.Clock;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@@ -35,12 +33,14 @@
* In charge of managing {@link PartitionState}s.
*
* This state is shared between the {@link BrokerPollSystem} thread and the {@link AbstractParallelEoSStreamProcessor}.
+ *
+ * @see PartitionState
*/
@Slf4j
@RequiredArgsConstructor
-// todo rename to partition manager
-public class PartitionMonitor implements ConsumerRebalanceListener {
+public class PartitionStateManager implements ConsumerRebalanceListener {
+ public static final double USED_PAYLOAD_THRESHOLD_MULTIPLIER_DEFAULT = 0.75;
/**
* Best efforts attempt to prevent usage of offset payload beyond X% - as encoding size test is currently only done
* per batch, we need to leave some buffer for the required space to overrun before hitting the hard limit where we
@@ -48,7 +48,8 @@ public class PartitionMonitor implements ConsumerRebalanceListener {
*/
@Getter
@Setter
- private static double USED_PAYLOAD_THRESHOLD_MULTIPLIER = 0.75;
+ // todo remove static
+ private static double USED_PAYLOAD_THRESHOLD_MULTIPLIER = USED_PAYLOAD_THRESHOLD_MULTIPLIER_DEFAULT;
private final Consumer consumer;
@@ -64,20 +65,18 @@ public class PartitionMonitor implements ConsumerRebalanceListener {
/**
* Record the generations of partition assignment, for fencing off invalid work.
*
- * This must live outside of {@link PartitionState}, as it must be tracked across partition lifecycles.
+ * NOTE: This must live outside of {@link PartitionState}, as it must be tracked across partition lifecycles.
*
* Starts at zero.
+ *
+ * NOTE: Must be concurrent because it can be set by one thread, but read by another.
*/
- private final Map partitionsAssignmentEpochs = new ConcurrentHashMap<>();
+ private final Map partitionsAssignmentEpochs = new ConcurrentHashMap<>();
private final Clock clock;
public PartitionState getPartitionState(TopicPartition tp) {
- // may cause the system to wait for a rebalance to finish
- // by locking on partitionState, may cause the system to wait for a rebalance to finish
- synchronized (partitionStates) {
- return partitionStates.get(tp);
- }
+ return partitionStates.get(tp);
}
/**
@@ -86,33 +85,30 @@ public PartitionState getPartitionState(TopicPartition tp) {
@Override
public void onPartitionsAssigned(Collection assignedPartitions) {
log.debug("Partitions assigned: {}", assignedPartitions);
- synchronized (this.partitionStates) {
-
- for (final TopicPartition partitionAssignment : assignedPartitions) {
- boolean isAlreadyAssigned = this.partitionStates.containsKey(partitionAssignment);
- if (isAlreadyAssigned) {
- PartitionState previouslyAssignedState = partitionStates.get(partitionAssignment);
- if (previouslyAssignedState.isRemoved()) {
- log.trace("Reassignment of previously revoked partition {} - state: {}", partitionAssignment, previouslyAssignedState);
- } else {
- log.warn("New assignment of partition which already exists and isn't recorded as removed in " +
- "partition state. Could be a state bug - was the partition revocation somehow missed, " +
- "or is this a race? Please file a GH issue. Partition: {}, state: {}", partitionAssignment, previouslyAssignedState);
- }
+
+ for (final TopicPartition partitionAssignment : assignedPartitions) {
+ boolean isAlreadyAssigned = this.partitionStates.containsKey(partitionAssignment);
+ if (isAlreadyAssigned) {
+ PartitionState previouslyAssignedState = partitionStates.get(partitionAssignment);
+ if (previouslyAssignedState.isRemoved()) {
+ log.trace("Reassignment of previously revoked partition {} - state: {}", partitionAssignment, previouslyAssignedState);
+ } else {
+ log.warn("New assignment of partition which already exists and isn't recorded as removed in " +
+ "partition state. Could be a state bug - was the partition revocation somehow missed, " +
+ "or is this a race? Please file a GH issue. Partition: {}, state: {}", partitionAssignment, previouslyAssignedState);
}
}
+ }
- incrementPartitionAssignmentEpoch(assignedPartitions);
-
- try {
- OffsetMapCodecManager om = new OffsetMapCodecManager<>(this.consumer); // todo remove throw away instance creation - #233
- var partitionStates = om.loadPartitionStateForAssignment(assignedPartitions);
- this.partitionStates.putAll(partitionStates);
- } catch (Exception e) {
- log.error("Error in onPartitionsAssigned", e);
- throw e;
- }
+ incrementPartitionAssignmentEpoch(assignedPartitions);
+ try {
+ OffsetMapCodecManager om = new OffsetMapCodecManager<>(this.consumer); // todo remove throw away instance creation - #233
+ var partitionStates = om.loadPartitionStateForAssignment(assignedPartitions);
+ this.partitionStates.putAll(partitionStates);
+ } catch (Exception e) {
+ log.error("Error in onPartitionsAssigned", e);
+ throw e;
}
}
@@ -136,10 +132,8 @@ public void onPartitionsRevoked(Collection partitions) {
}
void onPartitionsRemoved(final Collection partitions) {
- synchronized (this.partitionStates) {
- incrementPartitionAssignmentEpoch(partitions);
- resetOffsetMapAndRemoveWork(partitions);
- }
+ incrementPartitionAssignmentEpoch(partitions);
+ resetOffsetMapAndRemoveWork(partitions);
}
/**
@@ -198,19 +192,28 @@ private void resetOffsetMapAndRemoveWork(Collection allRemovedPa
}
}
- public int getEpoch(final ConsumerRecord rec) {
+ /**
+ * @return the current epoch of the partition this record belongs to
+ */
+ public Long getEpochOfPartitionForRecord(final ConsumerRecord rec) {
var tp = toTopicPartition(rec);
- Integer epoch = partitionsAssignmentEpochs.get(tp);
- rec.topic();
+ Long epoch = partitionsAssignmentEpochs.get(tp);
if (epoch == null) {
throw new InternalRuntimeError(msg("Received message for a partition which is not assigned: {}", rec));
}
return epoch;
}
+ /**
+ * @return the current epoch of the partition
+ */
+ public Long getEpochOfPartition(TopicPartition partition) {
+ return partitionsAssignmentEpochs.get(partition);
+ }
+
private void incrementPartitionAssignmentEpoch(final Collection partitions) {
for (final TopicPartition partition : partitions) {
- int epoch = partitionsAssignmentEpochs.getOrDefault(partition, -1);
+ Long epoch = partitionsAssignmentEpochs.getOrDefault(partition, PartitionState.KAFKA_OFFSET_ABSENCE);
epoch++;
partitionsAssignmentEpochs.put(partition, epoch);
}
@@ -227,8 +230,8 @@ private void incrementPartitionAssignmentEpoch(final Collection
boolean checkIfWorkIsStale(final WorkContainer, ?> workContainer) {
var topicPartitionKey = workContainer.getTopicPartition();
- Integer currentPartitionEpoch = partitionsAssignmentEpochs.get(topicPartitionKey);
- int workEpoch = workContainer.getEpoch();
+ Long currentPartitionEpoch = partitionsAssignmentEpochs.get(topicPartitionKey);
+ long workEpoch = workContainer.getEpoch();
boolean partitionNotAssigned = isPartitionRemovedOrNeverAssigned(workContainer.getCr());
@@ -327,36 +330,40 @@ public void onFailure(WorkContainer wc) {
/**
* Takes a record as work and puts it into internal queues, unless it's been previously recorded as completed as per
* loaded records.
- *
- * Locking on partition state here, means that the check for assignment is in the same sync block as registering the
- * work with the {@link TopicPartition}'s {@link PartitionState} and the {@link ShardManager}. Keeping the two
- * different views in sync. Of course now, having a shared nothing architecture would mean all access to the state
- * is by a single thread, and so this could never occur (see ).
- *
- * @return true if the record was taken, false if it was skipped (previously successful)
*/
- boolean maybeRegisterNewRecordAsWork(final ConsumerRecord rec) {
- if (rec == null) return false;
+ void maybeRegisterNewRecordAsWork(final EpochAndRecordsMap recordsMap) {
+ for (var partition : recordsMap.partitions()) {
+ var recordsList = recordsMap.records(partition);
+ var epochOfInboundRecords = recordsList.getEpochOfPartitionAtPoll();
+ for (var rec : recordsList.getRecords()) {
+ maybeRegisterNewRecordAsWork(epochOfInboundRecords, rec);
+ }
+ }
+ }
+
+ /**
+ * @see #maybeRegisterNewRecordAsWork(EpochAndRecordsMap)
+ */
+ private void maybeRegisterNewRecordAsWork(Long epochOfInboundRecords, ConsumerRecord rec) {
+ // do epochs still match? do a proactive check, but the epoch will be checked again at work completion as well
+ var currentPartitionEpoch = getEpochOfPartitionForRecord(rec);
+ if (Objects.equals(epochOfInboundRecords, currentPartitionEpoch)) {
- synchronized (partitionStates) {
if (isPartitionRemovedOrNeverAssigned(rec)) {
log.debug("Record in buffer for a partition no longer assigned. Dropping. TP: {} rec: {}", toTopicPartition(rec), rec);
- return false;
}
if (isRecordPreviouslyCompleted(rec)) {
log.trace("Record previously completed, skipping. offset: {}", rec.offset());
- return false;
} else {
- int currentPartitionEpoch = getEpoch(rec);
- var wc = new WorkContainer<>(currentPartitionEpoch, rec, options.getRetryDelayProvider(), clock);
-
- sm.addWorkContainer(wc);
+ var work = new WorkContainer<>(epochOfInboundRecords, rec, options.getRetryDelayProvider(), clock);
- addWorkContainer(wc);
-
- return true;
+ sm.addWorkContainer(work);
+ addWorkContainer(work);
}
+ } else {
+ log.debug("Inbound record of work has epoch ({}) not matching currently assigned epoch for the applicable partition ({}), skipping",
+ epochOfInboundRecords, currentPartitionEpoch);
}
}
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/ProcessingShard.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/ProcessingShard.java
index 22ff310fd..9727fcb73 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/ProcessingShard.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/ProcessingShard.java
@@ -35,6 +35,7 @@ public class ProcessingShard {
* Uses a ConcurrentSkipListMap instead of a TreeMap as under high pressure there appears to be some concurrency
* errors (missing WorkContainers).
*/
+ @Getter
private final NavigableMap> entries = new ConcurrentSkipListMap<>();
@Getter(PRIVATE)
@@ -42,7 +43,7 @@ public class ProcessingShard {
private final ParallelConsumerOptions, ?> options;
- private final PartitionMonitor pm;
+ private final PartitionStateManager pm;
private final RateLimiter slowWarningRateLimit = new RateLimiter(5);
@@ -74,9 +75,19 @@ public Optional> getWorkForOffset(long offset) {
}
public long getCountOfWorkAwaitingSelection() {
- return entries.values().parallelStream()
+ return entries.values().stream()
// todo missing pm.isBlocked(topicPartition) ?
- .filter(kvWorkContainer -> kvWorkContainer.isAvailableToTakeAsWork())
+ .filter(WorkContainer::isAvailableToTakeAsWork)
+ .count();
+ }
+
+ public long getCountOfWorkTracked() {
+ return entries.size();
+ }
+
+ public long getCountWorkInFlight() {
+ return entries.values().stream()
+ .filter(WorkContainer::isInFlight)
.count();
}
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/RemovedPartitionState.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/RemovedPartitionState.java
index 50b2f9749..aef4946f1 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/RemovedPartitionState.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/RemovedPartitionState.java
@@ -18,13 +18,13 @@
* references or {@link Optional}s. By replacing with a no op implementation, we protect for stale messages still in
* queues which reference it, among other things.
*
- * The alternative to this implementation, is having {@link PartitionMonitor#getPartitionState(TopicPartition)} return
- * {@link Optional}, which forces the implicit null check everywhere partition state is retrieved. This was drafted to a
- * degree, but found to be extremely invasive, where this solution with decent separation of concerns and encapsulation,
- * is sufficient and potentially more useful as is non-destructive. Potential issue is that of memory leak as the
- * collection will forever expand. However, even massive partition counts to a single consumer would be in the hundreds
- * of thousands, this would only result in hundreds of thousands of {@link TopicPartition} object keys all pointing to
- * the same instance of {@link RemovedPartitionState}.
+ * The alternative to this implementation, is having {@link PartitionStateManager#getPartitionState(TopicPartition)}
+ * return {@link Optional}, which forces the implicit null check everywhere partition state is retrieved. This was
+ * drafted to a degree, but found to be extremely invasive, where this solution with decent separation of concerns and
+ * encapsulation, is sufficient and potentially more useful as is non-destructive. Potential issue is that of memory
+ * leak as the collection will forever expand. However, even massive partition counts to a single consumer would be in
+ * the hundreds of thousands, this would only result in hundreds of thousands of {@link TopicPartition} object keys all
+ * pointing to the same instance of {@link RemovedPartitionState}.
*/
@Slf4j
public class RemovedPartitionState extends PartitionState {
@@ -82,13 +82,13 @@ public Set getIncompleteOffsetsBelowHighestSucceeded() {
@Override
public long getOffsetHighestSeen() {
log.debug(NO_OP);
- return -1L;
+ return PartitionState.KAFKA_OFFSET_ABSENCE;
}
@Override
public long getOffsetHighestSucceeded() {
log.debug(NO_OP);
- return -1L;
+ return PartitionState.KAFKA_OFFSET_ABSENCE;
}
@Override
diff --git a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/ShardManager.java b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/ShardManager.java
index e8da53748..215494c6e 100644
--- a/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/ShardManager.java
+++ b/parallel-consumer-core/src/main/java/io/confluent/parallelconsumer/state/ShardManager.java
@@ -57,7 +57,7 @@ public class ShardManager {
* @see K
* @see WorkManager#getWorkIfAvailable()
*/
- // todo performance: disable/remove if using partition order
+ // performance: could disable/remove if using partition order - but probably not worth the added complexity in the code to handle an extra special case
private final Map