You are viewing a plain text version of this content. The canonical link for it is here.
Posted to jira@kafka.apache.org by "adixitconfluent (via GitHub)" <gi...@apache.org> on 2025/03/21 11:48:51 UTC

[PR] Kafka 16729 [kafka]

adixitconfluent opened a new pull request, #19261:
URL: https://github.com/apache/kafka/pull/19261

   Delete this text and replace it with a detailed description of your change. The 
   PR title and body will become the squashed commit message.
   
   If you would like to tag individuals, add some commentary, upload images, or
   include other supplemental information that should not be part of the eventual
   commit message, please use a separate comment.
   
   If applicable, please include a summary of the testing strategy (including 
   rationale) for the proposed change. Unit and/or integration tests are expected
   for any behavior change and system tests should be considered for larger
   changes.
   


-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2036147271


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2475,6 +2496,192 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) {
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+        List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+        for (RecordBatch recordBatch : recordsToArchive) {
+            // Archive the offsets/batches in the cached state.
+            NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        }
+        return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+    }
+
+    /**
+     * This function filters out the offsets present in the acquired records list that are also a part of batches that need to be archived.
+     * It follows an iterative refinement of acquired records to eliminate batches to be archived.
+     * @param acquiredRecordsList The list containing acquired records. This list is sorted by the firstOffset of the acquired batch.
+     * @param batchesToArchive The list containing record batches to archive. This list is sorted by the baseOffset of the record batch.
+     * @return The list containing filtered acquired records offsets.
+     */
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecordsList,
+        List<RecordBatch> batchesToArchive
+    ) {
+        List<AcquiredRecords> result = new ArrayList<>();
+        Iterator<AcquiredRecords> acquiredRecordsListIter = acquiredRecordsList.iterator();
+        Iterator<RecordBatch> batchesToArchiveIterator = batchesToArchive.iterator();
+        if (!batchesToArchiveIterator.hasNext())
+            return acquiredRecordsList;

Review Comment:
   This should be the first check in the method, prior initializing `result = new ArrayList()`.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -1241,7 +1254,8 @@ private boolean archiveAvailableRecords(long startOffset, long endOffset, Naviga
 
     private boolean archivePerOffsetBatchRecords(InFlightBatch inFlightBatch,
                                                  long startOffsetToArchive,
-                                                 long endOffsetToArchive) {
+                                                 long endOffsetToArchive,
+                                                 RecordState initialState) {

Review Comment:
   nit: seems missed.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013577087


##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/GroupConfig.java:
##########
@@ -83,6 +92,8 @@ public final class GroupConfig extends AbstractConfig {
 
     public final int streamsNumStandbyReplicas;
 
+    public final int shareIsolationLevel;

Review Comment:
   my bad. I misread it. Changed the code to reflect the right values.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013847250


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {

Review Comment:
   I meant regarding why do we need Priority Queue and those details. Anyways, I ll figure it out.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013529068


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -847,6 +859,7 @@ public ShareAcquiredRecords acquire(
             }

Review Comment:
   we are doing the filtering after this point, so this should case would be automatically covered.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022439069


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2504,205 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), RecordState.ACQUIRED);
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteBatch(inFlightBatch, RecordState.ACQUIRED);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * This function fetches the sub map from cachedState where all the offset details present in the recordBatch can be referred to
+     * OR it gives an exception if those offsets are not present in cachedState.
+     * @param recordBatch The record batch for which we want to find the sub map.
+     * @return the sub map containing all the offset details.
+     */
+    private NavigableMap<Long, InFlightBatch> fetchSubMapOrException(RecordBatch recordBatch) {
+        lock.readLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorEntry = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorEntry == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorEntry.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = orderedAbortedTransactions(abortedTransactions);
+        Set<Long> abortedProducerIds = new HashSet<>();
+        List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+        for (RecordBatch currentBatch : batches) {
+            if (currentBatch.hasProducerId()) {
+                // remove from the aborted transactions queue, all aborted transactions which have begun before the
+                // current batch's last offset and add the associated producerIds to the aborted producer set.
+                while (!orderedAbortedTransactions.isEmpty() && orderedAbortedTransactions.peek().firstOffset() <= currentBatch.lastOffset()) {
+                    FetchResponseData.AbortedTransaction abortedTransaction = orderedAbortedTransactions.poll();
+                    abortedProducerIds.add(abortedTransaction.producerId());
+                }
+                long producerId = currentBatch.producerId();
+                if (containsAbortMarker(currentBatch)) {
+                    abortedProducerIds.remove(producerId);
+                } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                    log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                        "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                    recordsToArchive.add(currentBatch);
+                }
+            }
+        }
+        return recordsToArchive;
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = new PriorityQueue<>(
+            abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+        );
+        orderedAbortedTransactions.addAll(abortedTransactions);
+        return orderedAbortedTransactions;
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        if (!batch.isControlBatch())
+            return false;
+
+        Iterator<Record> batchIterator = batch.iterator();
+        if (!batchIterator.hasNext())
+            return false;
+
+        Record firstRecord = batchIterator.next();
+        return ControlRecordType.ABORT == ControlRecordType.parse(firstRecord.key());
+    }

Review Comment:
   The functions `orderedAbortedTransactions`, `isBatchAborted`, `containsAbortMarker` are mirrors of what happens in `CompletedFetch` class. You won't find any unit tests for these functions there as well. That's why I've avoided them here as well. I am testing the logic that comes after we get aborted transactional records provided by function `fetchAbortedTransactionRecordBatches`. You'll be finding mocks for this function in `SharePartitionTest`
   cc- @AndrewJSchofield 



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2504,205 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), RecordState.ACQUIRED);
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteBatch(inFlightBatch, RecordState.ACQUIRED);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * This function fetches the sub map from cachedState where all the offset details present in the recordBatch can be referred to
+     * OR it gives an exception if those offsets are not present in cachedState.
+     * @param recordBatch The record batch for which we want to find the sub map.
+     * @return the sub map containing all the offset details.
+     */
+    private NavigableMap<Long, InFlightBatch> fetchSubMapOrException(RecordBatch recordBatch) {
+        lock.readLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorEntry = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorEntry == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorEntry.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = orderedAbortedTransactions(abortedTransactions);
+        Set<Long> abortedProducerIds = new HashSet<>();
+        List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+        for (RecordBatch currentBatch : batches) {
+            if (currentBatch.hasProducerId()) {
+                // remove from the aborted transactions queue, all aborted transactions which have begun before the
+                // current batch's last offset and add the associated producerIds to the aborted producer set.
+                while (!orderedAbortedTransactions.isEmpty() && orderedAbortedTransactions.peek().firstOffset() <= currentBatch.lastOffset()) {
+                    FetchResponseData.AbortedTransaction abortedTransaction = orderedAbortedTransactions.poll();
+                    abortedProducerIds.add(abortedTransaction.producerId());
+                }
+                long producerId = currentBatch.producerId();
+                if (containsAbortMarker(currentBatch)) {
+                    abortedProducerIds.remove(producerId);
+                } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                    log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                        "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                    recordsToArchive.add(currentBatch);
+                }
+            }
+        }
+        return recordsToArchive;
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = new PriorityQueue<>(
+            abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+        );
+        orderedAbortedTransactions.addAll(abortedTransactions);
+        return orderedAbortedTransactions;
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        if (!batch.isControlBatch())
+            return false;
+
+        Iterator<Record> batchIterator = batch.iterator();
+        if (!batchIterator.hasNext())
+            return false;
+
+        Record firstRecord = batchIterator.next();
+        return ControlRecordType.ABORT == ControlRecordType.parse(firstRecord.key());
+    }

Review Comment:
   The functions `orderedAbortedTransactions`, `isBatchAborted`, `containsAbortMarker` are mirrors of what happens in `CompletedFetch` class. You won't find any unit tests for these functions there. That's why I've avoided them here as well. I am testing the logic that comes after we get aborted transactional records provided by function `fetchAbortedTransactionRecordBatches`. You'll be finding mocks for this function in `SharePartitionTest`
   cc- @AndrewJSchofield 



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "junrao (via GitHub)" <gi...@apache.org>.
junrao commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2025608562


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {

Review Comment:
   This is probably ok, but we could have just iterated recordsToArchive and tempAcquiredRecords once to do a merge instead of double loop.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();

Review Comment:
   Do we need to lock here? The only caller is `acquire()` and already holds the lock. Ditto for the locking in `archiveRecords()`



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();

Review Comment:
   Do we need to lock here since the caller already holds the lock?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2031767657


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);

Review Comment:
   I don't think that this will force the test to wait for 2.5 sec. This is the maximum time to block. The method returns immediately if there are records available. Otherwise, it will await the passed timeout.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "AndrewJSchofield (via GitHub)" <gi...@apache.org>.
AndrewJSchofield commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2020777296


##########
group-coordinator/src/test/java/org/apache/kafka/coordinator/group/GroupConfigTest.java:
##########
@@ -67,6 +67,8 @@ public void testFromPropsInvalid() {
                 assertPropertyInvalid(name, "not_a_number", "1.0");
             } else if (GroupConfig.STREAMS_SESSION_TIMEOUT_MS_CONFIG.equals(name)) {
                 assertPropertyInvalid(name, "not_a_number", "1.0");
+            } else if (GroupConfig.SHARE_ISOLATION_LEVEL_CONFIG.equals(name)) {

Review Comment:
   nit: Please move up to the end of the other share configs.



##########
group-coordinator/src/test/java/org/apache/kafka/coordinator/group/GroupConfigTest.java:
##########
@@ -247,6 +274,7 @@ private Properties createValidGroupConfig() {
         props.put(GroupConfig.STREAMS_SESSION_TIMEOUT_MS_CONFIG, "50000");
         props.put(GroupConfig.STREAMS_HEARTBEAT_INTERVAL_MS_CONFIG, "6000");
         props.put(GroupConfig.STREAMS_NUM_STANDBY_REPLICAS_CONFIG, "1");
+        props.put(GroupConfig.SHARE_ISOLATION_LEVEL_CONFIG, "read_uncommitted");

Review Comment:
   nit: I would tend to put all of the consumer configs, and then the share configs, and then the streams configs, just for neatness.



##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -3239,7 +3239,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         shareFetchRequest.maxWait,
         fetchMinBytes,
         fetchMaxBytes,
-        FetchIsolation.HIGH_WATERMARK,
+        FetchIsolation.of(-1, GroupConfig.defaultShareIsolationLevel),

Review Comment:
   This `-1` can be `FetchRequest.CONSUMER_REPLICA_ID`.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,243 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any

Review Comment:
   Isolation level, not type, please.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,243 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions);
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {

Review Comment:
   The name `abortedTransactionsHeap` seems very clumsy to me. What you are doing is ensuring that the returned list is ordered by increasing first offset. So, "sorted" or "ordered" might be useful in the name.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -683,6 +692,7 @@ public long nextFetchOffset() {
      *                           if the records are already part of the same fetch batch.
      * @param fetchOffset        The fetch offset for which the records are fetched.
      * @param fetchPartitionData The fetched records for the share partition.
+     * @param isolationType      The isolation level for the share fetch request.

Review Comment:
   nit: It's `isolationLevel` not `isolationType` I think. Even the comment agrees with me ;0)



##########
core/src/main/java/kafka/server/share/SharePartitionManager.java:
##########
@@ -267,8 +268,20 @@ public CompletableFuture<Map<TopicIdPartition, PartitionData>> fetchMessages(
             .rotate(topicIdPartitions, new PartitionRotateMetadata(sessionEpoch));
 
         CompletableFuture<Map<TopicIdPartition, PartitionData>> future = new CompletableFuture<>();
-        processShareFetch(new ShareFetch(fetchParams, groupId, memberId, future, rotatedTopicIdPartitions, batchSize, maxFetchRecords, brokerTopicStats));
-
+        if (groupConfigManager.groupConfig(groupId).isEmpty()) {
+            processShareFetch(new ShareFetch(fetchParams, groupId, memberId, future, rotatedTopicIdPartitions, batchSize, maxFetchRecords, brokerTopicStats));
+        } else {
+            FetchParams updatedFetchParams = new FetchParams(
+                fetchParams.replicaId,
+                fetchParams.replicaEpoch,
+                fetchParams.maxWaitMs,
+                fetchParams.minBytes,
+                fetchParams.maxBytes,
+                FetchIsolation.of(-1, groupConfigManager.groupConfig(groupId).get().shareIsolationLevel()),

Review Comment:
   This `-1` can be `FetchRequest.CONSUMER_REPLICA_ID`.



##########
core/src/test/scala/unit/kafka/server/KafkaApisTest.scala:
##########
@@ -338,6 +338,7 @@ class KafkaApisTest extends Logging {
     cgConfigs.put(STREAMS_HEARTBEAT_INTERVAL_MS_CONFIG, GroupCoordinatorConfig.STREAMS_GROUP_HEARTBEAT_INTERVAL_MS_DEFAULT.toString)
     cgConfigs.put(STREAMS_SESSION_TIMEOUT_MS_CONFIG, GroupCoordinatorConfig.STREAMS_GROUP_SESSION_TIMEOUT_MS_DEFAULT.toString)
     cgConfigs.put(STREAMS_NUM_STANDBY_REPLICAS_CONFIG, GroupCoordinatorConfig.STREAMS_GROUP_NUM_STANDBY_REPLICAS_DEFAULT.toString)
+    cgConfigs.put(SHARE_ISOLATION_LEVEL_CONFIG, GroupConfig.SHARE_ISOLATION_LEVEL_DEFAULT)

Review Comment:
   nit: Please move to the end of the other share group configs.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,243 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {

Review Comment:
   nit: This source file tends to put the parameter list over multiple lines when it gets this long.



##########
group-coordinator/src/test/java/org/apache/kafka/coordinator/group/GroupConfigTest.java:
##########
@@ -212,6 +237,7 @@ public void testFromPropsWithDefaultValue() {
         defaultValue.put(GroupConfig.STREAMS_HEARTBEAT_INTERVAL_MS_CONFIG, "10");
         defaultValue.put(GroupConfig.STREAMS_SESSION_TIMEOUT_MS_CONFIG, "2000");
         defaultValue.put(GroupConfig.STREAMS_NUM_STANDBY_REPLICAS_CONFIG, "1");
+        defaultValue.put(GroupConfig.SHARE_ISOLATION_LEVEL_CONFIG, "read_uncommitted");

Review Comment:
   nit: Move to the end of the other share group configs.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,243 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions);
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        if (abortedTransactions == null || abortedTransactions.isEmpty())

Review Comment:
   I think we know that neither of these conditions is true. I'd prefer to take advantage of that knowledge so that the caller does not need to handle the null return here with its own conditional logic.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {

Review Comment:
   This code essentially mirrors the way that aborted transactions are handled in `CompletedFetch.abortedTransactions`. If you look in detail at the way the information comes out of the replica manager, I think it's a good thing that the code being added to `SharePartition` is clearly doing the same thing. It would be easy to mess it up in odd cases by re-inventing the wheel.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2021428929


##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/GroupConfig.java:
##########
@@ -290,6 +309,10 @@ public static ShareGroupAutoOffsetResetStrategy defaultShareAutoOffsetReset() {
         return ShareGroupAutoOffsetResetStrategy.fromString(SHARE_AUTO_OFFSET_RESET_DEFAULT);
     }
 
+    public static IsolationLevel defaultShareIsolationLevel() {

Review Comment:
   Please write javadoc as being followed for other public methods in the class.



##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/GroupConfig.java:
##########
@@ -352,4 +375,15 @@ public int streamsHeartbeatIntervalMs() {
     public int streamsNumStandbyReplicas() {
         return streamsNumStandbyReplicas;
     }
+
+    public IsolationLevel shareIsolationLevel() {

Review Comment:
   Please write javadoc as being followed for other public methods in the class.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {

Review Comment:
   Please write method comments for the method. Also should the name be `fetchSubMapOrException`?



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");

Review Comment:
   Should it be `InvalidRecordStateException` when some offset is not found?



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());

Review Comment:
   ```suggestion
               Map.Entry<Long, InFlightBatch> floorEntry = cachedState.floorEntry(recordBatch.baseOffset());
   ```



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();

Review Comment:
   What operations in the method requires lock?



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());

Review Comment:
   nit: break the line in 2 lines please.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = orderedAbortedTransactions(abortedTransactions);
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun before the
+                    // current batch's last offset and add the associated producerIds to the aborted producer set.
+                    while (!orderedAbortedTransactions.isEmpty() && orderedAbortedTransactions.peek().firstOffset() <= currentBatch.lastOffset()) {
+                        FetchResponseData.AbortedTransaction abortedTransaction = orderedAbortedTransactions.poll();
+                        abortedProducerIds.add(abortedTransaction.producerId());
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = new PriorityQueue<>(
+            abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+        );
+        orderedAbortedTransactions.addAll(abortedTransactions);
+        return orderedAbortedTransactions;
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        if (!batch.isControlBatch())
+            return false;
+
+        Iterator<Record> batchIterator = batch.iterator();
+        if (!batchIterator.hasNext())
+            return false;
+
+        Record firstRecord = batchIterator.next();
+        return ControlRecordType.ABORT == ControlRecordType.parse(firstRecord.key());
+

Review Comment:
   nit: remove blank line



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();

Review Comment:
   Should it be readLock?



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {

Review Comment:
   Seems similar to `archivePerOffsetBatchRecords`, can't there be common code?



##########
core/src/main/java/kafka/server/share/SharePartitionManager.java:
##########
@@ -267,8 +269,20 @@ public CompletableFuture<Map<TopicIdPartition, PartitionData>> fetchMessages(
             .rotate(topicIdPartitions, new PartitionRotateMetadata(sessionEpoch));
 
         CompletableFuture<Map<TopicIdPartition, PartitionData>> future = new CompletableFuture<>();
-        processShareFetch(new ShareFetch(fetchParams, groupId, memberId, future, rotatedTopicIdPartitions, batchSize, maxFetchRecords, brokerTopicStats));
-
+        if (groupConfigManager.groupConfig(groupId).isEmpty()) {
+            processShareFetch(new ShareFetch(fetchParams, groupId, memberId, future, rotatedTopicIdPartitions, batchSize, maxFetchRecords, brokerTopicStats));
+        } else {
+            FetchParams updatedFetchParams = new FetchParams(
+                fetchParams.replicaId,
+                fetchParams.replicaEpoch,
+                fetchParams.maxWaitMs,
+                fetchParams.minBytes,
+                fetchParams.maxBytes,
+                FetchIsolation.of(FetchRequest.CONSUMER_REPLICA_ID, groupConfigManager.groupConfig(groupId).get().shareIsolationLevel()),
+                fetchParams.clientMetadata
+            );
+            processShareFetch(new ShareFetch(updatedFetchParams, groupId, memberId, future, rotatedTopicIdPartitions, batchSize, maxFetchRecords, brokerTopicStats));
+        }

Review Comment:
   Please have a method `maybUpdateFetchParams` which shall return updated fetchParams and use it here.
   ```
   FetchParams updatedFetchParams = maybeUpdateFetchParams(fetchParams);
   processShareFetch(new ShareFetch(updatedFetchParams, groupId, memberId, future, rotatedTopicIdPartitions, batchSize, maxFetchRecords, brokerTopicStats));
   ```
   ```
   private FetchParams  maybUpdateFetchParams(FetchParams fetchParams) {
   if (groupConfigManager.groupConfig(groupId).isEmpty()) {
               return fetchParams;
   }
    FetchParams updatedFetchParams = new FetchParams(
                   fetchParams.replicaId,
                   fetchParams.replicaEpoch,
                   fetchParams.maxWaitMs,
                   fetchParams.minBytes,
                   fetchParams.maxBytes,
                   FetchIsolation.of(FetchRequest.CONSUMER_REPLICA_ID, groupConfigManager.groupConfig(groupId).get().shareIsolationLevel()),
                   fetchParams.clientMetadata
               );
   return updatedFetchParams;
   }



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {

Review Comment:
   Seems similar to `archiveCompleteBatch`.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {

Review Comment:
   Should these `archive` methods be aligned together with `archiveAvailableRecords`.  Moreover seems the method is similar to `archiveAvailableRecords` itself, can't there be common method as the required batch/offset state can be passed as method argument?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2026281487


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));

Review Comment:
   good catch, yes



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "smjn (via GitHub)" <gi...@apache.org>.
smjn commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2024547661


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);
+                assertEquals(0, records.count());
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                // Message 3 would be returned by this poll.
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 3", new String(record.value()));
+                // We will make Message 3 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // Precautionary poll so that the aborted transaction for Message 4 is fetched by the consumer.
+                shareConsumer.poll(Duration.ofMillis(5000));
+
+                // We are altering IsolationLevel to READ_UNCOMMITTED now. We will read both committed/aborted transactions now.
+                alterShareIsolationLevel("group1", "read_uncommitted");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_UNCOMMITTED, we can consume Message 3 (committed transaction that was released), Message 5, Message 6, Message 7 and Message 8.
+                List<String> finalMessages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            finalMessages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return finalMessages.size() == 5;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", finalMessages.get(0));
+                assertEquals("Message 5", finalMessages.get(1));
+                assertEquals("Message 6", finalMessages.get(2));
+                assertEquals("Message 7", finalMessages.get(3));
+                assertEquals("Message 8", finalMessages.get(4));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithRejectAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);

Review Comment:
   This might not be enough. Ideally, we should call `waitForAssignment` to make sure something has been assigned as well as 0 records.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013434152


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions == null || abortedTransactions.isEmpty())
+                return null;
+
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = new PriorityQueue<>(
+                abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+            );
+            abortedTransactionsHeap.addAll(abortedTransactions);
+            return abortedTransactionsHeap;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        lock.writeLock().lock();
+        try {
+            return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        lock.writeLock().lock();

Review Comment:
   Why this method code should have any lock?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013997096


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {

Review Comment:
   Priority Queue makes our code more efficient, rather than iterating over all the offsets that we've already passed through.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013463435


##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -104,7 +104,8 @@ class KafkaApis(val requestChannel: RequestChannel,
                 time: Time,
                 val tokenManager: DelegationTokenManager,
                 val apiVersionManager: ApiVersionManager,
-                val clientMetricsManager: ClientMetricsManager
+                val clientMetricsManager: ClientMetricsManager,
+                val groupConfigManager: GroupConfigManager

Review Comment:
   The only thing which you might want to check is that if we need to have correct Isolation Level in FetchParams from beginning, if somewhere used in ReplicaManager fetch.



##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -104,7 +104,8 @@ class KafkaApis(val requestChannel: RequestChannel,
                 time: Time,
                 val tokenManager: DelegationTokenManager,
                 val apiVersionManager: ApiVersionManager,
-                val clientMetricsManager: ClientMetricsManager
+                val clientMetricsManager: ClientMetricsManager,
+                val groupConfigManager: GroupConfigManager

Review Comment:
   The other thing which you might want to check is that if we need to have correct Isolation Level in FetchParams from beginning, if somewhere used in ReplicaManager fetch.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "junrao (via GitHub)" <gi...@apache.org>.
junrao commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2033582699


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,339 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);

Review Comment:
   Since we are not waiting for the timeout in the common case, it's better to use a longer timeout like 5secs to avoid flakiness. 



##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,339 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // Setting the acknowledgement commit callback to verify acknowledgement completion.
+                Map<TopicPartition, Set<Long>> partitionOffsetsMap = new HashMap<>();
+                Map<TopicPartition, Exception> partitionExceptionMap = new HashMap<>();
+                shareConsumer.setAcknowledgementCommitCallback(new TestableAcknowledgementCommitCallback(partitionOffsetsMap, partitionExceptionMap));
+
+                // We will not receive any records since the transaction for Message 2 was aborted. Wait for the
+                // aborted marker offset for Message 2 (3L) to be fetched and acknowledged by the consumer.
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(500));
+                    return pollRecords.count() == 0 && partitionExceptionMap.containsKey(tp) && partitionOffsetsMap.containsKey(tp) && partitionOffsetsMap.get(tp).contains(3L);

Review Comment:
   Hmm, why do we wait for an exception? The exception is actually null. 
   
   Also, is it true that control records will also be acknowledged?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2024051965


##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -3239,7 +3239,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         shareFetchRequest.maxWait,
         fetchMinBytes,
         fetchMaxBytes,
-        FetchIsolation.HIGH_WATERMARK,
+        FetchIsolation.of(FetchRequest.CONSUMER_REPLICA_ID, GroupConfig.defaultShareIsolationLevel),

Review Comment:
   Thanks 👍 



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022196608


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");

Review Comment:
   Do you think it should be `IllegalStateException`?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2026469750


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {

Review Comment:
   I have optimized the code to avoid the double loop and iterate once.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2031745262


##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -6671,15 +6693,441 @@ private String assertionFailedMessage(SharePartition sharePartition, Map<Long, L
         return errorMessage.toString();
     }
 
+    @Test
+    public void testFilterRecordBatchesFromAcquiredRecords() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        List<AcquiredRecords> acquiredRecords1 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(5).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(10).setLastOffset(15).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches1 = List.of(
+            memoryRecordsBuilder(3, 2).build().batches().iterator().next(),
+            memoryRecordsBuilder(3, 12).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(1).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(5).setLastOffset(5).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(10).setLastOffset(11).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(15).setLastOffset(15).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)),
+            sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords1, recordBatches1));
+
+        List<AcquiredRecords> acquiredRecords2 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(5).setLastOffset(8).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(9).setLastOffset(30).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(31).setLastOffset(40).setDeliveryCount((short) 3)
+        );
+        List<RecordBatch> recordBatches2 = List.of(
+            memoryRecordsBuilder(21, 5).build().batches().iterator().next(),
+            memoryRecordsBuilder(5, 31).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+                new AcquiredRecords().setFirstOffset(26).setLastOffset(30).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(36).setLastOffset(40).setDeliveryCount((short) 3)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, recordBatches2)
+        );
+
+        // Record batches is empty.
+        assertEquals(acquiredRecords2, sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, List.of()));
+
+        List<AcquiredRecords> acquiredRecords3 = List.of(
+            new AcquiredRecords().setFirstOffset(0).setLastOffset(19).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches3 = List.of(
+            memoryRecordsBuilder(1, 8).build().batches().iterator().next(),
+            memoryRecordsBuilder(1, 18).build().batches().iterator().next()
+        );
+
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(0).setLastOffset(7).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(9).setLastOffset(17).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(19).setLastOffset(19).setDeliveryCount((short) 1)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords3, recordBatches3)
+        );
+    }
+
+    @Test
+    public void testAcquireWithReadCommittedIsolationLevel() {
+        SharePartition sharePartition = Mockito.spy(SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build());
+
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        memoryRecordsBuilder(buffer, 5, 10).close();
+        memoryRecordsBuilder(buffer, 5, 15).close();
+        memoryRecordsBuilder(buffer, 15, 20).close();
+        memoryRecordsBuilder(buffer, 8, 50).close();
+        memoryRecordsBuilder(buffer, 10, 58).close();
+        memoryRecordsBuilder(buffer, 5, 70).close();
+
+        buffer.flip();
+        MemoryRecords records = MemoryRecords.readableRecords(buffer);
+        FetchPartitionData fetchPartitionData = fetchPartitionData(records, newAbortedTransactions());
+
+        // We are mocking the result of function fetchAbortedTransactionRecordBatches. The records present at these offsets need to be archived.
+        // We won't be utilizing the aborted transactions passed in fetchPartitionData.
+        when(sharePartition.fetchAbortedTransactionRecordBatches(fetchPartitionData.records.batches(), fetchPartitionData.abortedTransactions.get())).thenReturn(
+            List.of(
+                memoryRecordsBuilder(5, 10).build().batches().iterator().next(),
+                memoryRecordsBuilder(10, 58).build().batches().iterator().next(),
+                memoryRecordsBuilder(5, 70).build().batches().iterator().next()
+            )
+        );
+
+        List<AcquiredRecords> acquiredRecordsList = fetchAcquiredRecords(
+            sharePartition.acquire(
+                MEMBER_ID,
+                10 /* Batch size */,
+                100,
+                DEFAULT_FETCH_OFFSET,
+                fetchPartitionData,
+                FetchIsolation.TXN_COMMITTED),
+            45 /* Gap of 15 records will be added to second batch, gap of 2 records will also be added to fourth batch */);
+
+        assertEquals(List.of(
+            new AcquiredRecords().setFirstOffset(15).setLastOffset(19).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(49).setDeliveryCount((short) 1),

Review Comment:
   This is because there is a gap of records from 35-49 in the original records to be acquired. This gap is added to this batch since we rely on the client to inform the broker about these natural gaps in the partition log.
   ```
   memoryRecordsBuilder(buffer, 5, 10).close(); // batch from 10-14
   memoryRecordsBuilder(buffer, 5, 15).close(); // batch from 15-19
   memoryRecordsBuilder(buffer, 15, 20).close(); // batch from 20-34
   memoryRecordsBuilder(buffer, 8, 50).close();  // batch from 35-49
   ```



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2033651560


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,339 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // Setting the acknowledgement commit callback to verify acknowledgement completion.
+                Map<TopicPartition, Set<Long>> partitionOffsetsMap = new HashMap<>();
+                Map<TopicPartition, Exception> partitionExceptionMap = new HashMap<>();
+                shareConsumer.setAcknowledgementCommitCallback(new TestableAcknowledgementCommitCallback(partitionOffsetsMap, partitionExceptionMap));
+
+                // We will not receive any records since the transaction for Message 2 was aborted. Wait for the
+                // aborted marker offset for Message 2 (3L) to be fetched and acknowledged by the consumer.
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(500));
+                    return pollRecords.count() == 0 && partitionExceptionMap.containsKey(tp) && partitionOffsetsMap.containsKey(tp) && partitionOffsetsMap.get(tp).contains(3L);

Review Comment:
   Yeah, we don't need to wait for an exception, so removed that check.
   Yes, it is true that control records will be acknowledged with the type `ARCHIVE` which means `REJECT` or `GAP`. The client has the required functionality to handle control records.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2030219266


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,182 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+        List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+        for (RecordBatch recordBatch : recordsToArchive) {
+            // Archive the offsets/batches in the cached state.
+            NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        }
+        return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+    }
+
+    /**
+     * This function filters out the offsets present in the acquired records list that are also a part of batches that need to be archived.
+     * It follows an iterative refinement of acquired records to eliminate batches to be archived.
+     * @param acquiredRecordsList The list containing acquired records. This list is sorted by the firstOffset of the acquired batch.
+     * @param batchesToArchive The list containing record batches to archive. This list is sorted by the baseOffset of the record batch.
+     * @return The list containing filtered acquired records offsets.
+     */
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecordsList,
+        List<RecordBatch> batchesToArchive
+    ) {
+        List<AcquiredRecords> result = new ArrayList<>();
+        for (AcquiredRecords acquiredRecords : acquiredRecordsList) {
+            List<AcquiredRecords> tempAcquiredRecordsList = new ArrayList<>();
+            tempAcquiredRecordsList.add(acquiredRecords);
+            for (RecordBatch batchToArchive : batchesToArchive) {

Review Comment:
   Hi @junrao , thanks for this approach. I agree that variables in this approach are more intuitive and it looks more readable. I have taken your changes and made a few changes to the comments (in line with the code).
   



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "AndrewJSchofield (via GitHub)" <gi...@apache.org>.
AndrewJSchofield commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2029317076


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,182 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+        List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+        for (RecordBatch recordBatch : recordsToArchive) {
+            // Archive the offsets/batches in the cached state.
+            NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        }
+        return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+    }
+
+    /**
+     * This function filters out the offsets present in the acquired records list that are also a part of batches that need to be archived.
+     * It follows an iterative refinement of acquired records to eliminate batches to be archived.
+     * @param acquiredRecordsList The list containing acquired records. This list is sorted by the firstOffset of the acquired batch.
+     * @param batchesToArchive The list containing record batches to archive. This list is sorted by the baseOffset of the record batch.
+     * @return The list containing filtered acquired records offsets.
+     */
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecordsList,
+        List<RecordBatch> batchesToArchive
+    ) {
+        List<AcquiredRecords> result = new ArrayList<>();
+        for (AcquiredRecords acquiredRecords : acquiredRecordsList) {
+            List<AcquiredRecords> tempAcquiredRecordsList = new ArrayList<>();

Review Comment:
   It's because the next iteration can divide the single AcquiredRecords, and then loop again. It's this iterative refinement that necessitates the list. It is true that the list is often only a single element.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2012532595


##########
server-common/src/main/java/org/apache/kafka/server/storage/log/FetchIsolation.java:
##########
@@ -25,11 +25,11 @@ public enum FetchIsolation {
     TXN_COMMITTED;
 
     public static FetchIsolation of(FetchRequest request) {
-        return of(request.replicaId(), request.isolationLevel());
+        return of(request.replicaId(), request.isolationLevel(), false);
     }
 
-    public static FetchIsolation of(int replicaId, IsolationLevel isolationLevel) {
-        if (!FetchRequest.isConsumer(replicaId)) {
+    public static FetchIsolation of(int replicaId, IsolationLevel isolationLevel, boolean isShareFetchRequest) {
+        if (!FetchRequest.isConsumer(replicaId) && !isShareFetchRequest) {

Review Comment:
   Hmmm, I am not sure if we should have `isShareFetchRequest` as a param. Shall there be any other replicaId for share consumers?
   
   cc: @junrao 
   
   ```
       public static final int ORDINARY_CONSUMER_ID = -1;
       public static final int DEBUGGING_CONSUMER_ID = -2;
       public static final int FUTURE_LOCAL_REPLICA_ID = -3;



##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/GroupConfig.java:
##########
@@ -83,6 +92,8 @@ public final class GroupConfig extends AbstractConfig {
 
     public final int streamsNumStandbyReplicas;
 
+    public final int shareIsolationLevel;

Review Comment:
   KIP says `Valid values "read_committed"  and "read_uncommitted" (default)` so why do we have the int value?



##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -104,7 +104,8 @@ class KafkaApis(val requestChannel: RequestChannel,
                 time: Time,
                 val tokenManager: DelegationTokenManager,
                 val apiVersionManager: ApiVersionManager,
-                val clientMetricsManager: ClientMetricsManager
+                val clientMetricsManager: ClientMetricsManager,
+                val groupConfigManager: GroupConfigManager

Review Comment:
   Though it's fine to have another manager in KafkaApis but would it be better to have `groupConfigManager` in `SharePartitionManager`? KafkaApis can pass default IsolationLevel using `FetchIsolation.of(-1, GroupConfig.defaultShareIsolationLevel, true)` and SharePartitionManager can check for updated IsolationLevel using below code. The final isolation level can go in `ShareFetch` class? This approach require less plumbing in KafkaApis but adds a isolation level param in `ShareFetch` which might be different from `FetchParams`. What do you think?
   
   ```
   val isolationLevel: IsolationLevel = IsolationLevel.forId(groupConfigManager.groupConfig(groupId).get().shareIsolationLevel().toByte)
             FetchIsolation.of(-1, isolationLevel, true)



##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/GroupConfig.java:
##########
@@ -65,6 +67,13 @@ public final class GroupConfig extends AbstractConfig {
 
     public static final String STREAMS_NUM_STANDBY_REPLICAS_CONFIG = "group.streams.num.standby.replicas";
 
+    public static final String SHARE_ISOLATION_LEVEL_CONFIG = "share.isolation.level";
+    public static final int SHARE_ISOLATION_LEVEL_DEFAULT = IsolationLevel.READ_UNCOMMITTED.id();
+    public static final String SHARE_ISOLATION_LEVEL_DOC = "Controls how to read records written transactionally. " +
+        "If set to \"read_committed\", the share group will only deliver transactional records which have been committed. " +
+        "If set to \"read_uncommitted\", the share group will return all messages, even transactional messages which have been aborted. " +
+        "Non-transactional records will be returned unconditionally in either mode.";

Review Comment:
   nit: would be good to align with share group configs above.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -847,6 +859,7 @@ public ShareAcquiredRecords acquire(
             }

Review Comment:
   Don't we need the filtering here as well? Do we cover this scenario in unit test?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2031757437


##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -6671,15 +6693,441 @@ private String assertionFailedMessage(SharePartition sharePartition, Map<Long, L
         return errorMessage.toString();
     }
 
+    @Test
+    public void testFilterRecordBatchesFromAcquiredRecords() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        List<AcquiredRecords> acquiredRecords1 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(5).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(10).setLastOffset(15).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches1 = List.of(
+            memoryRecordsBuilder(3, 2).build().batches().iterator().next(),
+            memoryRecordsBuilder(3, 12).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(1).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(5).setLastOffset(5).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(10).setLastOffset(11).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(15).setLastOffset(15).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)),
+            sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords1, recordBatches1));
+
+        List<AcquiredRecords> acquiredRecords2 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(5).setLastOffset(8).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(9).setLastOffset(30).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(31).setLastOffset(40).setDeliveryCount((short) 3)
+        );
+        List<RecordBatch> recordBatches2 = List.of(
+            memoryRecordsBuilder(21, 5).build().batches().iterator().next(),
+            memoryRecordsBuilder(5, 31).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+                new AcquiredRecords().setFirstOffset(26).setLastOffset(30).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(36).setLastOffset(40).setDeliveryCount((short) 3)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, recordBatches2)
+        );
+
+        // Record batches is empty.
+        assertEquals(acquiredRecords2, sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, List.of()));
+
+        List<AcquiredRecords> acquiredRecords3 = List.of(
+            new AcquiredRecords().setFirstOffset(0).setLastOffset(19).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches3 = List.of(
+            memoryRecordsBuilder(1, 8).build().batches().iterator().next(),
+            memoryRecordsBuilder(1, 18).build().batches().iterator().next()
+        );
+
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(0).setLastOffset(7).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(9).setLastOffset(17).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(19).setLastOffset(19).setDeliveryCount((short) 1)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords3, recordBatches3)
+        );
+    }
+
+    @Test
+    public void testAcquireWithReadCommittedIsolationLevel() {
+        SharePartition sharePartition = Mockito.spy(SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build());
+
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        memoryRecordsBuilder(buffer, 5, 10).close();
+        memoryRecordsBuilder(buffer, 5, 15).close();
+        memoryRecordsBuilder(buffer, 15, 20).close();
+        memoryRecordsBuilder(buffer, 8, 50).close();
+        memoryRecordsBuilder(buffer, 10, 58).close();
+        memoryRecordsBuilder(buffer, 5, 70).close();
+
+        buffer.flip();
+        MemoryRecords records = MemoryRecords.readableRecords(buffer);
+        FetchPartitionData fetchPartitionData = fetchPartitionData(records, newAbortedTransactions());
+
+        // We are mocking the result of function fetchAbortedTransactionRecordBatches. The records present at these offsets need to be archived.
+        // We won't be utilizing the aborted transactions passed in fetchPartitionData.
+        when(sharePartition.fetchAbortedTransactionRecordBatches(fetchPartitionData.records.batches(), fetchPartitionData.abortedTransactions.get())).thenReturn(
+            List.of(
+                memoryRecordsBuilder(5, 10).build().batches().iterator().next(),
+                memoryRecordsBuilder(10, 58).build().batches().iterator().next(),
+                memoryRecordsBuilder(5, 70).build().batches().iterator().next()
+            )
+        );
+
+        List<AcquiredRecords> acquiredRecordsList = fetchAcquiredRecords(
+            sharePartition.acquire(
+                MEMBER_ID,
+                10 /* Batch size */,
+                100,
+                DEFAULT_FETCH_OFFSET,
+                fetchPartitionData,
+                FetchIsolation.TXN_COMMITTED),
+            45 /* Gap of 15 records will be added to second batch, gap of 2 records will also be added to fourth batch */);
+
+        assertEquals(List.of(
+            new AcquiredRecords().setFirstOffset(15).setLastOffset(19).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(49).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(50).setLastOffset(57).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(68).setLastOffset(69).setDeliveryCount((short) 1)
+        ), acquiredRecordsList);
+        assertEquals(75, sharePartition.nextFetchOffset());
+
+        // Checking cached state.
+        assertEquals(4, sharePartition.cachedState().size());
+        assertTrue(sharePartition.cachedState().containsKey(10L));
+        assertTrue(sharePartition.cachedState().containsKey(20L));
+        assertTrue(sharePartition.cachedState().containsKey(50L));
+        assertTrue(sharePartition.cachedState().containsKey(70L));
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState());
+
+        assertEquals(19L, sharePartition.cachedState().get(10L).lastOffset());
+        assertEquals(49L, sharePartition.cachedState().get(20L).lastOffset());
+        assertEquals(69L, sharePartition.cachedState().get(50L).lastOffset());
+        assertEquals(74L, sharePartition.cachedState().get(70L).lastOffset());
+
+        assertEquals(RecordState.ACQUIRED, sharePartition.cachedState().get(20L).batchState());
+        assertEquals(RecordState.ARCHIVED, sharePartition.cachedState().get(70L).batchState());
+
+        assertEquals(MEMBER_ID, sharePartition.cachedState().get(20L).batchMemberId());
+        assertEquals(EMPTY_MEMBER_ID, sharePartition.cachedState().get(70L).batchMemberId());
+
+        assertNotNull(sharePartition.cachedState().get(20L).batchAcquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(70L).batchAcquisitionLockTimeoutTask());
+
+        Map<Long, InFlightState> expectedOffsetStateMap = new HashMap<>();
+        expectedOffsetStateMap.put(10L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(11L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(12L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(13L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(14L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(15L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(16L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(17L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(18L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(19L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        assertEquals(expectedOffsetStateMap, sharePartition.cachedState().get(10L).offsetState());
+
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(10L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(11L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(12L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(13L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(14L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(15L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(16L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(17L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(18L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(19L).acquisitionLockTimeoutTask());
+
+        expectedOffsetStateMap = new HashMap<>();
+        expectedOffsetStateMap.put(50L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(51L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(52L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(53L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(54L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(55L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(56L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(57L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(58L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(59L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(60L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(61L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(62L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(63L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(64L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(65L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(66L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(67L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(68L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(69L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        assertEquals(expectedOffsetStateMap, sharePartition.cachedState().get(50L).offsetState());
+
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(50L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(51L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(52L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(53L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(54L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(55L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(56L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(57L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(58L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(59L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(60L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(61L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(62L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(63L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(64L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(65L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(66L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(67L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(68L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(69L).acquisitionLockTimeoutTask());
+    }
+
+    @Test
+    public void testContainsAbortMarker() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        // Record batch is not a control batch.
+        RecordBatch recordBatch = mock(RecordBatch.class);
+        when(recordBatch.isControlBatch()).thenReturn(false);
+        assertFalse(sharePartition.containsAbortMarker(recordBatch));
+
+        // Record batch is a control batch but doesn't contain any records..
+        recordBatch = mock(RecordBatch.class);
+        Iterator batchIterator = mock(Iterator.class);
+        when(batchIterator.hasNext()).thenReturn(false);
+        when(recordBatch.iterator()).thenReturn(batchIterator);
+        when(recordBatch.isControlBatch()).thenReturn(true);
+        assertFalse(sharePartition.containsAbortMarker(recordBatch));
+
+        // Record batch is a control batch which contains a record of type ControlRecordType.ABORT.
+        recordBatch = mock(RecordBatch.class);
+        batchIterator = mock(Iterator.class);
+        when(batchIterator.hasNext()).thenReturn(true);
+        DefaultRecord record = mock(DefaultRecord.class);
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        // Buffer has to be created in a way that ControlRecordType.parse(buffer) returns ControlRecordType.ABORT.
+        buffer.putShort((short) 5);
+        buffer.putShort(ControlRecordType.ABORT.type());
+        buffer.putInt(23432); // some field added in version 5
+        buffer.flip();
+        when(record.key()).thenReturn(buffer);
+        when(batchIterator.next()).thenReturn(record);
+        when(recordBatch.iterator()).thenReturn(batchIterator);
+        when(recordBatch.isControlBatch()).thenReturn(true);
+        assertTrue(sharePartition.containsAbortMarker(recordBatch));
+
+        // Record batch is a control batch which contains a record of type ControlRecordType.COMMIT.
+        recordBatch = mock(RecordBatch.class);
+        batchIterator = mock(Iterator.class);
+        when(batchIterator.hasNext()).thenReturn(true);
+        record = mock(DefaultRecord.class);
+        buffer = ByteBuffer.allocate(4096);
+        // Buffer has to be created in a way that ControlRecordType.parse(buffer) returns ControlRecordType.COMMIT.
+        buffer.putShort((short) 5);
+        buffer.putShort(ControlRecordType.COMMIT.type());
+        buffer.putInt(23432); // some field added in version 5
+        buffer.flip();
+        when(record.key()).thenReturn(buffer);
+        when(batchIterator.next()).thenReturn(record);
+        when(recordBatch.iterator()).thenReturn(batchIterator);
+        when(recordBatch.isControlBatch()).thenReturn(true);
+        assertFalse(sharePartition.containsAbortMarker(recordBatch));
+    }
+
+    @Test
+    public void testFetchAbortedTransactionRecordBatchesForOnlyAbortedTransactions() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        // Case 1 - Creating 10 transactional records in a single batch followed by a ABORT marker record for producerId 1.
+        ByteBuffer buffer = ByteBuffer.allocate(1024);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 10, 1, 0);
+        buffer.flip();
+        Records records = MemoryRecords.readableRecords(buffer);
+
+        List<FetchResponseData.AbortedTransaction> abortedTransactions = List.of(
+            new FetchResponseData.AbortedTransaction().setFirstOffset(0).setProducerId(1)
+        );
+        // records from 0 to 9 should be archived because they are a part of aborted transactions.
+        List<RecordBatch> actual = sharePartition.fetchAbortedTransactionRecordBatches(records.batches(), abortedTransactions);
+        assertEquals(1, actual.size());
+        assertEquals(0, actual.get(0).baseOffset());
+        assertEquals(9, actual.get(0).lastOffset());
+        assertEquals(1, actual.get(0).producerId());
+
+        // Case 2: 3 individual batches each followed by a ABORT marker record for producerId 1.
+        buffer = ByteBuffer.allocate(1024);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 1, 1, 0);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 1, 1, 2);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 1, 1, 4);
+        buffer.flip();
+        records = MemoryRecords.readableRecords(buffer);
+        abortedTransactions = List.of(
+            new FetchResponseData.AbortedTransaction().setFirstOffset(0).setProducerId(1),
+            new FetchResponseData.AbortedTransaction().setFirstOffset(2).setProducerId(1),
+            new FetchResponseData.AbortedTransaction().setFirstOffset(4).setProducerId(1)
+        );
+
+        actual = sharePartition.fetchAbortedTransactionRecordBatches(records.batches(), abortedTransactions);
+        assertEquals(3, actual.size());
+        assertEquals(0, actual.get(0).baseOffset());
+        assertEquals(0, actual.get(0).lastOffset());
+        assertEquals(1, actual.get(0).producerId());
+        assertEquals(2, actual.get(1).baseOffset());
+        assertEquals(2, actual.get(1).lastOffset());
+        assertEquals(1, actual.get(1).producerId());
+        assertEquals(4, actual.get(2).baseOffset());
+        assertEquals(4, actual.get(2).lastOffset());
+        assertEquals(1, actual.get(2).producerId());
+
+        // Case 3: The producer id of records is different, so they should not be archived,
+        buffer = ByteBuffer.allocate(1024);
+        // We are creating 10 transactional records followed by a ABORT marker record for producerId 2.
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 10, 2, 0);
+        buffer.flip();
+        records = MemoryRecords.readableRecords(buffer);
+        abortedTransactions = List.of(
+            new FetchResponseData.AbortedTransaction().setFirstOffset(0).setProducerId(1)
+        );
+
+        actual = sharePartition.fetchAbortedTransactionRecordBatches(records.batches(), abortedTransactions);
+        assertEquals(0, actual.size());
+    }
+
+    @Test
+    public void testFetchAbortedTransactionRecordBatchesForAbortedAndCommittedTransactions() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        ByteBuffer buffer = ByteBuffer.allocate(1024);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 1, 0);
+        newTransactionalRecords(buffer, ControlRecordType.COMMIT, 2, 2, 3);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 2, 6);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 1, 9);
+        newTransactionalRecords(buffer, ControlRecordType.COMMIT, 2, 1, 12);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 1, 15);
+        buffer.flip();
+        Records records = MemoryRecords.readableRecords(buffer);
+
+        // Case 1 - Aborted transactions does not contain the record batch from 4-5 with producer id 2.

Review Comment:
   Corrected the comment to `Case 1 - Aborted transactions does not contain the record batch from offsets 6-7 with producer id 2.` I added this item `new FetchResponseData.AbortedTransaction().setFirstOffset(6).setProducerId(1)` to `abortedTransactions` to show that even if offsets 6-7 is mentioned under `abortedTransactions`, they won't get filtered out  for archiving since they have a different `producerId`.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2017977111


##########
server-common/src/main/java/org/apache/kafka/server/storage/log/FetchIsolation.java:
##########
@@ -25,11 +25,11 @@ public enum FetchIsolation {
     TXN_COMMITTED;
 
     public static FetchIsolation of(FetchRequest request) {
-        return of(request.replicaId(), request.isolationLevel());
+        return of(request.replicaId(), request.isolationLevel(), false);
     }
 
-    public static FetchIsolation of(int replicaId, IsolationLevel isolationLevel) {
-        if (!FetchRequest.isConsumer(replicaId)) {
+    public static FetchIsolation of(int replicaId, IsolationLevel isolationLevel, boolean isShareFetchRequest) {
+        if (!FetchRequest.isConsumer(replicaId) && !isShareFetchRequest) {

Review Comment:
   I have made the code change to use `replicaId` as -1 and removed `isShareFetchRequest` param.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013699771


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions == null || abortedTransactions.isEmpty())
+                return null;
+
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = new PriorityQueue<>(
+                abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+            );
+            abortedTransactionsHeap.addAll(abortedTransactions);
+            return abortedTransactionsHeap;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        lock.writeLock().lock();
+        try {
+            return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        lock.writeLock().lock();

Review Comment:
   you're right, we don't need it.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022231678


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {

Review Comment:
   I can introduce a method parameter `RecordState initialState` and make it a common function. Done this in my latest commit.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {

Review Comment:
   I can introduce a method parameter `RecordState initialState` and make it a common function. Done this in my latest commit.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on PR #19261:
URL: https://github.com/apache/kafka/pull/19261#issuecomment-2768671007

   > Are all scenarios covered in SharePartition unit tests?
   
   Hi @apoorvmittal10, I have added the unit tests for different cases of acquire (cached state can be empty/acquire full batch/acquire subset batch). I think a better way to test the added code was through integration tests, hence there I have added plenty in `ShareConsumerTest` since I can mimic transactions and there the code to process aborted transactions is also being tested. Please let me know if this doesn't look enough and I should add more?
   cc - @AndrewJSchofield 


-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022495006


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2504,205 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), RecordState.ACQUIRED);
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteBatch(inFlightBatch, RecordState.ACQUIRED);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * This function fetches the sub map from cachedState where all the offset details present in the recordBatch can be referred to
+     * OR it gives an exception if those offsets are not present in cachedState.
+     * @param recordBatch The record batch for which we want to find the sub map.
+     * @return the sub map containing all the offset details.
+     */
+    private NavigableMap<Long, InFlightBatch> fetchSubMapOrException(RecordBatch recordBatch) {
+        lock.readLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorEntry = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorEntry == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorEntry.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = orderedAbortedTransactions(abortedTransactions);
+        Set<Long> abortedProducerIds = new HashSet<>();
+        List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+        for (RecordBatch currentBatch : batches) {
+            if (currentBatch.hasProducerId()) {
+                // remove from the aborted transactions queue, all aborted transactions which have begun before the
+                // current batch's last offset and add the associated producerIds to the aborted producer set.
+                while (!orderedAbortedTransactions.isEmpty() && orderedAbortedTransactions.peek().firstOffset() <= currentBatch.lastOffset()) {
+                    FetchResponseData.AbortedTransaction abortedTransaction = orderedAbortedTransactions.poll();
+                    abortedProducerIds.add(abortedTransaction.producerId());
+                }
+                long producerId = currentBatch.producerId();
+                if (containsAbortMarker(currentBatch)) {
+                    abortedProducerIds.remove(producerId);
+                } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                    log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                        "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                    recordsToArchive.add(currentBatch);
+                }
+            }
+        }
+        return recordsToArchive;
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = new PriorityQueue<>(
+            abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+        );
+        orderedAbortedTransactions.addAll(abortedTransactions);
+        return orderedAbortedTransactions;
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        if (!batch.isControlBatch())
+            return false;
+
+        Iterator<Record> batchIterator = batch.iterator();
+        if (!batchIterator.hasNext())
+            return false;
+
+        Record firstRecord = batchIterator.next();
+        return ControlRecordType.ABORT == ControlRecordType.parse(firstRecord.key());
+    }

Review Comment:
   > You won't find any unit tests for these functions there. That's why I've avoided them here as well.
   
   It must be a miss there that doesn't mean we should avoid them too. There should be tests for the code we are writing to make sure it works as expected.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2026362292


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();

Review Comment:
   agreed, we don't need it.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2031745262


##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -6671,15 +6693,441 @@ private String assertionFailedMessage(SharePartition sharePartition, Map<Long, L
         return errorMessage.toString();
     }
 
+    @Test
+    public void testFilterRecordBatchesFromAcquiredRecords() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        List<AcquiredRecords> acquiredRecords1 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(5).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(10).setLastOffset(15).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches1 = List.of(
+            memoryRecordsBuilder(3, 2).build().batches().iterator().next(),
+            memoryRecordsBuilder(3, 12).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(1).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(5).setLastOffset(5).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(10).setLastOffset(11).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(15).setLastOffset(15).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)),
+            sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords1, recordBatches1));
+
+        List<AcquiredRecords> acquiredRecords2 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(5).setLastOffset(8).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(9).setLastOffset(30).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(31).setLastOffset(40).setDeliveryCount((short) 3)
+        );
+        List<RecordBatch> recordBatches2 = List.of(
+            memoryRecordsBuilder(21, 5).build().batches().iterator().next(),
+            memoryRecordsBuilder(5, 31).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+                new AcquiredRecords().setFirstOffset(26).setLastOffset(30).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(36).setLastOffset(40).setDeliveryCount((short) 3)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, recordBatches2)
+        );
+
+        // Record batches is empty.
+        assertEquals(acquiredRecords2, sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, List.of()));
+
+        List<AcquiredRecords> acquiredRecords3 = List.of(
+            new AcquiredRecords().setFirstOffset(0).setLastOffset(19).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches3 = List.of(
+            memoryRecordsBuilder(1, 8).build().batches().iterator().next(),
+            memoryRecordsBuilder(1, 18).build().batches().iterator().next()
+        );
+
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(0).setLastOffset(7).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(9).setLastOffset(17).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(19).setLastOffset(19).setDeliveryCount((short) 1)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords3, recordBatches3)
+        );
+    }
+
+    @Test
+    public void testAcquireWithReadCommittedIsolationLevel() {
+        SharePartition sharePartition = Mockito.spy(SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build());
+
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        memoryRecordsBuilder(buffer, 5, 10).close();
+        memoryRecordsBuilder(buffer, 5, 15).close();
+        memoryRecordsBuilder(buffer, 15, 20).close();
+        memoryRecordsBuilder(buffer, 8, 50).close();
+        memoryRecordsBuilder(buffer, 10, 58).close();
+        memoryRecordsBuilder(buffer, 5, 70).close();
+
+        buffer.flip();
+        MemoryRecords records = MemoryRecords.readableRecords(buffer);
+        FetchPartitionData fetchPartitionData = fetchPartitionData(records, newAbortedTransactions());
+
+        // We are mocking the result of function fetchAbortedTransactionRecordBatches. The records present at these offsets need to be archived.
+        // We won't be utilizing the aborted transactions passed in fetchPartitionData.
+        when(sharePartition.fetchAbortedTransactionRecordBatches(fetchPartitionData.records.batches(), fetchPartitionData.abortedTransactions.get())).thenReturn(
+            List.of(
+                memoryRecordsBuilder(5, 10).build().batches().iterator().next(),
+                memoryRecordsBuilder(10, 58).build().batches().iterator().next(),
+                memoryRecordsBuilder(5, 70).build().batches().iterator().next()
+            )
+        );
+
+        List<AcquiredRecords> acquiredRecordsList = fetchAcquiredRecords(
+            sharePartition.acquire(
+                MEMBER_ID,
+                10 /* Batch size */,
+                100,
+                DEFAULT_FETCH_OFFSET,
+                fetchPartitionData,
+                FetchIsolation.TXN_COMMITTED),
+            45 /* Gap of 15 records will be added to second batch, gap of 2 records will also be added to fourth batch */);
+
+        assertEquals(List.of(
+            new AcquiredRecords().setFirstOffset(15).setLastOffset(19).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(49).setDeliveryCount((short) 1),

Review Comment:
   This is because there is a gap of records from 35-49 in the original records to be acquired. This gap is added to this batch since we rely on the client to inform the broker about these natural gaps in the partition log.
   ```
   memoryRecordsBuilder(buffer, 5, 10).close(); // batch from 10-14
   memoryRecordsBuilder(buffer, 5, 15).close(); // batch from 15-19
   memoryRecordsBuilder(buffer, 15, 20).close(); // batch from 20-34
   memoryRecordsBuilder(buffer, 8, 50).close();  // batch from 50-57
   ```



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013699195


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions == null || abortedTransactions.isEmpty())
+                return null;
+
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = new PriorityQueue<>(
+                abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+            );
+            abortedTransactionsHeap.addAll(abortedTransactions);
+            return abortedTransactionsHeap;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        lock.writeLock().lock();
+        try {
+            return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        lock.writeLock().lock();
+        try {
+            if (!batch.isControlBatch())
+                return false;
+
+            Iterator<Record> batchIterator = batch.iterator();
+            if (!batchIterator.hasNext())
+                return false;
+
+            Record firstRecord = batchIterator.next();
+            return ControlRecordType.ABORT == ControlRecordType.parse(firstRecord.key());

Review Comment:
   I have replicated the code what is present in the regular consumer client for this case. When we do `batch.isControlBatch()`, we only read the batch header and not the entire record and we are doing a check on `record.key()` which involves reading the size of the key AFAIK. Not sure if there is a better way to do it.
   cc - @AndrewJSchofield 



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013704465


##########
server-common/src/main/java/org/apache/kafka/server/storage/log/FetchIsolation.java:
##########
@@ -25,11 +25,11 @@ public enum FetchIsolation {
     TXN_COMMITTED;
 
     public static FetchIsolation of(FetchRequest request) {
-        return of(request.replicaId(), request.isolationLevel());
+        return of(request.replicaId(), request.isolationLevel(), false);
     }
 
-    public static FetchIsolation of(int replicaId, IsolationLevel isolationLevel) {
-        if (!FetchRequest.isConsumer(replicaId)) {
+    public static FetchIsolation of(int replicaId, IsolationLevel isolationLevel, boolean isShareFetchRequest) {
+        if (!FetchRequest.isConsumer(replicaId) && !isShareFetchRequest) {

Review Comment:
   I think if I just use `replicaId` as -1, that also works. But I wasn't too sure if that's the right approach. Hence, opted for adding a new variable



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2021161644


##########
core/src/main/java/kafka/server/share/SharePartitionManager.java:
##########
@@ -267,8 +268,20 @@ public CompletableFuture<Map<TopicIdPartition, PartitionData>> fetchMessages(
             .rotate(topicIdPartitions, new PartitionRotateMetadata(sessionEpoch));
 
         CompletableFuture<Map<TopicIdPartition, PartitionData>> future = new CompletableFuture<>();
-        processShareFetch(new ShareFetch(fetchParams, groupId, memberId, future, rotatedTopicIdPartitions, batchSize, maxFetchRecords, brokerTopicStats));
-
+        if (groupConfigManager.groupConfig(groupId).isEmpty()) {
+            processShareFetch(new ShareFetch(fetchParams, groupId, memberId, future, rotatedTopicIdPartitions, batchSize, maxFetchRecords, brokerTopicStats));
+        } else {
+            FetchParams updatedFetchParams = new FetchParams(
+                fetchParams.replicaId,
+                fetchParams.replicaEpoch,
+                fetchParams.maxWaitMs,
+                fetchParams.minBytes,
+                fetchParams.maxBytes,
+                FetchIsolation.of(-1, groupConfigManager.groupConfig(groupId).get().shareIsolationLevel()),

Review Comment:
   @AndrewJSchofield, earlier I had thought to use `isShareFetchRequest` as a parameter within `FetchIsolation.of`. That idea did not go well as per discussion https://github.com/apache/kafka/pull/19261#discussion_r2012532595. Do you think I should add a new variable like below in `FetchRequest.java` or `ShareFetchRequest.java`?
   `public static final int SHARE_CONSUMER_ID = -4;`



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "AndrewJSchofield (via GitHub)" <gi...@apache.org>.
AndrewJSchofield commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2021223900


##########
core/src/main/java/kafka/server/share/SharePartitionManager.java:
##########
@@ -267,8 +268,20 @@ public CompletableFuture<Map<TopicIdPartition, PartitionData>> fetchMessages(
             .rotate(topicIdPartitions, new PartitionRotateMetadata(sessionEpoch));
 
         CompletableFuture<Map<TopicIdPartition, PartitionData>> future = new CompletableFuture<>();
-        processShareFetch(new ShareFetch(fetchParams, groupId, memberId, future, rotatedTopicIdPartitions, batchSize, maxFetchRecords, brokerTopicStats));
-
+        if (groupConfigManager.groupConfig(groupId).isEmpty()) {
+            processShareFetch(new ShareFetch(fetchParams, groupId, memberId, future, rotatedTopicIdPartitions, batchSize, maxFetchRecords, brokerTopicStats));
+        } else {
+            FetchParams updatedFetchParams = new FetchParams(
+                fetchParams.replicaId,
+                fetchParams.replicaEpoch,
+                fetchParams.maxWaitMs,
+                fetchParams.minBytes,
+                fetchParams.maxBytes,
+                FetchIsolation.of(-1, groupConfigManager.groupConfig(groupId).get().shareIsolationLevel()),

Review Comment:
   I think what I object to is using a bare `-1` when there is a specific constant name for `-1` already in existence. I believe that this parameter is strictly the replica ID, not really a consumer ID, but it's all a bit mixed up in `FetchRequest`. We certainly could introduce a `SHARE_CONSUMER_REPLICA_ID` I suppose, but if it's really just used for this situation, masquerading as a regular consumer with `CONSUMER_REPLICA_ID` seems fine to me.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2026360050


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();

Review Comment:
   agreed, we don't need it.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022231678


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {

Review Comment:
   I can introduce a method parameter `RecordState initialState` and make it a common function.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {

Review Comment:
   `archiveAvailableRecords` is quite different than `archiveAcquiredBatchRecords` in several ways- 
   1. `archiveAcquiredBatchRecords` doesn't need a return type unlike `archiveAvailableRecords`
   2. `archiveAvailableRecords` requires more checks than what are needed.
   
   Hence, I prefer keeping them as separate rather than adding more if else statements.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2023209778


##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -3239,7 +3239,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         shareFetchRequest.maxWait,
         fetchMinBytes,
         fetchMaxBytes,
-        FetchIsolation.HIGH_WATERMARK,
+        FetchIsolation.of(FetchRequest.CONSUMER_REPLICA_ID, GroupConfig.defaultShareIsolationLevel),

Review Comment:
   I have moved back the resolution of fetch isolation level to `KafkaApis` from `SharePartitionManager`



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013454652


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions

Review Comment:
   Are there any guarantees for `abortedTransactions` to be in some defined order with `firstOffset` for the RecordBatch?



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(

Review Comment:
   Shouldn't the method name be consistent with earlier method `filterAbortedTransactionalAcquiredRecords` as we are sending the `acquired` records list here as well. Hence earlier method can have `maybe` prefix and this one as `filterAbortedTransactionalAcquiredRecords`.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {

Review Comment:
   ```suggestion
       private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
   ```



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any

Review Comment:
   Why not to have the below check in the same method and pass the aborted transactions to `filterAbortedTransactionalRecords` without optional. This can make sure that when it's required to filter then only call goes to further methods, and maybe method should have all these pre-checks.
   
   ```
   if (abortedTransactions.isEmpty())
                   return acquiredRecords;



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022198417


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();

Review Comment:
   you're right, we don't require it.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022230974


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {

Review Comment:
   i can introduce a method parameter `RecordState initialState` and make it a common function.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {

Review Comment:
   I can introduce a method parameter `RecordState initialState` and make it a common function.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on PR #19261:
URL: https://github.com/apache/kafka/pull/19261#issuecomment-2769956735

   Hi @AndrewJSchofield @apoorvmittal10 , the PR is ready for review. Sorry for a force-push, I had to do it since my git history had become corrupt due to a problematic merge. Because of that, there are a lot more labels to this PR than required. Can you please remove the extra labels and re-review my 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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "junrao (via GitHub)" <gi...@apache.org>.
junrao commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2031937222


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);

Review Comment:
   Does this force the test to wait for 2.5 secs since it reads 0 record?



##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);
+                assertEquals(0, records.count());
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                // Message 3 would be returned by this poll.
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 3", new String(record.value()));
+                // We will make Message 3 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // Precautionary poll so that the aborted transaction for Message 4 is fetched by the consumer.
+                shareConsumer.poll(Duration.ofMillis(5000));

Review Comment:
   > Since this is a poll to make sure aborted transaction for message 4 is fetched by the consumer, it won't return m any records.
   
   Since the `poll()` call doesn't return any record, we are forced to wait for 2.5 secs, right? Also, how do we know for sure that message 4 is fetched by the consumer?



##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -6669,15 +6691,441 @@ private String assertionFailedMessage(SharePartition sharePartition, Map<Long, L
         return errorMessage.toString();
     }
 
+    @Test
+    public void testFilterRecordBatchesFromAcquiredRecords() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        List<AcquiredRecords> acquiredRecords1 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(5).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(10).setLastOffset(15).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches1 = List.of(
+            memoryRecordsBuilder(3, 2).build().batches().iterator().next(),
+            memoryRecordsBuilder(3, 12).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(1).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(5).setLastOffset(5).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(10).setLastOffset(11).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(15).setLastOffset(15).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)),
+            sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords1, recordBatches1));
+
+        List<AcquiredRecords> acquiredRecords2 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(5).setLastOffset(8).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(9).setLastOffset(30).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(31).setLastOffset(40).setDeliveryCount((short) 3)
+        );
+        List<RecordBatch> recordBatches2 = List.of(
+            memoryRecordsBuilder(21, 5).build().batches().iterator().next(),
+            memoryRecordsBuilder(5, 31).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+                new AcquiredRecords().setFirstOffset(26).setLastOffset(30).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(36).setLastOffset(40).setDeliveryCount((short) 3)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, recordBatches2)
+        );
+
+        // Record batches is empty.
+        assertEquals(acquiredRecords2, sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, List.of()));
+
+        List<AcquiredRecords> acquiredRecords3 = List.of(
+            new AcquiredRecords().setFirstOffset(0).setLastOffset(19).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches3 = List.of(
+            memoryRecordsBuilder(1, 8).build().batches().iterator().next(),
+            memoryRecordsBuilder(1, 18).build().batches().iterator().next()
+        );
+
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(0).setLastOffset(7).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(9).setLastOffset(17).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(19).setLastOffset(19).setDeliveryCount((short) 1)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords3, recordBatches3)
+        );
+    }
+
+    @Test
+    public void testAcquireWithReadCommittedIsolationLevel() {
+        SharePartition sharePartition = Mockito.spy(SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build());
+
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        memoryRecordsBuilder(buffer, 5, 10).close();
+        memoryRecordsBuilder(buffer, 5, 15).close();
+        memoryRecordsBuilder(buffer, 15, 20).close();
+        memoryRecordsBuilder(buffer, 8, 50).close();
+        memoryRecordsBuilder(buffer, 10, 58).close();
+        memoryRecordsBuilder(buffer, 5, 70).close();
+
+        buffer.flip();
+        MemoryRecords records = MemoryRecords.readableRecords(buffer);
+        FetchPartitionData fetchPartitionData = fetchPartitionData(records, newAbortedTransactions());
+
+        // We are mocking the result of function fetchAbortedTransactionRecordBatches. The records present at these offsets need to be archived.
+        // We won't be utilizing the aborted transactions passed in fetchPartitionData.
+        when(sharePartition.fetchAbortedTransactionRecordBatches(fetchPartitionData.records.batches(), fetchPartitionData.abortedTransactions.get())).thenReturn(
+            List.of(
+                memoryRecordsBuilder(5, 10).build().batches().iterator().next(),
+                memoryRecordsBuilder(10, 58).build().batches().iterator().next(),
+                memoryRecordsBuilder(5, 70).build().batches().iterator().next()
+            )
+        );
+
+        List<AcquiredRecords> acquiredRecordsList = fetchAcquiredRecords(
+            sharePartition.acquire(
+                MEMBER_ID,
+                10 /* Batch size */,
+                100,
+                DEFAULT_FETCH_OFFSET,
+                fetchPartitionData,
+                FetchIsolation.TXN_COMMITTED),
+            45 /* Gap of 15 records will be added to second batch, gap of 2 records will also be added to fourth batch */);
+
+        assertEquals(List.of(
+            new AcquiredRecords().setFirstOffset(15).setLastOffset(19).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(49).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(50).setLastOffset(57).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(68).setLastOffset(69).setDeliveryCount((short) 1)
+        ), acquiredRecordsList);
+        assertEquals(75, sharePartition.nextFetchOffset());
+
+        // Checking cached state.
+        assertEquals(4, sharePartition.cachedState().size());
+        assertTrue(sharePartition.cachedState().containsKey(10L));
+        assertTrue(sharePartition.cachedState().containsKey(20L));
+        assertTrue(sharePartition.cachedState().containsKey(50L));
+        assertTrue(sharePartition.cachedState().containsKey(70L));
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState());
+
+        assertEquals(19L, sharePartition.cachedState().get(10L).lastOffset());
+        assertEquals(49L, sharePartition.cachedState().get(20L).lastOffset());
+        assertEquals(69L, sharePartition.cachedState().get(50L).lastOffset());
+        assertEquals(74L, sharePartition.cachedState().get(70L).lastOffset());
+
+        assertEquals(RecordState.ACQUIRED, sharePartition.cachedState().get(20L).batchState());
+        assertEquals(RecordState.ARCHIVED, sharePartition.cachedState().get(70L).batchState());
+
+        assertEquals(MEMBER_ID, sharePartition.cachedState().get(20L).batchMemberId());
+        assertEquals(EMPTY_MEMBER_ID, sharePartition.cachedState().get(70L).batchMemberId());
+
+        assertNotNull(sharePartition.cachedState().get(20L).batchAcquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(70L).batchAcquisitionLockTimeoutTask());
+
+        Map<Long, InFlightState> expectedOffsetStateMap = new HashMap<>();
+        expectedOffsetStateMap.put(10L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(11L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(12L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(13L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(14L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(15L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(16L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(17L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(18L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(19L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        assertEquals(expectedOffsetStateMap, sharePartition.cachedState().get(10L).offsetState());
+
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(10L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(11L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(12L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(13L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(14L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(15L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(16L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(17L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(18L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(19L).acquisitionLockTimeoutTask());
+
+        expectedOffsetStateMap = new HashMap<>();
+        expectedOffsetStateMap.put(50L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(51L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(52L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(53L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(54L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(55L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(56L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(57L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(58L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(59L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(60L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(61L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(62L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(63L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(64L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(65L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(66L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(67L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(68L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(69L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        assertEquals(expectedOffsetStateMap, sharePartition.cachedState().get(50L).offsetState());
+
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(50L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(51L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(52L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(53L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(54L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(55L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(56L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(57L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(58L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(59L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(60L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(61L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(62L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(63L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(64L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(65L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(66L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(67L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(68L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(69L).acquisitionLockTimeoutTask());
+    }
+
+    @Test
+    public void testContainsAbortMarker() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        // Record batch is not a control batch.
+        RecordBatch recordBatch = mock(RecordBatch.class);
+        when(recordBatch.isControlBatch()).thenReturn(false);
+        assertFalse(sharePartition.containsAbortMarker(recordBatch));
+
+        // Record batch is a control batch but doesn't contain any records.
+        recordBatch = mock(RecordBatch.class);
+        Iterator batchIterator = mock(Iterator.class);
+        when(batchIterator.hasNext()).thenReturn(false);
+        when(recordBatch.iterator()).thenReturn(batchIterator);
+        when(recordBatch.isControlBatch()).thenReturn(true);
+        assertFalse(sharePartition.containsAbortMarker(recordBatch));
+
+        // Record batch is a control batch which contains a record of type ControlRecordType.ABORT.
+        recordBatch = mock(RecordBatch.class);
+        batchIterator = mock(Iterator.class);
+        when(batchIterator.hasNext()).thenReturn(true);
+        DefaultRecord record = mock(DefaultRecord.class);
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        // Buffer has to be created in a way that ControlRecordType.parse(buffer) returns ControlRecordType.ABORT.
+        buffer.putShort((short) 5);
+        buffer.putShort(ControlRecordType.ABORT.type());
+        buffer.putInt(23432); // some field added in version 5
+        buffer.flip();
+        when(record.key()).thenReturn(buffer);
+        when(batchIterator.next()).thenReturn(record);
+        when(recordBatch.iterator()).thenReturn(batchIterator);
+        when(recordBatch.isControlBatch()).thenReturn(true);
+        assertTrue(sharePartition.containsAbortMarker(recordBatch));
+
+        // Record batch is a control batch which contains a record of type ControlRecordType.COMMIT.
+        recordBatch = mock(RecordBatch.class);
+        batchIterator = mock(Iterator.class);
+        when(batchIterator.hasNext()).thenReturn(true);
+        record = mock(DefaultRecord.class);
+        buffer = ByteBuffer.allocate(4096);
+        // Buffer has to be created in a way that ControlRecordType.parse(buffer) returns ControlRecordType.COMMIT.
+        buffer.putShort((short) 5);
+        buffer.putShort(ControlRecordType.COMMIT.type());
+        buffer.putInt(23432); // some field added in version 5
+        buffer.flip();
+        when(record.key()).thenReturn(buffer);
+        when(batchIterator.next()).thenReturn(record);
+        when(recordBatch.iterator()).thenReturn(batchIterator);
+        when(recordBatch.isControlBatch()).thenReturn(true);
+        assertFalse(sharePartition.containsAbortMarker(recordBatch));
+    }
+
+    @Test
+    public void testFetchAbortedTransactionRecordBatchesForOnlyAbortedTransactions() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        // Case 1 - Creating 10 transactional records in a single batch followed by a ABORT marker record for producerId 1.
+        ByteBuffer buffer = ByteBuffer.allocate(1024);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 10, 1, 0);
+        buffer.flip();
+        Records records = MemoryRecords.readableRecords(buffer);
+
+        List<FetchResponseData.AbortedTransaction> abortedTransactions = List.of(
+            new FetchResponseData.AbortedTransaction().setFirstOffset(0).setProducerId(1)
+        );
+        // records from 0 to 9 should be archived because they are a part of aborted transactions.
+        List<RecordBatch> actual = sharePartition.fetchAbortedTransactionRecordBatches(records.batches(), abortedTransactions);
+        assertEquals(1, actual.size());
+        assertEquals(0, actual.get(0).baseOffset());
+        assertEquals(9, actual.get(0).lastOffset());
+        assertEquals(1, actual.get(0).producerId());
+
+        // Case 2: 3 individual batches each followed by a ABORT marker record for producerId 1.
+        buffer = ByteBuffer.allocate(1024);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 1, 1, 0);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 1, 1, 2);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 1, 1, 4);
+        buffer.flip();
+        records = MemoryRecords.readableRecords(buffer);
+        abortedTransactions = List.of(
+            new FetchResponseData.AbortedTransaction().setFirstOffset(0).setProducerId(1),
+            new FetchResponseData.AbortedTransaction().setFirstOffset(2).setProducerId(1),
+            new FetchResponseData.AbortedTransaction().setFirstOffset(4).setProducerId(1)
+        );
+
+        actual = sharePartition.fetchAbortedTransactionRecordBatches(records.batches(), abortedTransactions);
+        assertEquals(3, actual.size());
+        assertEquals(0, actual.get(0).baseOffset());
+        assertEquals(0, actual.get(0).lastOffset());
+        assertEquals(1, actual.get(0).producerId());
+        assertEquals(2, actual.get(1).baseOffset());
+        assertEquals(2, actual.get(1).lastOffset());
+        assertEquals(1, actual.get(1).producerId());
+        assertEquals(4, actual.get(2).baseOffset());
+        assertEquals(4, actual.get(2).lastOffset());
+        assertEquals(1, actual.get(2).producerId());
+
+        // Case 3: The producer id of records is different, so they should not be archived,
+        buffer = ByteBuffer.allocate(1024);
+        // We are creating 10 transactional records followed by a ABORT marker record for producerId 2.
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 10, 2, 0);
+        buffer.flip();
+        records = MemoryRecords.readableRecords(buffer);
+        abortedTransactions = List.of(
+            new FetchResponseData.AbortedTransaction().setFirstOffset(0).setProducerId(1)
+        );
+
+        actual = sharePartition.fetchAbortedTransactionRecordBatches(records.batches(), abortedTransactions);
+        assertEquals(0, actual.size());
+    }
+
+    @Test
+    public void testFetchAbortedTransactionRecordBatchesForAbortedAndCommittedTransactions() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        ByteBuffer buffer = ByteBuffer.allocate(1024);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 1, 0);
+        newTransactionalRecords(buffer, ControlRecordType.COMMIT, 2, 2, 3);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 2, 6);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 1, 9);
+        newTransactionalRecords(buffer, ControlRecordType.COMMIT, 2, 1, 12);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 1, 15);
+        buffer.flip();
+        Records records = MemoryRecords.readableRecords(buffer);
+
+        // Case 1 - Aborted transactions does not contain the record batch from offsets 6-7 with producer id 2.
+        List<FetchResponseData.AbortedTransaction> abortedTransactions = List.of(
+            new FetchResponseData.AbortedTransaction().setFirstOffset(0).setProducerId(1),
+            new FetchResponseData.AbortedTransaction().setFirstOffset(6).setProducerId(1),
+            new FetchResponseData.AbortedTransaction().setFirstOffset(9).setProducerId(1),
+            new FetchResponseData.AbortedTransaction().setFirstOffset(15).setProducerId(1)
+        );
+
+        List<RecordBatch> actual = sharePartition.fetchAbortedTransactionRecordBatches(records.batches(), abortedTransactions);
+        assertEquals(3, actual.size());
+        assertEquals(0, actual.get(0).baseOffset());
+        assertEquals(1, actual.get(0).lastOffset());
+        assertEquals(1, actual.get(0).producerId());
+        assertEquals(9, actual.get(1).baseOffset());
+        assertEquals(10, actual.get(1).lastOffset());
+        assertEquals(1, actual.get(1).producerId());
+        assertEquals(15, actual.get(2).baseOffset());
+        assertEquals(16, actual.get(2).lastOffset());
+        assertEquals(1, actual.get(2).producerId());
+
+        // Case 2 - Aborted transactions contains the record batch from 4-5 with producer id 2.

Review Comment:
   Should we fix this comment too?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "smjn (via GitHub)" <gi...@apache.org>.
smjn commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2024547661


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);
+                assertEquals(0, records.count());
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                // Message 3 would be returned by this poll.
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 3", new String(record.value()));
+                // We will make Message 3 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // Precautionary poll so that the aborted transaction for Message 4 is fetched by the consumer.
+                shareConsumer.poll(Duration.ofMillis(5000));
+
+                // We are altering IsolationLevel to READ_UNCOMMITTED now. We will read both committed/aborted transactions now.
+                alterShareIsolationLevel("group1", "read_uncommitted");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_UNCOMMITTED, we can consume Message 3 (committed transaction that was released), Message 5, Message 6, Message 7 and Message 8.
+                List<String> finalMessages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            finalMessages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return finalMessages.size() == 5;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", finalMessages.get(0));
+                assertEquals("Message 5", finalMessages.get(1));
+                assertEquals("Message 6", finalMessages.get(2));
+                assertEquals("Message 7", finalMessages.get(3));
+                assertEquals("Message 8", finalMessages.get(4));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithRejectAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);

Review Comment:
   This might not be enough. Ideally, we should call `waitForAssignment` to make sure something has been assigned as well as 0 records.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "junrao (via GitHub)" <gi...@apache.org>.
junrao commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2029290363


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,182 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+        List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+        for (RecordBatch recordBatch : recordsToArchive) {
+            // Archive the offsets/batches in the cached state.
+            NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        }
+        return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+    }
+
+    /**
+     * This function filters out the offsets present in the acquired records list that are also a part of batches that need to be archived.
+     * It follows an iterative refinement of acquired records to eliminate batches to be archived.
+     * @param acquiredRecordsList The list containing acquired records. This list is sorted by the firstOffset of the acquired batch.
+     * @param batchesToArchive The list containing record batches to archive. This list is sorted by the baseOffset of the record batch.
+     * @return The list containing filtered acquired records offsets.
+     */
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecordsList,
+        List<RecordBatch> batchesToArchive
+    ) {
+        List<AcquiredRecords> result = new ArrayList<>();
+        for (AcquiredRecords acquiredRecords : acquiredRecordsList) {
+            List<AcquiredRecords> tempAcquiredRecordsList = new ArrayList<>();

Review Comment:
   Hmm, I don't quite understand this. tempAcquiredRecordsList seems to always contain a single AcquiredRecords. Why does it need to be a list?



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,182 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+        List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+        for (RecordBatch recordBatch : recordsToArchive) {
+            // Archive the offsets/batches in the cached state.
+            NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        }
+        return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+    }
+
+    /**
+     * This function filters out the offsets present in the acquired records list that are also a part of batches that need to be archived.
+     * It follows an iterative refinement of acquired records to eliminate batches to be archived.
+     * @param acquiredRecordsList The list containing acquired records. This list is sorted by the firstOffset of the acquired batch.
+     * @param batchesToArchive The list containing record batches to archive. This list is sorted by the baseOffset of the record batch.
+     * @return The list containing filtered acquired records offsets.
+     */
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecordsList,
+        List<RecordBatch> batchesToArchive
+    ) {
+        List<AcquiredRecords> result = new ArrayList<>();
+        for (AcquiredRecords acquiredRecords : acquiredRecordsList) {
+            List<AcquiredRecords> tempAcquiredRecordsList = new ArrayList<>();
+            tempAcquiredRecordsList.add(acquiredRecords);
+            for (RecordBatch batchToArchive : batchesToArchive) {

Review Comment:
   This might be ok for now. But for each acquiredRecords, we are still iterating every batchToArchive. Since both acquiredRecordsList and batchesToArchive are in offset order, we could just iterating them together in one pass?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2026267169


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -1246,7 +1259,9 @@ private boolean archiveAvailableRecords(long startOffset, long endOffset, Naviga
 
     private boolean archivePerOffsetBatchRecords(InFlightBatch inFlightBatch,
                                                  long startOffsetToArchive,
-                                                 long endOffsetToArchive) {
+                                                 long endOffsetToArchive,
+                                                 RecordState initialState
+    ) {

Review Comment:
   I made the change as per this review comment https://github.com/apache/kafka/pull/19261#discussion_r2022398987 but I agree that this file has the convention to use  `) { ` on the end of the parameter line.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2026362292


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();

Review Comment:
   agreed, we don't need it for `filterAbortedTransactionalAcquiredRecords`



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013468986


##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -104,7 +104,8 @@ class KafkaApis(val requestChannel: RequestChannel,
                 time: Time,
                 val tokenManager: DelegationTokenManager,
                 val apiVersionManager: ApiVersionManager,
-                val clientMetricsManager: ClientMetricsManager
+                val clientMetricsManager: ClientMetricsManager,
+                val groupConfigManager: GroupConfigManager

Review Comment:
   `isolation level` is utilzied in `DelayedShareFetch`, `ShareFetchUtils` and `SharePartition`, so we should be fine with this change moving to `SharePartitionManager`



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2014733112


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {

Review Comment:
   I am sure. My ask was to write the details down in description. Anyways it might be too much, I ll look into.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022314413


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2504,205 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), RecordState.ACQUIRED);
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteBatch(inFlightBatch, RecordState.ACQUIRED);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * This function fetches the sub map from cachedState where all the offset details present in the recordBatch can be referred to
+     * OR it gives an exception if those offsets are not present in cachedState.
+     * @param recordBatch The record batch for which we want to find the sub map.
+     * @return the sub map containing all the offset details.
+     */
+    private NavigableMap<Long, InFlightBatch> fetchSubMapOrException(RecordBatch recordBatch) {
+        lock.readLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorEntry = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorEntry == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorEntry.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = orderedAbortedTransactions(abortedTransactions);
+        Set<Long> abortedProducerIds = new HashSet<>();
+        List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+        for (RecordBatch currentBatch : batches) {
+            if (currentBatch.hasProducerId()) {
+                // remove from the aborted transactions queue, all aborted transactions which have begun before the
+                // current batch's last offset and add the associated producerIds to the aborted producer set.
+                while (!orderedAbortedTransactions.isEmpty() && orderedAbortedTransactions.peek().firstOffset() <= currentBatch.lastOffset()) {
+                    FetchResponseData.AbortedTransaction abortedTransaction = orderedAbortedTransactions.poll();
+                    abortedProducerIds.add(abortedTransaction.producerId());
+                }
+                long producerId = currentBatch.producerId();
+                if (containsAbortMarker(currentBatch)) {
+                    abortedProducerIds.remove(producerId);
+                } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                    log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                        "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                    recordsToArchive.add(currentBatch);

Review Comment:
   The code is not covered in tests, can we please add scenarios which can test the code.



##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -3239,7 +3239,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         shareFetchRequest.maxWait,
         fetchMinBytes,
         fetchMaxBytes,
-        FetchIsolation.HIGH_WATERMARK,
+        FetchIsolation.of(FetchRequest.CONSUMER_REPLICA_ID, GroupConfig.defaultShareIsolationLevel),

Review Comment:
   Just FYI: there was a need to have `groupConfigManager` in KafkaApis as the final response is creted in KafkaApis hence I added it in following PR: https://github.com/apache/kafka/pull/19334/files. Once merged, we can move the code back here from SharePartitionManager. Sorry for the trouble.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {

Review Comment:
   Return types can be ignored. I re-compared both and still thinks that we should merge. The comparison of startOffset and endOffset seems relevant in both methods and we should have them. Let me know if I am missing something.
   
   ```
   private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
        archiveAvailableRecords(recordBatch.firstOffset(), recordBatch.lastOffset() + 1, subMap, Acquired);
   ```
   
   ```
   private boolean archiveAvailableRecords(long startOffset, long endOffset, NavigableMap<Long, InFlightBatch> map, RecordState initialState)
   ```
   
   ```
   move archiveAvailableRecords => archiveRecords
   ```



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");

Review Comment:
   `IllegalStateException` might be better as this condition should not hit.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -1241,7 +1254,8 @@ private boolean archiveAvailableRecords(long startOffset, long endOffset, Naviga
 
     private boolean archivePerOffsetBatchRecords(InFlightBatch inFlightBatch,
                                                  long startOffsetToArchive,
-                                                 long endOffsetToArchive) {
+                                                 long endOffsetToArchive,
+                                                 RecordState initialState) {

Review Comment:
   ```suggestion
                                                    RecordState initialState
        ) {
   ```



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2504,205 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), RecordState.ACQUIRED);
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteBatch(inFlightBatch, RecordState.ACQUIRED);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * This function fetches the sub map from cachedState where all the offset details present in the recordBatch can be referred to
+     * OR it gives an exception if those offsets are not present in cachedState.
+     * @param recordBatch The record batch for which we want to find the sub map.
+     * @return the sub map containing all the offset details.
+     */
+    private NavigableMap<Long, InFlightBatch> fetchSubMapOrException(RecordBatch recordBatch) {
+        lock.readLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorEntry = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorEntry == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorEntry.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = orderedAbortedTransactions(abortedTransactions);
+        Set<Long> abortedProducerIds = new HashSet<>();
+        List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+        for (RecordBatch currentBatch : batches) {
+            if (currentBatch.hasProducerId()) {
+                // remove from the aborted transactions queue, all aborted transactions which have begun before the
+                // current batch's last offset and add the associated producerIds to the aborted producer set.
+                while (!orderedAbortedTransactions.isEmpty() && orderedAbortedTransactions.peek().firstOffset() <= currentBatch.lastOffset()) {
+                    FetchResponseData.AbortedTransaction abortedTransaction = orderedAbortedTransactions.poll();
+                    abortedProducerIds.add(abortedTransaction.producerId());
+                }
+                long producerId = currentBatch.producerId();
+                if (containsAbortMarker(currentBatch)) {
+                    abortedProducerIds.remove(producerId);
+                } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                    log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                        "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                    recordsToArchive.add(currentBatch);
+                }
+            }
+        }
+        return recordsToArchive;
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = new PriorityQueue<>(
+            abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+        );
+        orderedAbortedTransactions.addAll(abortedTransactions);
+        return orderedAbortedTransactions;
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        if (!batch.isControlBatch())
+            return false;
+
+        Iterator<Record> batchIterator = batch.iterator();
+        if (!batchIterator.hasNext())
+            return false;
+
+        Record firstRecord = batchIterator.next();
+        return ControlRecordType.ABORT == ControlRecordType.parse(firstRecord.key());
+    }

Review Comment:
   Are there scenrios which tests these methods?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022224141


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();

Review Comment:
   you're right, we don't need it.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2026286010


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {

Review Comment:
   I have renamed `acquiredRecords` to `acquiredRecordsList` and `acquiredRecord` to `acquiredRecords`



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on PR #19261:
URL: https://github.com/apache/kafka/pull/19261#issuecomment-2774680482

   > Thanks for the updates. In general, I'm happy with the PR, with the exception of the `filterRecordBatchesFromAcquiredRecords` method. It needs much more comprehensive commenting and attention to naming. For example, it's not clear whether the input parameters are sorted and so on. This makes it difficult to work out the algorithm that it is using.
   
   Hi @AndrewJSchofield, thanks for the feedback. I have made naming changes as per your review comments and added the javadoc over the method and more comments in this method explaning my approach. Please let me know if this is explanatory enough or I need more changes. Thanks!


-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2026273027


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * This function fetches the sub map from cachedState where all the offset details present in the recordBatch can be referred to
+     * OR it gives an exception if those offsets are not present in cachedState.
+     * @param recordBatch The record batch for which we want to find the sub map.
+     * @return the sub map containing all the offset details.
+     */
+    private NavigableMap<Long, InFlightBatch> fetchSubMapOrException(RecordBatch recordBatch) {

Review Comment:
   I agree to your point. Earlier I had accepted the suggestion as per review comment https://github.com/apache/kafka/pull/19261#discussion_r2021438959, but I'll change it back to `fetchSubMap`



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "junrao (via GitHub)" <gi...@apache.org>.
junrao commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2029543043


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,182 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+        List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+        for (RecordBatch recordBatch : recordsToArchive) {
+            // Archive the offsets/batches in the cached state.
+            NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        }
+        return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+    }
+
+    /**
+     * This function filters out the offsets present in the acquired records list that are also a part of batches that need to be archived.
+     * It follows an iterative refinement of acquired records to eliminate batches to be archived.
+     * @param acquiredRecordsList The list containing acquired records. This list is sorted by the firstOffset of the acquired batch.
+     * @param batchesToArchive The list containing record batches to archive. This list is sorted by the baseOffset of the record batch.
+     * @return The list containing filtered acquired records offsets.
+     */
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecordsList,
+        List<RecordBatch> batchesToArchive
+    ) {
+        List<AcquiredRecords> result = new ArrayList<>();
+        for (AcquiredRecords acquiredRecords : acquiredRecordsList) {
+            List<AcquiredRecords> tempAcquiredRecordsList = new ArrayList<>();

Review Comment:
   Got it. The usage of `tempAcquiredRecordsIndex` still seems unintuitive. 



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,182 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+        List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+        for (RecordBatch recordBatch : recordsToArchive) {
+            // Archive the offsets/batches in the cached state.
+            NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        }
+        return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+    }
+
+    /**
+     * This function filters out the offsets present in the acquired records list that are also a part of batches that need to be archived.
+     * It follows an iterative refinement of acquired records to eliminate batches to be archived.
+     * @param acquiredRecordsList The list containing acquired records. This list is sorted by the firstOffset of the acquired batch.
+     * @param batchesToArchive The list containing record batches to archive. This list is sorted by the baseOffset of the record batch.
+     * @return The list containing filtered acquired records offsets.
+     */
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecordsList,
+        List<RecordBatch> batchesToArchive
+    ) {
+        List<AcquiredRecords> result = new ArrayList<>();
+        for (AcquiredRecords acquiredRecords : acquiredRecordsList) {
+            List<AcquiredRecords> tempAcquiredRecordsList = new ArrayList<>();
+            tempAcquiredRecordsList.add(acquiredRecords);
+            for (RecordBatch batchToArchive : batchesToArchive) {

Review Comment:
   I was thinking of sth like the following. Will it work?
   ```
           List<AcquiredRecords> result = new ArrayList<>();
           Iterator<AcquiredRecords> acquiredRecordsListIter = acquiredRecordsList.iterator();
           Iterator<RecordBatch> batchesToArchiveIterator = batchesToArchive.iterator();
           if (!batchesToArchiveIterator.hasNext())
               return acquiredRecordsList;
           RecordBatch batchToArchive = batchesToArchiveIterator.next();
           AcquiredRecords unresolvedAcquiredRecords = null;
   
           while (unresolvedAcquiredRecords != null || acquiredRecordsListIter.hasNext()) {
               if (unresolvedAcquiredRecords == null)
                   unresolvedAcquiredRecords = acquiredRecordsListIter.next();
   
               long unresolvedFirstOffset = unresolvedAcquiredRecords.firstOffset();
               long unresolvedLastOffset = unresolvedAcquiredRecords.lastOffset();
               short unresolvedDeliveryCount = unresolvedAcquiredRecords.deliveryCount();
   
               if (batchToArchive == null) {
                   result.add(unresolvedAcquiredRecords);
                   unresolvedAcquiredRecords = null;
                   continue;
               }
   
               // Non-overlap check - acquired records offsets lie before the batchToArchive offsets. No need to filter out the offsets in such a scenario.
               if (unresolvedLastOffset < batchToArchive.baseOffset()) {
                   // Offsets in tempAcquiredRecordsList element do not overlap with batchToArchive, hence it should not get filtered out.
                   result.add(unresolvedAcquiredRecords);
                   unresolvedAcquiredRecords = null;
               }
   
               // Overlap check - acquired records offsets overlap with the record batch offsets. We need to filter out the overlapping
               // offsets in such a scenario.
               if (unresolvedFirstOffset <= batchToArchive.lastOffset() &&
                       unresolvedLastOffset >= batchToArchive.baseOffset()) {
                   unresolvedAcquiredRecords = null;
                   // Split the acquired record into parts - before and after the overlapping record batch.
                   if (unresolvedFirstOffset < batchToArchive.baseOffset()) {
                       // The offsets in tempAcquiredRecords that are present before batchToArchive's baseOffset should not get filtered out.
                       result.add(new AcquiredRecords()
                               .setFirstOffset(unresolvedFirstOffset)
                               .setLastOffset(batchToArchive.baseOffset() - 1)
                               .setDeliveryCount(unresolvedDeliveryCount));
                   }
                   if (unresolvedLastOffset > batchToArchive.lastOffset()) {
                       // The offsets in tempAcquiredRecords that are present after batchToArchive's lastOffset should not get filtered out.
                       unresolvedAcquiredRecords = new AcquiredRecords()
                               .setFirstOffset(batchToArchive.lastOffset() + 1)
                               .setLastOffset(unresolvedLastOffset)
                               .setDeliveryCount(unresolvedDeliveryCount);
                   }
               }
   
               if (unresolvedLastOffset >= batchToArchive.lastOffset()) {
                   if (batchesToArchiveIterator.hasNext())
                       batchToArchive = batchesToArchiveIterator.next();
                   else
                       batchToArchive = null;
               }
           }
   
           return result;
   ```



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2033651560


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,339 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // Setting the acknowledgement commit callback to verify acknowledgement completion.
+                Map<TopicPartition, Set<Long>> partitionOffsetsMap = new HashMap<>();
+                Map<TopicPartition, Exception> partitionExceptionMap = new HashMap<>();
+                shareConsumer.setAcknowledgementCommitCallback(new TestableAcknowledgementCommitCallback(partitionOffsetsMap, partitionExceptionMap));
+
+                // We will not receive any records since the transaction for Message 2 was aborted. Wait for the
+                // aborted marker offset for Message 2 (3L) to be fetched and acknowledged by the consumer.
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(500));
+                    return pollRecords.count() == 0 && partitionExceptionMap.containsKey(tp) && partitionOffsetsMap.containsKey(tp) && partitionOffsetsMap.get(tp).contains(3L);

Review Comment:
   Yeah, we don't need to wait for an exception, so removed that check.
   Yes, it is true that control records will be acknowledged with the type `ARCHIVE` which means `REJECT` or `GAP`



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013529068


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -847,6 +859,7 @@ public ShareAcquiredRecords acquire(
             }

Review Comment:
   we are doing the filtering after this point, so this case would be automatically covered.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013806185


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {

Review Comment:
   highlighted the approach in PR description



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013589373


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions

Review Comment:
   so far in the testing I have observed that the `abortedTransactions` contains all the first offset of all the fetched record batches that have been aborted in **ascending order** of the first offset.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "AndrewJSchofield (via GitHub)" <gi...@apache.org>.
AndrewJSchofield commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2021229230


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions == null || abortedTransactions.isEmpty())
+                return null;
+
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = new PriorityQueue<>(
+                abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+            );
+            abortedTransactionsHeap.addAll(abortedTransactions);
+            return abortedTransactionsHeap;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        lock.writeLock().lock();
+        try {
+            return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        lock.writeLock().lock();
+        try {
+            if (!batch.isControlBatch())
+                return false;
+
+            Iterator<Record> batchIterator = batch.iterator();
+            if (!batchIterator.hasNext())
+                return false;
+
+            Record firstRecord = batchIterator.next();
+            return ControlRecordType.ABORT == ControlRecordType.parse(firstRecord.key());

Review Comment:
   I think this is unavoidable. We are doing server-side filtering of records because otherwise we need to post-process the list of aborted transactions as part of dividing up fetched records for the set of consumers which are waiting for the data.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022439069


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2504,205 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), RecordState.ACQUIRED);
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteBatch(inFlightBatch, RecordState.ACQUIRED);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * This function fetches the sub map from cachedState where all the offset details present in the recordBatch can be referred to
+     * OR it gives an exception if those offsets are not present in cachedState.
+     * @param recordBatch The record batch for which we want to find the sub map.
+     * @return the sub map containing all the offset details.
+     */
+    private NavigableMap<Long, InFlightBatch> fetchSubMapOrException(RecordBatch recordBatch) {
+        lock.readLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorEntry = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorEntry == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorEntry.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = orderedAbortedTransactions(abortedTransactions);
+        Set<Long> abortedProducerIds = new HashSet<>();
+        List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+        for (RecordBatch currentBatch : batches) {
+            if (currentBatch.hasProducerId()) {
+                // remove from the aborted transactions queue, all aborted transactions which have begun before the
+                // current batch's last offset and add the associated producerIds to the aborted producer set.
+                while (!orderedAbortedTransactions.isEmpty() && orderedAbortedTransactions.peek().firstOffset() <= currentBatch.lastOffset()) {
+                    FetchResponseData.AbortedTransaction abortedTransaction = orderedAbortedTransactions.poll();
+                    abortedProducerIds.add(abortedTransaction.producerId());
+                }
+                long producerId = currentBatch.producerId();
+                if (containsAbortMarker(currentBatch)) {
+                    abortedProducerIds.remove(producerId);
+                } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                    log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                        "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                    recordsToArchive.add(currentBatch);
+                }
+            }
+        }
+        return recordsToArchive;
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = new PriorityQueue<>(
+            abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+        );
+        orderedAbortedTransactions.addAll(abortedTransactions);
+        return orderedAbortedTransactions;
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        if (!batch.isControlBatch())
+            return false;
+
+        Iterator<Record> batchIterator = batch.iterator();
+        if (!batchIterator.hasNext())
+            return false;
+
+        Record firstRecord = batchIterator.next();
+        return ControlRecordType.ABORT == ControlRecordType.parse(firstRecord.key());
+    }

Review Comment:
   The functions `orderedAbortedTransactions`, `isBatchAborted`, `containsAbortMarker` are mirrors of what happens in `CompletedFetch` class. You won't find any tests for these functions there as well. That's why I've avoided them here as well. I am testing the logic that comes after we get aborted transactional records provided by function `fetchAbortedTransactionRecordBatches`. You'll be finding mocks for this function in `SharePartitionTest`
   cc- @AndrewJSchofield 



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "AndrewJSchofield (via GitHub)" <gi...@apache.org>.
AndrewJSchofield commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2025141887


##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -3199,13 +3199,21 @@ class KafkaApis(val requestChannel: RequestChannel,
           request.context.principal,
           request.context.listenerName.value))
 
+      def fetchIsolation(): FetchIsolation = {
+        if (groupConfigManager.groupConfig(groupId).isPresent) {
+          FetchIsolation.of(FetchRequest.CONSUMER_REPLICA_ID, groupConfigManager.groupConfig(groupId).get().shareIsolationLevel())
+        } else {
+          FetchIsolation.of(FetchRequest.CONSUMER_REPLICA_ID, GroupConfig.defaultShareIsolationLevel)
+        }
+      }
+
       val params = new FetchParams(
         FetchRequest.CONSUMER_REPLICA_ID,
         -1,
         shareFetchRequest.maxWait,
         fetchMinBytes,
         fetchMaxBytes,
-        FetchIsolation.HIGH_WATERMARK,
+        fetchIsolation(),

Review Comment:
   Maybe this could be simplified to something like
   ```FetchIsolation.of(FetchRequest.CONSUMER_REPLICA_ID, groupConfigManager.groupConfig(groupId).map(config => config.shareIsolationLevel()).orElse(GroupConfig.defaultShareIsolationLevel))```



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -1246,7 +1259,9 @@ private boolean archiveAvailableRecords(long startOffset, long endOffset, Naviga
 
     private boolean archivePerOffsetBatchRecords(InFlightBatch inFlightBatch,
                                                  long startOffsetToArchive,
-                                                 long endOffsetToArchive) {
+                                                 long endOffsetToArchive,
+                                                 RecordState initialState
+    ) {

Review Comment:
   nit: This source file tends to have the `) {` on the end of the parameter line.



##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -3199,13 +3199,21 @@ class KafkaApis(val requestChannel: RequestChannel,
           request.context.principal,
           request.context.listenerName.value))
 
+      def fetchIsolation(): FetchIsolation = {
+        if (groupConfigManager.groupConfig(groupId).isPresent) {
+          FetchIsolation.of(FetchRequest.CONSUMER_REPLICA_ID, groupConfigManager.groupConfig(groupId).get().shareIsolationLevel())
+        } else {
+          FetchIsolation.of(FetchRequest.CONSUMER_REPLICA_ID, GroupConfig.defaultShareIsolationLevel)
+        }
+      }
+
       val params = new FetchParams(
         FetchRequest.CONSUMER_REPLICA_ID,
         -1,
         shareFetchRequest.maxWait,
         fetchMinBytes,
         fetchMaxBytes,
-        FetchIsolation.HIGH_WATERMARK,
+        fetchIsolation(),

Review Comment:
   Although "simplified" might be stretching the point.



##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/GroupConfig.java:
##########
@@ -137,7 +149,13 @@ public final class GroupConfig extends AbstractConfig {
             GroupCoordinatorConfig.STREAMS_GROUP_NUM_STANDBY_REPLICAS_DEFAULT,
             atLeast(0),
             MEDIUM,
-            GroupCoordinatorConfig.STREAMS_GROUP_NUM_STANDBY_REPLICAS_DOC);
+            GroupCoordinatorConfig.STREAMS_GROUP_NUM_STANDBY_REPLICAS_DOC)
+        .define(SHARE_ISOLATION_LEVEL_CONFIG,

Review Comment:
   Please put this config adjacent to the other share group configs above.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -1206,7 +1219,7 @@ private boolean archiveAvailableRecordsOnLsoMovement(long logStartOffset) {
      * @param map The map containing the in-flight records.
      * @return A boolean which indicates whether any record is archived or not.

Review Comment:
   nit: Add `initialState` to the javadoc comment.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);

Review Comment:
   Given that this method is only really a single line of code, I would remove the method and just have the line of code here.



##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -116,6 +124,7 @@ public class SharePartitionTest {
     private static final int DEFAULT_FETCH_OFFSET = 0;
     private static final int MAX_FETCH_RECORDS = Integer.MAX_VALUE;
     private static final byte ACKNOWLEDGE_TYPE_GAP_ID = 0;
+    private static final FetchIsolation FETCH_ISOLATION = FetchIsolation.HIGH_WATERMARK;

Review Comment:
   This constant name really tells us the type, not the value. I wonder if `FETCH_ISOLATION_HWM` might be more instructive.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any

Review Comment:
   nit: I'd add a blank line to make it a bit more readable at a glance. The general case is to return on line 2514.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * This function fetches the sub map from cachedState where all the offset details present in the recordBatch can be referred to
+     * OR it gives an exception if those offsets are not present in cachedState.
+     * @param recordBatch The record batch for which we want to find the sub map.
+     * @return the sub map containing all the offset details.
+     */
+    private NavigableMap<Long, InFlightBatch> fetchSubMapOrException(RecordBatch recordBatch) {

Review Comment:
   I think this style is a bit strange. If I understand correctly, we never expect to see the exception. That would be a bug. So, I would just call it `getSubMap` or `getCachedSubMap` or similar. Yes, it throws an exception if it's called in an invalid state, but then the share-partition would be messed up already.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));

Review Comment:
   Also here.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {

Review Comment:
   It is a bit odd having the loop variable of type `AcquiredRecords` called `acquiredRecord`, and the list of `AcquiredRecords` called `acquiredRecords`.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {

Review Comment:
   If you renamed `recordsToArchive` as `batchesToArchive`, and then renamed `recordBatch` to `batchToArchive`, I think that would help with the readability.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));

Review Comment:
   Wouldn't this be `temp.deliveryCount()`?



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -1206,7 +1219,7 @@ private boolean archiveAvailableRecordsOnLsoMovement(long logStartOffset) {
      * @param map The map containing the in-flight records.
      * @return A boolean which indicates whether any record is archived or not.

Review Comment:
   And correct the description so it doesn't just return to the available records.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2031778027


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);
+                assertEquals(0, records.count());
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                // Message 3 would be returned by this poll.
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 3", new String(record.value()));
+                // We will make Message 3 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // Precautionary poll so that the aborted transaction for Message 4 is fetched by the consumer.
+                shareConsumer.poll(Duration.ofMillis(5000));

Review Comment:
   I am not sure if I follow the comment. Since this is a poll to make sure aborted transaction for message 4 is fetched by the consumer, it won't return m any records. So what condition shall I put for `waitForCondition`? Anyways, I have reduced the poll duration to 2.5 seconds since I see that value is being used in polls for all the other test cases in this class.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2032529508


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);

Review Comment:
   ys, in this implementation it did. I have changed the testing implementation by setting an acknowledgement commit callback to verify that aborted marker offset for Message 2 (3L) is fetched and acknowledged by the consumer. I have used `TestUtils.waitForCondition()` and done a poll for `500ms` for the same. This will verify that transaction corresponding to message 4 is fetched by the consumer and also takes much less time now.



##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);

Review Comment:
   yes, in this implementation it did. I have changed the testing implementation by setting an acknowledgement commit callback to verify that aborted marker offset for Message 2 (3L) is fetched and acknowledged by the consumer. I have used `TestUtils.waitForCondition()` and done a poll for `500ms` for the same. This will verify that transaction corresponding to message 4 is fetched by the consumer and also takes much less time now.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2028100666


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,174 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();

Review Comment:
   I think `archiveRecords()` needs a lock because `inFlightBatch.maybeInitializeOffsetStateUpdate()` is called which can change the cachedState



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "AndrewJSchofield (via GitHub)" <gi...@apache.org>.
AndrewJSchofield commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2026700007


##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -1212,7 +1223,8 @@ public void testAcquireWithMultipleBatchesAndMaxFetchRecords() throws Interrupte
             BATCH_SIZE,
             10,
             DEFAULT_FETCH_OFFSET,
-            fetchPartitionData(records, 10)),
+            fetchPartitionData(records, 10),
+                FETCH_ISOLATION_HWM),

Review Comment:
   nit: indentation



##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -1308,7 +1321,8 @@ public void testAcquireWithBatchSizeAndSingleBatch() {
             2 /* Batch size */,
             10,
             DEFAULT_FETCH_OFFSET,
-            fetchPartitionData(records)),
+            fetchPartitionData(records),
+                FETCH_ISOLATION_HWM),

Review Comment:
   nit: indentation



##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -1175,7 +1185,8 @@ public void testAcquireWithMaxFetchRecords() {
             BATCH_SIZE,
             10,
             DEFAULT_FETCH_OFFSET,
-            fetchPartitionData(records)),
+            fetchPartitionData(records),
+                FETCH_ISOLATION_HWM),

Review Comment:
   nit: indentation



##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -1339,7 +1353,8 @@ public void testAcquireWithBatchSizeAndMultipleBatches() {
             5 /* Batch size */,
             100,
             DEFAULT_FETCH_OFFSET,
-            fetchPartitionData(records)),
+            fetchPartitionData(records),
+                FETCH_ISOLATION_HWM),

Review Comment:
   nit: indentation



##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -6671,15 +6693,441 @@ private String assertionFailedMessage(SharePartition sharePartition, Map<Long, L
         return errorMessage.toString();
     }
 
+    @Test
+    public void testFilterRecordBatchesFromAcquiredRecords() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        List<AcquiredRecords> acquiredRecords1 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(5).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(10).setLastOffset(15).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches1 = List.of(
+            memoryRecordsBuilder(3, 2).build().batches().iterator().next(),
+            memoryRecordsBuilder(3, 12).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(1).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(5).setLastOffset(5).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(10).setLastOffset(11).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(15).setLastOffset(15).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)),
+            sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords1, recordBatches1));
+
+        List<AcquiredRecords> acquiredRecords2 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 1),

Review Comment:
   Let's put some variation in the delivery count here.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "junrao (via GitHub)" <gi...@apache.org>.
junrao commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2031538464


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2488,6 +2509,190 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+        List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+        for (RecordBatch recordBatch : recordsToArchive) {
+            // Archive the offsets/batches in the cached state.
+            NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        }
+        return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+    }
+
+    /**
+     * This function filters out the offsets present in the acquired records list that are also a part of batches that need to be archived.
+     * It follows an iterative refinement of acquired records to eliminate batches to be archived.
+     * @param acquiredRecordsList The list containing acquired records. This list is sorted by the firstOffset of the acquired batch.
+     * @param batchesToArchive The list containing record batches to archive. This list is sorted by the baseOffset of the record batch.
+     * @return The list containing filtered acquired records offsets.
+     */
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecordsList,
+        List<RecordBatch> batchesToArchive
+    ) {
+        List<AcquiredRecords> result = new ArrayList<>();
+        Iterator<AcquiredRecords> acquiredRecordsListIter = acquiredRecordsList.iterator();
+        Iterator<RecordBatch> batchesToArchiveIterator = batchesToArchive.iterator();
+        if (!batchesToArchiveIterator.hasNext())
+            return acquiredRecordsList;
+        RecordBatch batchToArchive = batchesToArchiveIterator.next();
+        AcquiredRecords unresolvedAcquiredRecords = null;
+
+        while (unresolvedAcquiredRecords != null || acquiredRecordsListIter.hasNext()) {
+            if (unresolvedAcquiredRecords == null)
+                unresolvedAcquiredRecords = acquiredRecordsListIter.next();
+
+            long unresolvedFirstOffset = unresolvedAcquiredRecords.firstOffset();
+            long unresolvedLastOffset = unresolvedAcquiredRecords.lastOffset();
+            short unresolvedDeliveryCount = unresolvedAcquiredRecords.deliveryCount();
+
+            if (batchToArchive == null) {
+                result.add(unresolvedAcquiredRecords);
+                unresolvedAcquiredRecords = null;
+                continue;
+            }
+
+            // Non-overlap check - unresolvedFirstOffset offsets lie before the batchToArchive offsets. No need to filter out the offsets in such a scenario.
+            if (unresolvedLastOffset < batchToArchive.baseOffset()) {
+                // Offsets in unresolvedAcquiredRecords do not overlap with batchToArchive, hence it should not get filtered out.
+                result.add(unresolvedAcquiredRecords);
+                unresolvedAcquiredRecords = null;
+            }
+
+            // Overlap check - unresolvedFirstOffset offsets overlap with the batchToArchive offsets. We need to filter out the overlapping
+            // offsets in such a scenario.
+            if (unresolvedFirstOffset <= batchToArchive.lastOffset() &&
+                unresolvedLastOffset >= batchToArchive.baseOffset()) {
+                unresolvedAcquiredRecords = null;
+                // Split the unresolvedFirstOffset into parts - before and after the overlapping record batchToArchive.
+                if (unresolvedFirstOffset < batchToArchive.baseOffset()) {
+                    // The offsets in unresolvedAcquiredRecords that are present before batchToArchive's baseOffset should not get filtered out.
+                    result.add(new AcquiredRecords()
+                        .setFirstOffset(unresolvedFirstOffset)
+                        .setLastOffset(batchToArchive.baseOffset() - 1)
+                        .setDeliveryCount(unresolvedDeliveryCount));
+                }
+                if (unresolvedLastOffset > batchToArchive.lastOffset()) {
+                    // The offsets in unresolvedAcquiredRecords that are present after batchToArchive's lastOffset should not get filtered out
+                    // and should be taken forward for further processing since they could potentially contain offsets that need to be archived.
+                    unresolvedAcquiredRecords = new AcquiredRecords()
+                        .setFirstOffset(batchToArchive.lastOffset() + 1)
+                        .setLastOffset(unresolvedLastOffset)
+                        .setDeliveryCount(unresolvedDeliveryCount);
+                }
+            }
+
+            if (unresolvedLastOffset >= batchToArchive.lastOffset()) {

Review Comment:
   Could we add a comment for this case?



##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);

Review Comment:
   Does this force the test to wait for 2.5 secs? It's kind of long.



##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -6671,15 +6693,441 @@ private String assertionFailedMessage(SharePartition sharePartition, Map<Long, L
         return errorMessage.toString();
     }
 
+    @Test
+    public void testFilterRecordBatchesFromAcquiredRecords() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        List<AcquiredRecords> acquiredRecords1 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(5).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(10).setLastOffset(15).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches1 = List.of(
+            memoryRecordsBuilder(3, 2).build().batches().iterator().next(),
+            memoryRecordsBuilder(3, 12).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(1).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(5).setLastOffset(5).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(10).setLastOffset(11).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(15).setLastOffset(15).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)),
+            sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords1, recordBatches1));
+
+        List<AcquiredRecords> acquiredRecords2 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(5).setLastOffset(8).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(9).setLastOffset(30).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(31).setLastOffset(40).setDeliveryCount((short) 3)
+        );
+        List<RecordBatch> recordBatches2 = List.of(
+            memoryRecordsBuilder(21, 5).build().batches().iterator().next(),
+            memoryRecordsBuilder(5, 31).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+                new AcquiredRecords().setFirstOffset(26).setLastOffset(30).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(36).setLastOffset(40).setDeliveryCount((short) 3)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, recordBatches2)
+        );
+
+        // Record batches is empty.
+        assertEquals(acquiredRecords2, sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, List.of()));
+
+        List<AcquiredRecords> acquiredRecords3 = List.of(
+            new AcquiredRecords().setFirstOffset(0).setLastOffset(19).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches3 = List.of(
+            memoryRecordsBuilder(1, 8).build().batches().iterator().next(),
+            memoryRecordsBuilder(1, 18).build().batches().iterator().next()
+        );
+
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(0).setLastOffset(7).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(9).setLastOffset(17).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(19).setLastOffset(19).setDeliveryCount((short) 1)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords3, recordBatches3)
+        );
+    }
+
+    @Test
+    public void testAcquireWithReadCommittedIsolationLevel() {
+        SharePartition sharePartition = Mockito.spy(SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build());
+
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        memoryRecordsBuilder(buffer, 5, 10).close();
+        memoryRecordsBuilder(buffer, 5, 15).close();
+        memoryRecordsBuilder(buffer, 15, 20).close();
+        memoryRecordsBuilder(buffer, 8, 50).close();
+        memoryRecordsBuilder(buffer, 10, 58).close();
+        memoryRecordsBuilder(buffer, 5, 70).close();
+
+        buffer.flip();
+        MemoryRecords records = MemoryRecords.readableRecords(buffer);
+        FetchPartitionData fetchPartitionData = fetchPartitionData(records, newAbortedTransactions());
+
+        // We are mocking the result of function fetchAbortedTransactionRecordBatches. The records present at these offsets need to be archived.
+        // We won't be utilizing the aborted transactions passed in fetchPartitionData.
+        when(sharePartition.fetchAbortedTransactionRecordBatches(fetchPartitionData.records.batches(), fetchPartitionData.abortedTransactions.get())).thenReturn(
+            List.of(
+                memoryRecordsBuilder(5, 10).build().batches().iterator().next(),
+                memoryRecordsBuilder(10, 58).build().batches().iterator().next(),
+                memoryRecordsBuilder(5, 70).build().batches().iterator().next()
+            )
+        );
+
+        List<AcquiredRecords> acquiredRecordsList = fetchAcquiredRecords(
+            sharePartition.acquire(
+                MEMBER_ID,
+                10 /* Batch size */,
+                100,
+                DEFAULT_FETCH_OFFSET,
+                fetchPartitionData,
+                FetchIsolation.TXN_COMMITTED),
+            45 /* Gap of 15 records will be added to second batch, gap of 2 records will also be added to fourth batch */);
+
+        assertEquals(List.of(
+            new AcquiredRecords().setFirstOffset(15).setLastOffset(19).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(49).setDeliveryCount((short) 1),

Review Comment:
   Why do we set the last offset to 49 when the original batch ends at offset 34?



##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -1154,7 +1163,8 @@ public void testAcquireWithMaxFetchRecords() {
             BATCH_SIZE,
             10,
             DEFAULT_FETCH_OFFSET,
-            fetchPartitionData(records)),
+            fetchPartitionData(records),
+                FETCH_ISOLATION_HWM),

Review Comment:
   indentation



##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -6671,15 +6693,441 @@ private String assertionFailedMessage(SharePartition sharePartition, Map<Long, L
         return errorMessage.toString();
     }
 
+    @Test
+    public void testFilterRecordBatchesFromAcquiredRecords() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        List<AcquiredRecords> acquiredRecords1 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(5).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(10).setLastOffset(15).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches1 = List.of(
+            memoryRecordsBuilder(3, 2).build().batches().iterator().next(),
+            memoryRecordsBuilder(3, 12).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(1).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(5).setLastOffset(5).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(10).setLastOffset(11).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(15).setLastOffset(15).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)),
+            sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords1, recordBatches1));
+
+        List<AcquiredRecords> acquiredRecords2 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(5).setLastOffset(8).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(9).setLastOffset(30).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(31).setLastOffset(40).setDeliveryCount((short) 3)
+        );
+        List<RecordBatch> recordBatches2 = List.of(
+            memoryRecordsBuilder(21, 5).build().batches().iterator().next(),
+            memoryRecordsBuilder(5, 31).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+                new AcquiredRecords().setFirstOffset(26).setLastOffset(30).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(36).setLastOffset(40).setDeliveryCount((short) 3)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, recordBatches2)
+        );
+
+        // Record batches is empty.
+        assertEquals(acquiredRecords2, sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, List.of()));
+
+        List<AcquiredRecords> acquiredRecords3 = List.of(
+            new AcquiredRecords().setFirstOffset(0).setLastOffset(19).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches3 = List.of(
+            memoryRecordsBuilder(1, 8).build().batches().iterator().next(),
+            memoryRecordsBuilder(1, 18).build().batches().iterator().next()
+        );
+
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(0).setLastOffset(7).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(9).setLastOffset(17).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(19).setLastOffset(19).setDeliveryCount((short) 1)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords3, recordBatches3)
+        );
+    }
+
+    @Test
+    public void testAcquireWithReadCommittedIsolationLevel() {
+        SharePartition sharePartition = Mockito.spy(SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build());
+
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        memoryRecordsBuilder(buffer, 5, 10).close();
+        memoryRecordsBuilder(buffer, 5, 15).close();
+        memoryRecordsBuilder(buffer, 15, 20).close();
+        memoryRecordsBuilder(buffer, 8, 50).close();
+        memoryRecordsBuilder(buffer, 10, 58).close();
+        memoryRecordsBuilder(buffer, 5, 70).close();
+
+        buffer.flip();
+        MemoryRecords records = MemoryRecords.readableRecords(buffer);
+        FetchPartitionData fetchPartitionData = fetchPartitionData(records, newAbortedTransactions());
+
+        // We are mocking the result of function fetchAbortedTransactionRecordBatches. The records present at these offsets need to be archived.
+        // We won't be utilizing the aborted transactions passed in fetchPartitionData.
+        when(sharePartition.fetchAbortedTransactionRecordBatches(fetchPartitionData.records.batches(), fetchPartitionData.abortedTransactions.get())).thenReturn(
+            List.of(
+                memoryRecordsBuilder(5, 10).build().batches().iterator().next(),
+                memoryRecordsBuilder(10, 58).build().batches().iterator().next(),
+                memoryRecordsBuilder(5, 70).build().batches().iterator().next()
+            )
+        );
+
+        List<AcquiredRecords> acquiredRecordsList = fetchAcquiredRecords(
+            sharePartition.acquire(
+                MEMBER_ID,
+                10 /* Batch size */,
+                100,
+                DEFAULT_FETCH_OFFSET,
+                fetchPartitionData,
+                FetchIsolation.TXN_COMMITTED),
+            45 /* Gap of 15 records will be added to second batch, gap of 2 records will also be added to fourth batch */);
+
+        assertEquals(List.of(
+            new AcquiredRecords().setFirstOffset(15).setLastOffset(19).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(49).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(50).setLastOffset(57).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(68).setLastOffset(69).setDeliveryCount((short) 1)
+        ), acquiredRecordsList);
+        assertEquals(75, sharePartition.nextFetchOffset());
+
+        // Checking cached state.
+        assertEquals(4, sharePartition.cachedState().size());
+        assertTrue(sharePartition.cachedState().containsKey(10L));
+        assertTrue(sharePartition.cachedState().containsKey(20L));
+        assertTrue(sharePartition.cachedState().containsKey(50L));
+        assertTrue(sharePartition.cachedState().containsKey(70L));
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState());
+
+        assertEquals(19L, sharePartition.cachedState().get(10L).lastOffset());
+        assertEquals(49L, sharePartition.cachedState().get(20L).lastOffset());
+        assertEquals(69L, sharePartition.cachedState().get(50L).lastOffset());
+        assertEquals(74L, sharePartition.cachedState().get(70L).lastOffset());
+
+        assertEquals(RecordState.ACQUIRED, sharePartition.cachedState().get(20L).batchState());
+        assertEquals(RecordState.ARCHIVED, sharePartition.cachedState().get(70L).batchState());
+
+        assertEquals(MEMBER_ID, sharePartition.cachedState().get(20L).batchMemberId());
+        assertEquals(EMPTY_MEMBER_ID, sharePartition.cachedState().get(70L).batchMemberId());
+
+        assertNotNull(sharePartition.cachedState().get(20L).batchAcquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(70L).batchAcquisitionLockTimeoutTask());
+
+        Map<Long, InFlightState> expectedOffsetStateMap = new HashMap<>();
+        expectedOffsetStateMap.put(10L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(11L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(12L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(13L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(14L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(15L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(16L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(17L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(18L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(19L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        assertEquals(expectedOffsetStateMap, sharePartition.cachedState().get(10L).offsetState());
+
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(10L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(11L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(12L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(13L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(14L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(15L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(16L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(17L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(18L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(19L).acquisitionLockTimeoutTask());
+
+        expectedOffsetStateMap = new HashMap<>();
+        expectedOffsetStateMap.put(50L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(51L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(52L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(53L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(54L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(55L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(56L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(57L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(58L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(59L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(60L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(61L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(62L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(63L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(64L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(65L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(66L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(67L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(68L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(69L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        assertEquals(expectedOffsetStateMap, sharePartition.cachedState().get(50L).offsetState());
+
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(50L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(51L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(52L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(53L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(54L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(55L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(56L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(57L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(58L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(59L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(60L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(61L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(62L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(63L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(64L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(65L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(66L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(67L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(68L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(69L).acquisitionLockTimeoutTask());
+    }
+
+    @Test
+    public void testContainsAbortMarker() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        // Record batch is not a control batch.
+        RecordBatch recordBatch = mock(RecordBatch.class);
+        when(recordBatch.isControlBatch()).thenReturn(false);
+        assertFalse(sharePartition.containsAbortMarker(recordBatch));
+
+        // Record batch is a control batch but doesn't contain any records..

Review Comment:
   extra .



##########
core/src/test/java/kafka/server/share/SharePartitionTest.java:
##########
@@ -6671,15 +6693,441 @@ private String assertionFailedMessage(SharePartition sharePartition, Map<Long, L
         return errorMessage.toString();
     }
 
+    @Test
+    public void testFilterRecordBatchesFromAcquiredRecords() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        List<AcquiredRecords> acquiredRecords1 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(5).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(10).setLastOffset(15).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches1 = List.of(
+            memoryRecordsBuilder(3, 2).build().batches().iterator().next(),
+            memoryRecordsBuilder(3, 12).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(1).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(5).setLastOffset(5).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(10).setLastOffset(11).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(15).setLastOffset(15).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(20).setLastOffset(25).setDeliveryCount((short) 1)),
+            sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords1, recordBatches1));
+
+        List<AcquiredRecords> acquiredRecords2 = List.of(
+            new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(5).setLastOffset(8).setDeliveryCount((short) 3),
+            new AcquiredRecords().setFirstOffset(9).setLastOffset(30).setDeliveryCount((short) 2),
+            new AcquiredRecords().setFirstOffset(31).setLastOffset(40).setDeliveryCount((short) 3)
+        );
+        List<RecordBatch> recordBatches2 = List.of(
+            memoryRecordsBuilder(21, 5).build().batches().iterator().next(),
+            memoryRecordsBuilder(5, 31).build().batches().iterator().next()
+        );
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(1).setLastOffset(4).setDeliveryCount((short) 3),
+                new AcquiredRecords().setFirstOffset(26).setLastOffset(30).setDeliveryCount((short) 2),
+                new AcquiredRecords().setFirstOffset(36).setLastOffset(40).setDeliveryCount((short) 3)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, recordBatches2)
+        );
+
+        // Record batches is empty.
+        assertEquals(acquiredRecords2, sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords2, List.of()));
+
+        List<AcquiredRecords> acquiredRecords3 = List.of(
+            new AcquiredRecords().setFirstOffset(0).setLastOffset(19).setDeliveryCount((short) 1)
+        );
+        List<RecordBatch> recordBatches3 = List.of(
+            memoryRecordsBuilder(1, 8).build().batches().iterator().next(),
+            memoryRecordsBuilder(1, 18).build().batches().iterator().next()
+        );
+
+        assertEquals(
+            List.of(
+                new AcquiredRecords().setFirstOffset(0).setLastOffset(7).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(9).setLastOffset(17).setDeliveryCount((short) 1),
+                new AcquiredRecords().setFirstOffset(19).setLastOffset(19).setDeliveryCount((short) 1)
+
+            ), sharePartition.filterRecordBatchesFromAcquiredRecords(acquiredRecords3, recordBatches3)
+        );
+    }
+
+    @Test
+    public void testAcquireWithReadCommittedIsolationLevel() {
+        SharePartition sharePartition = Mockito.spy(SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build());
+
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        memoryRecordsBuilder(buffer, 5, 10).close();
+        memoryRecordsBuilder(buffer, 5, 15).close();
+        memoryRecordsBuilder(buffer, 15, 20).close();
+        memoryRecordsBuilder(buffer, 8, 50).close();
+        memoryRecordsBuilder(buffer, 10, 58).close();
+        memoryRecordsBuilder(buffer, 5, 70).close();
+
+        buffer.flip();
+        MemoryRecords records = MemoryRecords.readableRecords(buffer);
+        FetchPartitionData fetchPartitionData = fetchPartitionData(records, newAbortedTransactions());
+
+        // We are mocking the result of function fetchAbortedTransactionRecordBatches. The records present at these offsets need to be archived.
+        // We won't be utilizing the aborted transactions passed in fetchPartitionData.
+        when(sharePartition.fetchAbortedTransactionRecordBatches(fetchPartitionData.records.batches(), fetchPartitionData.abortedTransactions.get())).thenReturn(
+            List.of(
+                memoryRecordsBuilder(5, 10).build().batches().iterator().next(),
+                memoryRecordsBuilder(10, 58).build().batches().iterator().next(),
+                memoryRecordsBuilder(5, 70).build().batches().iterator().next()
+            )
+        );
+
+        List<AcquiredRecords> acquiredRecordsList = fetchAcquiredRecords(
+            sharePartition.acquire(
+                MEMBER_ID,
+                10 /* Batch size */,
+                100,
+                DEFAULT_FETCH_OFFSET,
+                fetchPartitionData,
+                FetchIsolation.TXN_COMMITTED),
+            45 /* Gap of 15 records will be added to second batch, gap of 2 records will also be added to fourth batch */);
+
+        assertEquals(List.of(
+            new AcquiredRecords().setFirstOffset(15).setLastOffset(19).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(20).setLastOffset(49).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(50).setLastOffset(57).setDeliveryCount((short) 1),
+            new AcquiredRecords().setFirstOffset(68).setLastOffset(69).setDeliveryCount((short) 1)
+        ), acquiredRecordsList);
+        assertEquals(75, sharePartition.nextFetchOffset());
+
+        // Checking cached state.
+        assertEquals(4, sharePartition.cachedState().size());
+        assertTrue(sharePartition.cachedState().containsKey(10L));
+        assertTrue(sharePartition.cachedState().containsKey(20L));
+        assertTrue(sharePartition.cachedState().containsKey(50L));
+        assertTrue(sharePartition.cachedState().containsKey(70L));
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState());
+
+        assertEquals(19L, sharePartition.cachedState().get(10L).lastOffset());
+        assertEquals(49L, sharePartition.cachedState().get(20L).lastOffset());
+        assertEquals(69L, sharePartition.cachedState().get(50L).lastOffset());
+        assertEquals(74L, sharePartition.cachedState().get(70L).lastOffset());
+
+        assertEquals(RecordState.ACQUIRED, sharePartition.cachedState().get(20L).batchState());
+        assertEquals(RecordState.ARCHIVED, sharePartition.cachedState().get(70L).batchState());
+
+        assertEquals(MEMBER_ID, sharePartition.cachedState().get(20L).batchMemberId());
+        assertEquals(EMPTY_MEMBER_ID, sharePartition.cachedState().get(70L).batchMemberId());
+
+        assertNotNull(sharePartition.cachedState().get(20L).batchAcquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(70L).batchAcquisitionLockTimeoutTask());
+
+        Map<Long, InFlightState> expectedOffsetStateMap = new HashMap<>();
+        expectedOffsetStateMap.put(10L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(11L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(12L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(13L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(14L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(15L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(16L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(17L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(18L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(19L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        assertEquals(expectedOffsetStateMap, sharePartition.cachedState().get(10L).offsetState());
+
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(10L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(11L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(12L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(13L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(10L).offsetState().get(14L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(15L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(16L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(17L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(18L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(10L).offsetState().get(19L).acquisitionLockTimeoutTask());
+
+        expectedOffsetStateMap = new HashMap<>();
+        expectedOffsetStateMap.put(50L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(51L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(52L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(53L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(54L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(55L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(56L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(57L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(58L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(59L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(60L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(61L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(62L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(63L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(64L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(65L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(66L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(67L, new InFlightState(RecordState.ARCHIVED, (short) 1, EMPTY_MEMBER_ID));
+        expectedOffsetStateMap.put(68L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        expectedOffsetStateMap.put(69L, new InFlightState(RecordState.ACQUIRED, (short) 1, MEMBER_ID));
+        assertEquals(expectedOffsetStateMap, sharePartition.cachedState().get(50L).offsetState());
+
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(50L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(51L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(52L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(53L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(54L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(55L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(56L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(57L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(58L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(59L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(60L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(61L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(62L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(63L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(64L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(65L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(66L).acquisitionLockTimeoutTask());
+        assertNull(sharePartition.cachedState().get(50L).offsetState().get(67L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(68L).acquisitionLockTimeoutTask());
+        assertNotNull(sharePartition.cachedState().get(50L).offsetState().get(69L).acquisitionLockTimeoutTask());
+    }
+
+    @Test
+    public void testContainsAbortMarker() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        // Record batch is not a control batch.
+        RecordBatch recordBatch = mock(RecordBatch.class);
+        when(recordBatch.isControlBatch()).thenReturn(false);
+        assertFalse(sharePartition.containsAbortMarker(recordBatch));
+
+        // Record batch is a control batch but doesn't contain any records..
+        recordBatch = mock(RecordBatch.class);
+        Iterator batchIterator = mock(Iterator.class);
+        when(batchIterator.hasNext()).thenReturn(false);
+        when(recordBatch.iterator()).thenReturn(batchIterator);
+        when(recordBatch.isControlBatch()).thenReturn(true);
+        assertFalse(sharePartition.containsAbortMarker(recordBatch));
+
+        // Record batch is a control batch which contains a record of type ControlRecordType.ABORT.
+        recordBatch = mock(RecordBatch.class);
+        batchIterator = mock(Iterator.class);
+        when(batchIterator.hasNext()).thenReturn(true);
+        DefaultRecord record = mock(DefaultRecord.class);
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        // Buffer has to be created in a way that ControlRecordType.parse(buffer) returns ControlRecordType.ABORT.
+        buffer.putShort((short) 5);
+        buffer.putShort(ControlRecordType.ABORT.type());
+        buffer.putInt(23432); // some field added in version 5
+        buffer.flip();
+        when(record.key()).thenReturn(buffer);
+        when(batchIterator.next()).thenReturn(record);
+        when(recordBatch.iterator()).thenReturn(batchIterator);
+        when(recordBatch.isControlBatch()).thenReturn(true);
+        assertTrue(sharePartition.containsAbortMarker(recordBatch));
+
+        // Record batch is a control batch which contains a record of type ControlRecordType.COMMIT.
+        recordBatch = mock(RecordBatch.class);
+        batchIterator = mock(Iterator.class);
+        when(batchIterator.hasNext()).thenReturn(true);
+        record = mock(DefaultRecord.class);
+        buffer = ByteBuffer.allocate(4096);
+        // Buffer has to be created in a way that ControlRecordType.parse(buffer) returns ControlRecordType.COMMIT.
+        buffer.putShort((short) 5);
+        buffer.putShort(ControlRecordType.COMMIT.type());
+        buffer.putInt(23432); // some field added in version 5
+        buffer.flip();
+        when(record.key()).thenReturn(buffer);
+        when(batchIterator.next()).thenReturn(record);
+        when(recordBatch.iterator()).thenReturn(batchIterator);
+        when(recordBatch.isControlBatch()).thenReturn(true);
+        assertFalse(sharePartition.containsAbortMarker(recordBatch));
+    }
+
+    @Test
+    public void testFetchAbortedTransactionRecordBatchesForOnlyAbortedTransactions() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        // Case 1 - Creating 10 transactional records in a single batch followed by a ABORT marker record for producerId 1.
+        ByteBuffer buffer = ByteBuffer.allocate(1024);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 10, 1, 0);
+        buffer.flip();
+        Records records = MemoryRecords.readableRecords(buffer);
+
+        List<FetchResponseData.AbortedTransaction> abortedTransactions = List.of(
+            new FetchResponseData.AbortedTransaction().setFirstOffset(0).setProducerId(1)
+        );
+        // records from 0 to 9 should be archived because they are a part of aborted transactions.
+        List<RecordBatch> actual = sharePartition.fetchAbortedTransactionRecordBatches(records.batches(), abortedTransactions);
+        assertEquals(1, actual.size());
+        assertEquals(0, actual.get(0).baseOffset());
+        assertEquals(9, actual.get(0).lastOffset());
+        assertEquals(1, actual.get(0).producerId());
+
+        // Case 2: 3 individual batches each followed by a ABORT marker record for producerId 1.
+        buffer = ByteBuffer.allocate(1024);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 1, 1, 0);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 1, 1, 2);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 1, 1, 4);
+        buffer.flip();
+        records = MemoryRecords.readableRecords(buffer);
+        abortedTransactions = List.of(
+            new FetchResponseData.AbortedTransaction().setFirstOffset(0).setProducerId(1),
+            new FetchResponseData.AbortedTransaction().setFirstOffset(2).setProducerId(1),
+            new FetchResponseData.AbortedTransaction().setFirstOffset(4).setProducerId(1)
+        );
+
+        actual = sharePartition.fetchAbortedTransactionRecordBatches(records.batches(), abortedTransactions);
+        assertEquals(3, actual.size());
+        assertEquals(0, actual.get(0).baseOffset());
+        assertEquals(0, actual.get(0).lastOffset());
+        assertEquals(1, actual.get(0).producerId());
+        assertEquals(2, actual.get(1).baseOffset());
+        assertEquals(2, actual.get(1).lastOffset());
+        assertEquals(1, actual.get(1).producerId());
+        assertEquals(4, actual.get(2).baseOffset());
+        assertEquals(4, actual.get(2).lastOffset());
+        assertEquals(1, actual.get(2).producerId());
+
+        // Case 3: The producer id of records is different, so they should not be archived,
+        buffer = ByteBuffer.allocate(1024);
+        // We are creating 10 transactional records followed by a ABORT marker record for producerId 2.
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 10, 2, 0);
+        buffer.flip();
+        records = MemoryRecords.readableRecords(buffer);
+        abortedTransactions = List.of(
+            new FetchResponseData.AbortedTransaction().setFirstOffset(0).setProducerId(1)
+        );
+
+        actual = sharePartition.fetchAbortedTransactionRecordBatches(records.batches(), abortedTransactions);
+        assertEquals(0, actual.size());
+    }
+
+    @Test
+    public void testFetchAbortedTransactionRecordBatchesForAbortedAndCommittedTransactions() {
+        SharePartition sharePartition = SharePartitionBuilder.builder()
+            .withState(SharePartitionState.ACTIVE)
+            .build();
+
+        ByteBuffer buffer = ByteBuffer.allocate(1024);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 1, 0);
+        newTransactionalRecords(buffer, ControlRecordType.COMMIT, 2, 2, 3);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 2, 6);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 1, 9);
+        newTransactionalRecords(buffer, ControlRecordType.COMMIT, 2, 1, 12);
+        newTransactionalRecords(buffer, ControlRecordType.ABORT, 2, 1, 15);
+        buffer.flip();
+        Records records = MemoryRecords.readableRecords(buffer);
+
+        // Case 1 - Aborted transactions does not contain the record batch from 4-5 with producer id 2.

Review Comment:
   Hmm, batch 2-3 have produce id 2.



##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);
+                assertEquals(0, records.count());
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                // Message 3 would be returned by this poll.
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 3", new String(record.value()));
+                // We will make Message 3 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // Precautionary poll so that the aborted transaction for Message 4 is fetched by the consumer.
+                shareConsumer.poll(Duration.ofMillis(5000));

Review Comment:
   Could we do `waitUntil` to avoid having to wait for 5 secs?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2032527377


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,307 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 2500L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // We will not receive any records since the transaction was aborted.
+                records = waitedPoll(shareConsumer, 2500L, 0);
+                assertEquals(0, records.count());
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 2500L, 1);
+                // Message 3 would be returned by this poll.
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 3", new String(record.value()));
+                // We will make Message 3 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // Precautionary poll so that the aborted transaction for Message 4 is fetched by the consumer.
+                shareConsumer.poll(Duration.ofMillis(5000));

Review Comment:
   You are right, we would be are forced to wait for 2.5 secs. I have changed the testing for this scenario by setting an acknowledgement commit callback to verify that aborted marker offset for Message 4 (7L) is fetched and acknowledged by the consumer. I have used `TestUtils.waitForCondition()` and done a poll for `500ms` for the same. This will verify that transaction corresponding to message 4 is fetched by the consumer and also takes much less time now.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "junrao (via GitHub)" <gi...@apache.org>.
junrao commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2034133896


##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerTest.java:
##########
@@ -2090,6 +2091,335 @@ public void testComplexShareConsumer() throws Exception {
         verifyShareGroupStateTopicRecordsProduced();
     }
 
+    @ClusterTest
+    public void testReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 5000L, 8);
+            // 5th and 10th message transaction was aborted, hence they won't be included in the fetched records.
+            assertEquals(8, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                if (messageCounter % 5 == 0)
+                    messageCounter++;
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testReadUncommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            produceCommittedAndAbortedTransactionsInInterval(transactionalProducer, 10, 5);
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 5000L, 10);
+            // Even though 5th and 10th message transaction was aborted, they will be included in the fetched records since IsolationLevel is READ_UNCOMMITTED.
+            assertEquals(10, records.count());
+            int messageCounter = 1;
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                assertEquals("Message " + messageCounter, new String(record.value()));
+                messageCounter++;
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadUncommittedToReadCommittedIsolationLevel() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_uncommitted");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 5000L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                records = waitedPoll(shareConsumer, 5000L, 1);
+                assertEquals(1, records.count());
+                record = records.iterator().next();
+                assertEquals("Message 2", new String(record.value()));
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Third transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 3");
+                // Fourth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 4");
+
+                records = waitedPoll(shareConsumer, 5000L, 2);
+                // Message 3 and Message 4 would be returned by this poll.
+                assertEquals(2, records.count());
+                Iterator<ConsumerRecord<byte[], byte[]>> recordIterator = records.iterator();
+                record = recordIterator.next();
+                assertEquals("Message 3", new String(record.value()));
+                record = recordIterator.next();
+                assertEquals("Message 4", new String(record.value()));
+                // We will make Message 3 and Message 4 available for re-consumption.
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.RELEASE));
+                shareConsumer.commitSync();
+
+                // We are altering IsolationLevel to READ_COMMITTED now. We will only read committed transactions now.
+                alterShareIsolationLevel("group1", "read_committed");
+
+                // Fifth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 5");
+                // Sixth transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 6");
+                // Seventh transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 7");
+                // Eighth transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 8");
+
+                // Since isolation level is READ_COMMITTED, we can consume Message 3 (committed transaction that was released), Message 5 and Message 8.
+                // We cannot consume Message 4 (aborted transaction that was released), Message 6 and Message 7 since they were aborted.
+                List<String> messages = new ArrayList<>();
+                TestUtils.waitForCondition(() -> {
+                    ConsumerRecords<byte[], byte[]> pollRecords = shareConsumer.poll(Duration.ofMillis(5000));
+                    if (pollRecords.count() > 0) {
+                        for (ConsumerRecord<byte[], byte[]> pollRecord : pollRecords)
+                            messages.add(new String(pollRecord.value()));
+                        pollRecords.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                        shareConsumer.commitSync();
+                    }
+                    return messages.size() == 3;
+                }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Failed to consume all records post altering share isolation level");
+
+                assertEquals("Message 3", messages.get(0));
+                assertEquals("Message 5", messages.get(1));
+                assertEquals("Message 8", messages.get(2));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                transactionalProducer.close();
+            }
+        }
+        verifyShareGroupStateTopicRecordsProduced();
+    }
+
+    @ClusterTest
+    public void testAlterReadCommittedToReadUncommittedIsolationLevelWithReleaseAck() {
+        alterShareAutoOffsetReset("group1", "earliest");
+        alterShareIsolationLevel("group1", "read_committed");
+        try (Producer<byte[], byte[]> transactionalProducer = createProducer("T1");
+             ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer("group1")) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+            transactionalProducer.initTransactions();
+
+            try {
+                // First transaction is committed.
+                produceCommittedTransaction(transactionalProducer, "Message 1");
+
+                ConsumerRecords<byte[], byte[]> records = waitedPoll(shareConsumer, 5000L, 1);
+                assertEquals(1, records.count());
+                ConsumerRecord<byte[], byte[]> record = records.iterator().next();
+                assertEquals("Message 1", new String(record.value()));
+                assertEquals(tp.topic(), record.topic());
+                assertEquals(tp.partition(), record.partition());
+                records.forEach(consumedRecord -> shareConsumer.acknowledge(consumedRecord, AcknowledgeType.ACCEPT));
+                shareConsumer.commitSync();
+
+                // Second transaction is aborted.
+                produceAbortedTransaction(transactionalProducer, "Message 2");
+
+                // Setting the acknowledgement commit callback to verify acknowledgement completion.
+                Map<TopicPartition, Set<Long>> partitionOffsetsMap = new HashMap<>();
+                shareConsumer.setAcknowledgementCommitCallback(new TestableAcknowledgementCommitCallback(partitionOffsetsMap, new HashMap<>()));

Review Comment:
   Could we fix `TestableAcknowledgementCommitCallback.onComplete `so that it only populates `partitionExceptionMap` is `exception` is not null?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "AndrewJSchofield (via GitHub)" <gi...@apache.org>.
AndrewJSchofield commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2026988588


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,241 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {

Review Comment:
   No, I don't think it should.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2029322081


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,182 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+        List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+        for (RecordBatch recordBatch : recordsToArchive) {
+            // Archive the offsets/batches in the cached state.
+            NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        }
+        return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+    }
+
+    /**
+     * This function filters out the offsets present in the acquired records list that are also a part of batches that need to be archived.
+     * It follows an iterative refinement of acquired records to eliminate batches to be archived.
+     * @param acquiredRecordsList The list containing acquired records. This list is sorted by the firstOffset of the acquired batch.
+     * @param batchesToArchive The list containing record batches to archive. This list is sorted by the baseOffset of the record batch.
+     * @return The list containing filtered acquired records offsets.
+     */
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecordsList,
+        List<RecordBatch> batchesToArchive
+    ) {
+        List<AcquiredRecords> result = new ArrayList<>();
+        for (AcquiredRecords acquiredRecords : acquiredRecordsList) {
+            List<AcquiredRecords> tempAcquiredRecordsList = new ArrayList<>();
+            tempAcquiredRecordsList.add(acquiredRecords);
+            for (RecordBatch batchToArchive : batchesToArchive) {

Review Comment:
   I don't think a single pass will work. Because there could be a case that each element of `acquiredRecordsList` breaks up as we pass the `batchesToArchive`, and in doing so we might need to refine that element into parts. These parts need iteration to refine them further, if required. Hence, I think we need the double pass here.



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2030219266


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2505,182 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+        List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+        for (RecordBatch recordBatch : recordsToArchive) {
+            // Archive the offsets/batches in the cached state.
+            NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+            archiveRecords(recordBatch.baseOffset(), recordBatch.lastOffset() + 1, subMap, RecordState.ACQUIRED);
+        }
+        return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+    }
+
+    /**
+     * This function filters out the offsets present in the acquired records list that are also a part of batches that need to be archived.
+     * It follows an iterative refinement of acquired records to eliminate batches to be archived.
+     * @param acquiredRecordsList The list containing acquired records. This list is sorted by the firstOffset of the acquired batch.
+     * @param batchesToArchive The list containing record batches to archive. This list is sorted by the baseOffset of the record batch.
+     * @return The list containing filtered acquired records offsets.
+     */
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecordsList,
+        List<RecordBatch> batchesToArchive
+    ) {
+        List<AcquiredRecords> result = new ArrayList<>();
+        for (AcquiredRecords acquiredRecords : acquiredRecordsList) {
+            List<AcquiredRecords> tempAcquiredRecordsList = new ArrayList<>();
+            tempAcquiredRecordsList.add(acquiredRecords);
+            for (RecordBatch batchToArchive : batchesToArchive) {

Review Comment:
   Hi @junrao , thanks for this approach. I agree that variables in this approach are more intuitive and it looks more readable. I have taken your changes and made a few changes to the code comments (in line with your code).
   



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "apoorvmittal10 (via GitHub)" <gi...@apache.org>.
apoorvmittal10 commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013434152


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions == null || abortedTransactions.isEmpty())
+                return null;
+
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = new PriorityQueue<>(
+                abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+            );
+            abortedTransactionsHeap.addAll(abortedTransactions);
+            return abortedTransactionsHeap;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        lock.writeLock().lock();
+        try {
+            return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        lock.writeLock().lock();

Review Comment:
   Why the code should have any lock?



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions == null || abortedTransactions.isEmpty())
+                return null;
+
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = new PriorityQueue<>(
+                abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+            );
+            abortedTransactionsHeap.addAll(abortedTransactions);
+            return abortedTransactionsHeap;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        lock.writeLock().lock();
+        try {
+            return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        lock.writeLock().lock();
+        try {
+            if (!batch.isControlBatch())
+                return false;
+
+            Iterator<Record> batchIterator = batch.iterator();
+            if (!batchIterator.hasNext())
+                return false;
+
+            Record firstRecord = batchIterator.next();
+            return ControlRecordType.ABORT == ControlRecordType.parse(firstRecord.key());

Review Comment:
   Will it result in reading the records in memory? We should not be reading records in broker rather should rely on client to update. We should only read batch headers and not the records.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2497,259 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords filterAbortedTransactionalAcquiredRecords(FetchPartitionData fetchPartitionData, FetchIsolation isolationLevel, ShareAcquiredRecords shareAcquiredRecords) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED)
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation type by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalRecords(fetchPartitionData.records.batches(), shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions);
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            if (abortedTransactions.isEmpty())
+                return acquiredRecords;
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMap(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetAcquiredBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteAcquiredBatch(inFlightBatch);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archivePerOffsetAcquiredBatchRecords(InFlightBatch inFlightBatch, long startOffsetToArchive, long endOffsetToArchive) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving offset tracked batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            for (Map.Entry<Long, InFlightState> offsetState : inFlightBatch.offsetState().entrySet()) {
+                if (offsetState.getKey() < startOffsetToArchive) {
+                    continue;
+                }
+                if (offsetState.getKey() > endOffsetToArchive) {
+                    // No further offsets to process.
+                    break;
+                }
+                if (offsetState.getValue().state != RecordState.ACQUIRED) {
+                    continue;
+                }
+                offsetState.getValue().archive(EMPTY_MEMBER_ID);
+                offsetState.getValue().cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveCompleteAcquiredBatch(InFlightBatch inFlightBatch) {
+        lock.writeLock().lock();
+        try {
+            log.trace("Archiving complete batch: {} for the share partition: {}-{} since it was a part of aborted transaction", inFlightBatch, groupId, topicIdPartition);
+            if (inFlightBatch.batchState() == RecordState.ACQUIRED) {
+                // Change the state of complete batch since the same state exists for the entire inFlight batch.
+                inFlightBatch.archiveBatch(EMPTY_MEMBER_ID);
+                inFlightBatch.batchState.cancelAndClearAcquisitionLockTimeoutTask();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private NavigableMap<Long, InFlightBatch> fetchSubMap(RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorOffset = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorOffset == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorOffset.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        Optional<List<FetchResponseData.AbortedTransaction>> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap = abortedTransactionsHeap(abortedTransactions.get());
+            Set<Long> abortedProducerIds = new HashSet<>();
+            List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+            for (RecordBatch currentBatch : batches) {
+                if (currentBatch.hasProducerId()) {
+                    // remove from the aborted transactions queue, all aborted transactions which have begun
+                    // before the current batch's last offset and add the associated producerIds to the
+                    // aborted producer set
+                    if (abortedTransactionsHeap != null) {
+                        while (!abortedTransactionsHeap.isEmpty() && abortedTransactionsHeap.peek().firstOffset() <= currentBatch.lastOffset()) {
+                            FetchResponseData.AbortedTransaction abortedTransaction = abortedTransactionsHeap.poll();
+                            abortedProducerIds.add(abortedTransaction.producerId());
+                        }
+                    }
+                    long producerId = currentBatch.producerId();
+                    if (containsAbortMarker(currentBatch)) {
+                        abortedProducerIds.remove(producerId);
+                    } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                        log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                            "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                        recordsToArchive.add(currentBatch);
+                    }
+                }
+            }
+            return recordsToArchive;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> abortedTransactionsHeap(List<FetchResponseData.AbortedTransaction> abortedTransactions) {

Review Comment:
   Can you please write the high level approach for the handling in PR description?



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2013442972


##########
core/src/main/scala/kafka/server/KafkaApis.scala:
##########
@@ -104,7 +104,8 @@ class KafkaApis(val requestChannel: RequestChannel,
                 time: Time,
                 val tokenManager: DelegationTokenManager,
                 val apiVersionManager: ApiVersionManager,
-                val clientMetricsManager: ClientMetricsManager
+                val clientMetricsManager: ClientMetricsManager,
+                val groupConfigManager: GroupConfigManager

Review Comment:
   sounds good to me. I am happy doing it either way



-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

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


-- 
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: jira-unsubscribe@kafka.apache.org

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


Re: [PR] KAFKA-16729: Support isolation level for share consumer [kafka]

Posted by "adixitconfluent (via GitHub)" <gi...@apache.org>.
adixitconfluent commented on code in PR #19261:
URL: https://github.com/apache/kafka/pull/19261#discussion_r2022946269


##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2504,205 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), RecordState.ACQUIRED);
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteBatch(inFlightBatch, RecordState.ACQUIRED);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * This function fetches the sub map from cachedState where all the offset details present in the recordBatch can be referred to
+     * OR it gives an exception if those offsets are not present in cachedState.
+     * @param recordBatch The record batch for which we want to find the sub map.
+     * @return the sub map containing all the offset details.
+     */
+    private NavigableMap<Long, InFlightBatch> fetchSubMapOrException(RecordBatch recordBatch) {
+        lock.readLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorEntry = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorEntry == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorEntry.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = orderedAbortedTransactions(abortedTransactions);
+        Set<Long> abortedProducerIds = new HashSet<>();
+        List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+        for (RecordBatch currentBatch : batches) {
+            if (currentBatch.hasProducerId()) {
+                // remove from the aborted transactions queue, all aborted transactions which have begun before the
+                // current batch's last offset and add the associated producerIds to the aborted producer set.
+                while (!orderedAbortedTransactions.isEmpty() && orderedAbortedTransactions.peek().firstOffset() <= currentBatch.lastOffset()) {
+                    FetchResponseData.AbortedTransaction abortedTransaction = orderedAbortedTransactions.poll();
+                    abortedProducerIds.add(abortedTransaction.producerId());
+                }
+                long producerId = currentBatch.producerId();
+                if (containsAbortMarker(currentBatch)) {
+                    abortedProducerIds.remove(producerId);
+                } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                    log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                        "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                    recordsToArchive.add(currentBatch);

Review Comment:
   I have added unit tests for different scenarios corresponding to this code.



##########
core/src/main/java/kafka/server/share/SharePartition.java:
##########
@@ -2484,6 +2504,205 @@ private long startOffsetDuringInitialization(long partitionDataStartOffset) thro
         }
     }
 
+    private ShareAcquiredRecords maybeFilterAbortedTransactionalAcquiredRecords(
+        FetchPartitionData fetchPartitionData,
+        FetchIsolation isolationLevel,
+        ShareAcquiredRecords shareAcquiredRecords
+    ) {
+        if (isolationLevel != FetchIsolation.TXN_COMMITTED || fetchPartitionData.abortedTransactions.isEmpty() || fetchPartitionData.abortedTransactions.get().isEmpty())
+            return shareAcquiredRecords;
+        // When FetchIsolation.TXN_COMMITTED is used as isolation level by the share group, we need to filter any
+        // transactions that were aborted/did not commit due to timeout.
+        List<AcquiredRecords> result = filterAbortedTransactionalAcquiredRecords(fetchPartitionData.records.batches(),
+            shareAcquiredRecords.acquiredRecords(), fetchPartitionData.abortedTransactions.get());
+        int acquiredCount = 0;
+        for (AcquiredRecords records : result) {
+            acquiredCount += (int) (records.lastOffset() - records.firstOffset() + 1);
+        }
+        return new ShareAcquiredRecords(result, acquiredCount);
+    }
+
+    private List<AcquiredRecords> filterAbortedTransactionalAcquiredRecords(
+        Iterable<? extends RecordBatch> batches,
+        List<AcquiredRecords> acquiredRecords,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        lock.writeLock().lock();
+        try {
+            // The record batches that need to be archived in cachedState because they were a part of aborted transactions.
+            List<RecordBatch> recordsToArchive = fetchAbortedTransactionRecordBatches(batches, abortedTransactions);
+            for (RecordBatch recordBatch : recordsToArchive) {
+                // Archive the offsets/batches in the cached state.
+                NavigableMap<Long, InFlightBatch> subMap = fetchSubMapOrException(recordBatch);
+                archiveAcquiredBatchRecords(subMap, recordBatch);
+            }
+            return filterRecordBatchesFromAcquiredRecords(acquiredRecords, recordsToArchive);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<AcquiredRecords> filterRecordBatchesFromAcquiredRecords(
+        List<AcquiredRecords> acquiredRecords,
+        List<RecordBatch> recordsToArchive
+    ) {
+        lock.writeLock().lock();
+        try {
+            List<AcquiredRecords> result = new ArrayList<>();
+
+            for (AcquiredRecords acquiredRecord : acquiredRecords) {
+                List<AcquiredRecords> tempAcquiredRecords = new ArrayList<>();
+                tempAcquiredRecords.add(acquiredRecord);
+                for (RecordBatch recordBatch : recordsToArchive) {
+                    List<AcquiredRecords> newAcquiredRecords = new ArrayList<>();
+                    for (AcquiredRecords temp : tempAcquiredRecords) {
+                        // Check if record batch overlaps with the acquired records.
+                        if (temp.firstOffset() <= recordBatch.lastOffset() && temp.lastOffset() >= recordBatch.baseOffset()) {
+                            // Split the acquired record into parts before, inside, and after the overlapping record batch.
+                            if (temp.firstOffset() < recordBatch.baseOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(temp.firstOffset())
+                                    .setLastOffset(recordBatch.baseOffset() - 1)
+                                    .setDeliveryCount((short) 1));
+                            }
+                            if (temp.lastOffset() > recordBatch.lastOffset()) {
+                                newAcquiredRecords.add(new AcquiredRecords()
+                                    .setFirstOffset(recordBatch.lastOffset() + 1)
+                                    .setLastOffset(temp.lastOffset())
+                                    .setDeliveryCount((short) 1));
+                            }
+                        } else {
+                            newAcquiredRecords.add(temp);
+                        }
+                    }
+                    tempAcquiredRecords = newAcquiredRecords;
+                }
+                result.addAll(tempAcquiredRecords);
+            }
+            return result;
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    private void archiveAcquiredBatchRecords(NavigableMap<Long, InFlightBatch> subMap, RecordBatch recordBatch) {
+        lock.writeLock().lock();
+        try {
+            // The fetched batch either is exact fetch equivalent batch (mostly), subset
+            // or spans over multiple fetched batches. The state can vary per offset itself from
+            // the fetched batch in case of subset.
+            for (Map.Entry<Long, InFlightBatch> entry : subMap.entrySet()) {
+                InFlightBatch inFlightBatch = entry.getValue();
+
+                // If startOffset has moved ahead of the in-flight batch, skip the batch.
+                if (inFlightBatch.lastOffset() < startOffset) {
+                    log.trace("All offsets in the inflight batch {} are already archived: {}-{}",
+                        inFlightBatch, groupId, topicIdPartition);
+                    continue;
+                }
+
+                // Determine if the in-flight batch is a full match from the request batch.
+                boolean fullMatch = checkForFullMatch(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset());
+
+                // Maintain state per offset if the inflight batch is not a full match or the
+                // offset state is managed for this in-flight batch.
+                if (!fullMatch || inFlightBatch.offsetState() != null) {
+                    log.debug("Subset or offset tracked batch record found for record,"
+                            + " batch: {}, request offsets - first: {}, last: {} for the share partition: {}-{}",
+                        inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), groupId, topicIdPartition);
+                    if (inFlightBatch.offsetState() == null) {
+                        // The record batch is a subset and requires per offset state hence initialize
+                        // the offsets state in the in-flight batch.
+                        inFlightBatch.maybeInitializeOffsetStateUpdate();
+                    }
+                    archivePerOffsetBatchRecords(inFlightBatch, recordBatch.baseOffset(), recordBatch.lastOffset(), RecordState.ACQUIRED);
+                    continue;
+                }
+                // The in-flight batch is a full match hence change the state of the complete batch.
+                archiveCompleteBatch(inFlightBatch, RecordState.ACQUIRED);
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * This function fetches the sub map from cachedState where all the offset details present in the recordBatch can be referred to
+     * OR it gives an exception if those offsets are not present in cachedState.
+     * @param recordBatch The record batch for which we want to find the sub map.
+     * @return the sub map containing all the offset details.
+     */
+    private NavigableMap<Long, InFlightBatch> fetchSubMapOrException(RecordBatch recordBatch) {
+        lock.readLock().lock();
+        try {
+            Map.Entry<Long, InFlightBatch> floorEntry = cachedState.floorEntry(recordBatch.baseOffset());
+            if (floorEntry == null) {
+                log.debug("Fetched batch record {} not found for share partition: {}-{}", recordBatch, groupId,
+                    topicIdPartition);
+                throw new InvalidRecordStateException(
+                    "Batch record not found. The request batch offsets are not found in the cache.");
+            }
+            return cachedState.subMap(floorEntry.getKey(), true, recordBatch.lastOffset(), true);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    // Visible for testing.
+    List<RecordBatch> fetchAbortedTransactionRecordBatches(
+        Iterable<? extends RecordBatch> batches,
+        List<FetchResponseData.AbortedTransaction> abortedTransactions
+    ) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = orderedAbortedTransactions(abortedTransactions);
+        Set<Long> abortedProducerIds = new HashSet<>();
+        List<RecordBatch> recordsToArchive = new ArrayList<>();
+
+        for (RecordBatch currentBatch : batches) {
+            if (currentBatch.hasProducerId()) {
+                // remove from the aborted transactions queue, all aborted transactions which have begun before the
+                // current batch's last offset and add the associated producerIds to the aborted producer set.
+                while (!orderedAbortedTransactions.isEmpty() && orderedAbortedTransactions.peek().firstOffset() <= currentBatch.lastOffset()) {
+                    FetchResponseData.AbortedTransaction abortedTransaction = orderedAbortedTransactions.poll();
+                    abortedProducerIds.add(abortedTransaction.producerId());
+                }
+                long producerId = currentBatch.producerId();
+                if (containsAbortMarker(currentBatch)) {
+                    abortedProducerIds.remove(producerId);
+                } else if (isBatchAborted(currentBatch, abortedProducerIds)) {
+                    log.debug("Skipping aborted record batch for share partition: {}-{} with producerId {} and " +
+                        "offsets {} to {}", groupId, topicIdPartition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
+                    recordsToArchive.add(currentBatch);
+                }
+            }
+        }
+        return recordsToArchive;
+    }
+
+    private PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions(List<FetchResponseData.AbortedTransaction> abortedTransactions) {
+        PriorityQueue<FetchResponseData.AbortedTransaction> orderedAbortedTransactions = new PriorityQueue<>(
+            abortedTransactions.size(), Comparator.comparingLong(FetchResponseData.AbortedTransaction::firstOffset)
+        );
+        orderedAbortedTransactions.addAll(abortedTransactions);
+        return orderedAbortedTransactions;
+    }
+
+    private boolean isBatchAborted(RecordBatch batch, Set<Long> abortedProducerIds) {
+        return batch.isTransactional() && abortedProducerIds.contains(batch.producerId());
+    }
+
+    private boolean containsAbortMarker(RecordBatch batch) {
+        if (!batch.isControlBatch())
+            return false;
+
+        Iterator<Record> batchIterator = batch.iterator();
+        if (!batchIterator.hasNext())
+            return false;
+
+        Record firstRecord = batchIterator.next();
+        return ControlRecordType.ABORT == ControlRecordType.parse(firstRecord.key());
+    }

Review Comment:
   I have added unit tests for different scenarios corresponding to this code.



-- 
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: jira-unsubscribe@kafka.apache.org

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