@ -23,7 +23,25 @@ import java.util.Map;
@@ -23,7 +23,25 @@ import java.util.Map;
import java.util.Set ;
/ * *
* A class for tracking the topics , partitions , and offsets for the consumer
* A class for tracking the topics , partitions , and offsets for the consumer . A partition
* is "assigned" either directly with { @link # subscribe ( TopicPartition ) } ( manual assignment )
* or with { @link # changePartitionAssignment ( List ) } ( automatic assignment ) .
*
* Once assigned , the partition is not considered "fetchable" until its initial position has
* been set with { @link # seek ( TopicPartition , long ) } . Fetchable partitions track a fetch
* position which is used to set the offset of the next fetch , and a consumed position
* which is the last offset that has been returned to the user . You can suspend fetching
* from a partition through { @link # pause ( TopicPartition ) } without affecting the fetched / consumed
* offsets . The partition will remain unfetchable until the { @link # resume ( TopicPartition ) } is
* used . You can also query the pause state independently with { @link # isPaused ( TopicPartition ) } .
*
* Note that pause state as well as fetch / consumed positions are not preserved when partition
* assignment is changed either with { @link # unsubscribe ( TopicPartition ) } or
* { @link # changePartitionAssignment ( List ) } .
*
* This class also maintains a cache of the latest commit position for each of the assigned
* partitions . This is updated through { @link # committed ( TopicPartition , long ) } and can be used
* to set the initial fetch position ( e . g . { @link Fetcher # resetOffset ( TopicPartition ) } .
* /
public class SubscriptionState {
@ -34,16 +52,7 @@ public class SubscriptionState {
@@ -34,16 +52,7 @@ public class SubscriptionState {
private final Set < TopicPartition > subscribedPartitions ;
/* the list of partitions currently assigned */
private final Set < TopicPartition > assignedPartitions ;
/* the offset exposed to the user */
private final Map < TopicPartition , Long > consumed ;
/* the current point we have fetched up to */
private final Map < TopicPartition , Long > fetched ;
/* the last committed offset for each partition */
private final Map < TopicPartition , Long > committed ;
private final Map < TopicPartition , TopicPartitionState > assignedPartitions ;
/* do we need to request a partition assignment from the coordinator? */
private boolean needsPartitionAssignment ;
@ -51,28 +60,21 @@ public class SubscriptionState {
@@ -51,28 +60,21 @@ public class SubscriptionState {
/* do we need to request the latest committed offsets from the coordinator? */
private boolean needsFetchCommittedOffsets ;
/* Partitions that need to be reset before fetching */
private Map < TopicPartition , OffsetResetStrategy > resetPartitions ;
/* Default offset reset strategy */
private OffsetResetStrategy offsetResetStrategy ;
public SubscriptionState ( OffsetResetStrategy offsetResetStrategy ) {
this . offsetResetStrategy = offsetResetStrategy ;
this . subscribedTopics = new HashSet < String > ( ) ;
this . subscribedPartitions = new HashSet < TopicPartition > ( ) ;
this . assignedPartitions = new HashSet < TopicPartition > ( ) ;
this . consumed = new HashMap < TopicPartition , Long > ( ) ;
this . fetched = new HashMap < TopicPartition , Long > ( ) ;
this . committed = new HashMap < TopicPartition , Long > ( ) ;
private final OffsetResetStrategy defaultResetStrategy ;
public SubscriptionState ( OffsetResetStrategy defaultResetStrategy ) {
this . defaultResetStrategy = defaultResetStrategy ;
this . subscribedTopics = new HashSet < > ( ) ;
this . subscribedPartitions = new HashSet < > ( ) ;
this . assignedPartitions = new HashMap < > ( ) ;
this . needsPartitionAssignment = false ;
this . needsFetchCommittedOffsets = true ; // initialize to true for the consumers to fetch offset upon starting up
this . resetPartitions = new HashMap < TopicPartition , OffsetResetStrategy > ( ) ;
}
public void subscribe ( String topic ) {
if ( this . subscribedPartitions . size ( ) > 0 )
throw new IllegalStateException ( "Subcription to topics and partitions are mutually exclusive" ) ;
if ( ! this . subscribedPartitions . isEmpty ( ) )
throw new IllegalStateException ( "Subs cription to topics and partitions are mutually exclusive" ) ;
if ( ! this . subscribedTopics . contains ( topic ) ) {
this . subscribedTopics . add ( topic ) ;
this . needsPartitionAssignment = true ;
@ -95,10 +97,10 @@ public class SubscriptionState {
@@ -95,10 +97,10 @@ public class SubscriptionState {
}
public void subscribe ( TopicPartition tp ) {
if ( this . subscribedTopics . size ( ) > 0 )
throw new IllegalStateException ( "Subcription to topics and partitions are mutually exclusive" ) ;
if ( ! this . subscribedTopics . isEmpty ( ) )
throw new IllegalStateException ( "Subs cription to topics and partitions are mutually exclusive" ) ;
this . subscribedPartitions . add ( tp ) ;
this . assignedPartitions . add ( tp ) ;
addAssignedPartition ( tp ) ;
}
public void unsubscribe ( TopicPartition partition ) {
@ -110,17 +112,10 @@ public class SubscriptionState {
@@ -110,17 +112,10 @@ public class SubscriptionState {
private void clearPartition ( TopicPartition tp ) {
this . assignedPartitions . remove ( tp ) ;
this . committed . remove ( tp ) ;
this . fetched . remove ( tp ) ;
this . consumed . remove ( tp ) ;
this . resetPartitions . remove ( tp ) ;
}
public void clearAssignment ( ) {
this . assignedPartitions . clear ( ) ;
this . committed . clear ( ) ;
this . fetched . clear ( ) ;
this . consumed . clear ( ) ;
this . needsPartitionAssignment = ! subscribedTopics ( ) . isEmpty ( ) ;
}
@ -129,21 +124,26 @@ public class SubscriptionState {
@@ -129,21 +124,26 @@ public class SubscriptionState {
}
public Long fetched ( TopicPartition tp ) {
return this . fetched . get ( tp ) ;
return assignedState ( tp ) . fetched ;
}
public void fetched ( TopicPartition tp , long offset ) {
if ( ! this . assignedPartitions . contains ( tp ) )
throw new IllegalArgumentException ( "Can't change the fetch position for a partition you are not currently subscribed to." ) ;
this . fetched . put ( tp , offset ) ;
assignedState ( tp ) . fetched ( offset ) ;
}
private TopicPartitionState assignedState ( TopicPartition tp ) {
TopicPartitionState state = this . assignedPartitions . get ( tp ) ;
if ( state = = null )
throw new IllegalStateException ( "No current assignment for partition " + tp ) ;
return state ;
}
public void committed ( TopicPartition tp , long offset ) {
this . committed . put ( tp , offset ) ;
assignedState ( tp ) . committed ( offset ) ;
}
public Long committed ( TopicPartition tp ) {
return this . committed . get ( tp ) ;
return assignedState ( tp ) . committed ;
}
public void needRefreshCommits ( ) {
@ -157,15 +157,22 @@ public class SubscriptionState {
@@ -157,15 +157,22 @@ public class SubscriptionState {
public void commitsRefreshed ( ) {
this . needsFetchCommittedOffsets = false ;
}
public void seek ( TopicPartition tp , long offset ) {
fetched ( tp , offset ) ;
consumed ( tp , offset ) ;
resetPartitions . remove ( tp ) ;
assignedState ( tp ) . seek ( offset ) ;
}
public Set < TopicPartition > assignedPartitions ( ) {
return this . assignedPartitions ;
return this . assignedPartitions . keySet ( ) ;
}
public Set < TopicPartition > fetchablePartitions ( ) {
Set < TopicPartition > fetchable = new HashSet < > ( ) ;
for ( Map . Entry < TopicPartition , TopicPartitionState > entry : assignedPartitions . entrySet ( ) ) {
if ( entry . getValue ( ) . isFetchable ( ) )
fetchable . add ( entry . getKey ( ) ) ;
}
return fetchable ;
}
public boolean partitionsAutoAssigned ( ) {
@ -173,49 +180,52 @@ public class SubscriptionState {
@@ -173,49 +180,52 @@ public class SubscriptionState {
}
public void consumed ( TopicPartition tp , long offset ) {
if ( ! this . assignedPartitions . contains ( tp ) )
throw new IllegalArgumentException ( "Can't change the consumed position for a partition you are not currently subscribed to." ) ;
this . consumed . put ( tp , offset ) ;
assignedState ( tp ) . consumed ( offset ) ;
}
public Long consumed ( TopicPartition partition ) {
return this . consumed . get ( partition ) ;
public Long consumed ( TopicPartition t p) {
return assignedState ( tp ) . consumed ;
}
public Map < TopicPartition , Long > allConsumed ( ) {
return this . consumed ;
Map < TopicPartition , Long > allConsumed = new HashMap < > ( ) ;
for ( Map . Entry < TopicPartition , TopicPartitionState > entry : assignedPartitions . entrySet ( ) ) {
TopicPartitionState state = entry . getValue ( ) ;
if ( state . hasValidPosition )
allConsumed . put ( entry . getKey ( ) , state . consumed ) ;
}
return allConsumed ;
}
public void needOffsetReset ( TopicPartition partition , OffsetResetStrategy offsetResetStrategy ) {
this . resetPartitions . put ( partition , offsetResetStrategy ) ;
this . fetched . remove ( partition ) ;
this . consumed . remove ( partition ) ;
assignedState ( partition ) . awaitReset ( offsetResetStrategy ) ;
}
public void needOffsetReset ( TopicPartition partition ) {
needOffsetReset ( partition , offse tResetStrategy) ;
needOffsetReset ( partition , defaul tResetStrategy) ;
}
public boolean isOffsetResetNeeded ( TopicPartition partition ) {
return resetPartitions . containsKey ( partition ) ;
}
public boolean isOffsetResetNeeded ( ) {
return ! resetPartitions . isEmpty ( ) ;
return assignedState ( partition ) . awaitingReset ;
}
public OffsetResetStrategy resetStrategy ( TopicPartition partition ) {
return resetPartitions . get ( partition ) ;
return assignedState ( partition ) . resetStrategy ;
}
public boolean hasAllFetchPositions ( ) {
return this . fetched . size ( ) > = this . assignedPartitions . size ( ) ;
for ( TopicPartitionState state : assignedPartitions . values ( ) )
if ( ! state . hasValidPosition )
return false ;
return true ;
}
public Set < TopicPartition > missingFetchPositions ( ) {
Set < TopicPartition > copy = new HashSet < TopicPartition > ( this . assignedPartitions ) ;
copy . removeAll ( this . fetched . keySet ( ) ) ;
return copy ;
Set < TopicPartition > missing = new HashSet < > ( this . assignedPartitions . keySet ( ) ) ;
for ( Map . Entry < TopicPartition , TopicPartitionState > entry : assignedPartitions . entrySet ( ) )
if ( ! entry . getValue ( ) . hasValidPosition )
missing . add ( entry . getKey ( ) ) ;
return missing ;
}
public boolean partitionAssignmentNeeded ( ) {
@ -227,9 +237,99 @@ public class SubscriptionState {
@@ -227,9 +237,99 @@ public class SubscriptionState {
if ( ! this . subscribedTopics . contains ( tp . topic ( ) ) )
throw new IllegalArgumentException ( "Assigned partition " + tp + " for non-subscribed topic." ) ;
this . clearAssignment ( ) ;
this . assignedPartitions . addAll ( assignments ) ;
for ( TopicPartition tp : assignments )
addAssignedPartition ( tp ) ;
this . needsPartitionAssignment = false ;
}
public boolean isAssigned ( TopicPartition tp ) {
return assignedPartitions . containsKey ( tp ) ;
}
public boolean isPaused ( TopicPartition tp ) {
return isAssigned ( tp ) & & assignedState ( tp ) . paused ;
}
public boolean isFetchable ( TopicPartition tp ) {
return isAssigned ( tp ) & & assignedState ( tp ) . isFetchable ( ) ;
}
public void pause ( TopicPartition tp ) {
assignedState ( tp ) . pause ( ) ;
}
public void resume ( TopicPartition tp ) {
assignedState ( tp ) . resume ( ) ;
}
private void addAssignedPartition ( TopicPartition tp ) {
this . assignedPartitions . put ( tp , new TopicPartitionState ( ) ) ;
}
private static class TopicPartitionState {
private Long consumed ; // offset exposed to the user
private Long fetched ; // current fetch position
private Long committed ; // last committed position
private boolean hasValidPosition ; // whether we have valid consumed and fetched positions
private boolean paused ; // whether this partition has been paused by the user
private boolean awaitingReset ; // whether we are awaiting reset
private OffsetResetStrategy resetStrategy ; // the reset strategy if awaitingReset is set
public TopicPartitionState ( ) {
this . paused = false ;
this . consumed = null ;
this . fetched = null ;
this . committed = null ;
this . awaitingReset = false ;
this . hasValidPosition = false ;
this . resetStrategy = null ;
}
private void awaitReset ( OffsetResetStrategy strategy ) {
this . awaitingReset = true ;
this . resetStrategy = strategy ;
this . consumed = null ;
this . fetched = null ;
this . hasValidPosition = false ;
}
private void seek ( long offset ) {
this . consumed = offset ;
this . fetched = offset ;
this . awaitingReset = false ;
this . resetStrategy = null ;
this . hasValidPosition = true ;
}
private void fetched ( long offset ) {
if ( ! hasValidPosition )
throw new IllegalStateException ( "Cannot update fetch position without valid consumed/fetched positions" ) ;
this . fetched = offset ;
}
private void consumed ( long offset ) {
if ( ! hasValidPosition )
throw new IllegalStateException ( "Cannot update consumed position without valid consumed/fetched positions" ) ;
this . consumed = offset ;
}
private void committed ( Long offset ) {
this . committed = offset ;
}
private void pause ( ) {
this . paused = true ;
}
private void resume ( ) {
this . paused = false ;
}
private boolean isFetchable ( ) {
return ! paused & & hasValidPosition ;
}
}
}