@@ -19,7 +19,7 @@ const {
19
19
partitionKey,
20
20
} = require ( './_common' ) ;
21
21
const { Buffer } = require ( 'buffer' ) ;
22
- const { hrtime } = require ( 'process ' ) ;
22
+ const MessageCache = require ( './_consumer_cache ' ) ;
23
23
24
24
const ConsumerState = Object . freeze ( {
25
25
INIT : 0 ,
@@ -35,218 +35,6 @@ const PartitionAssigners = Object.freeze({
35
35
cooperativeSticky : 'cooperative-sticky' ,
36
36
} ) ;
37
37
38
-
39
- /**
40
- * A PerPartitionMessageCache is a cache for messages for a single partition.
41
- */
42
- class PerPartitionMessageCache {
43
- /* The cache is a list of messages. */
44
- cache = [ ] ;
45
- /* Index of next element to be fetched in the cache. */
46
- currentIndex = 0 ;
47
- /* Whether the cache is stale. */
48
- stale = false ;
49
-
50
- /**
51
- * Returns the number of total elements in the cache.
52
- */
53
- size ( ) {
54
- return this . cache . length ;
55
- }
56
-
57
- /**
58
- * Clears the cache.
59
- */
60
- clear ( ) {
61
- this . cache = [ ] ;
62
- this . currentIndex = 0 ;
63
- this . stale = false ;
64
- }
65
-
66
- /**
67
- * Adds a message to the cache.
68
- */
69
- add ( message ) {
70
- this . cache . push ( message ) ;
71
- }
72
-
73
- /**
74
- * Returns whether the cache is stale.
75
- */
76
- isStale ( ) {
77
- return this . stale ;
78
- }
79
-
80
- /**
81
- * @returns The next element in the cache or null if none exists.
82
- * @warning Does not check for staleness.
83
- */
84
- next ( ) {
85
- return this . currentIndex < this . cache . length ? this . cache [ this . currentIndex ++ ] : null ;
86
- }
87
- }
88
-
89
-
90
- /**
91
- * MessageCache defines a dynamically sized cache for messages.
92
- * Internally, it uses PerPartitionMessageCache to store messages for each partition.
93
- * The capacity is increased or decreased according to whether the last fetch of messages
94
- * was less than the current capacity or saturated the current capacity.
95
- */
96
- class MessageCache {
97
-
98
- constructor ( expiryDurationMs ) {
99
- /* Per partition cache list containing non-empty PPCs */
100
- this . ppcList = [ ] ;
101
- /* Map of topic+partition to PerPartitionMessageCache. */
102
- this . tpToPpc = new Map ( ) ;
103
- /* Index of the current PPC in the ppcList. */
104
- this . currentPpc = 0 ;
105
- /* Maximum size of the cache. (Capacity) */
106
- this . maxSize = 1 ;
107
- /* Number of times the size has been increased in a row, used for accounting for maxSize. */
108
- this . increaseCount = 0 ;
109
- /* Last cached time */
110
- this . cachedTime = hrtime ( ) ;
111
- /* Whether the cache is stale. */
112
- this . stale = false ;
113
- /* Expiry duration for this cache */
114
- this . expiryDurationMs = expiryDurationMs ;
115
- }
116
-
117
- addTopicPartitions ( topicPartitions ) {
118
- if ( this . ppcList . length !== 0 ) {
119
- throw new Error ( 'Cannot add topic partitions to a non-empty cache.' ) ;
120
- }
121
- for ( const topicPartition of topicPartitions ) {
122
- const key = partitionKey ( topicPartition ) ;
123
- this . tpToPpc . set ( key , new PerPartitionMessageCache ( ) ) ;
124
- }
125
- }
126
-
127
- removeTopicPartitions ( topicPartitions = null ) {
128
- if ( this . ppcList . length !== 0 ) {
129
- throw new Error ( 'Cannot remove topic partitions from a non-empty cache.' ) ;
130
- }
131
-
132
- if ( topicPartitions === null ) {
133
- this . tpToPpc . clear ( ) ;
134
- return ;
135
- }
136
- for ( const topicPartition of assignment ) {
137
- const key = partitionKey ( topicPartition ) ;
138
- this . tpToPpc . delete ( key ) ;
139
- }
140
- }
141
-
142
- /**
143
- * Returns whether the cache is stale.
144
- */
145
- isStale ( ) {
146
- if ( this . stale )
147
- return true ;
148
-
149
- const cacheTime = hrtime ( this . cachedTime ) ;
150
- const cacheTimeMs = Math . floor ( cacheTime [ 0 ] * 1000 + cacheTime [ 1 ] / 1000000 ) ;
151
- this . stale = cacheTimeMs > this . expiryDurationMs ;
152
-
153
- // TODO: ideally, local staleness should not lead to global staleness.
154
- // But for now, make it so because seeking to stored offset on local staleness is tricky.
155
- this . stale = this . stale || this . ppcList . some ( cache => cache . isStale ( ) ) ;
156
- return this . stale ;
157
- }
158
-
159
- /**
160
- * Request a size increase.
161
- * It increases the size by 2x, but only if the size is less than 1024,
162
- * only if the size has been requested to be increased twice in a row.
163
- */
164
- increaseMaxSize ( ) {
165
- if ( this . maxSize === 1024 )
166
- return ;
167
-
168
- this . increaseCount ++ ;
169
- if ( this . increaseCount <= 1 )
170
- return ;
171
-
172
- this . maxSize = Math . min ( this . maxSize << 1 , 1024 ) ;
173
- this . increaseCount = 0 ;
174
- }
175
-
176
- /**
177
- * Request a size decrease.
178
- * It decreases the size to 80% of the last received size, with a minimum of 1.
179
- * @param {number } recvdSize - the number of messages received in the last poll.
180
- */
181
- decreaseMaxSize ( recvdSize ) {
182
- this . maxSize = Math . max ( Math . floor ( ( recvdSize * 8 ) / 10 ) , 1 ) ;
183
- this . increaseCount = 0 ;
184
- }
185
-
186
- /**
187
- * Add a single message to the cache.
188
- */
189
- #add( message ) {
190
- const key = partitionKey ( message )
191
- const cache = this . tpToPpc . get ( key ) ;
192
- cache . add ( message ) ;
193
- if ( cache . size ( ) === 1 ) {
194
- this . ppcList . push ( cache ) ;
195
- }
196
- }
197
-
198
- /**
199
- * Adds many messages into the cache, partitioning them as per their toppar.
200
- */
201
- addMessages ( messages ) {
202
- this . stale = false ;
203
- this . cachedTime = hrtime ( ) ;
204
- this . currentPpc = 0 ;
205
- for ( const message of messages )
206
- this . #add( message ) ;
207
-
208
- // TODO: add ppcList sort step.
209
- // Rationale: ideally it's best to consume in the ascending order of timestamps.
210
- }
211
-
212
- /**
213
- * Returns the next element in the cache, or null if none exists.
214
- *
215
- * If the current PPC is exhausted, it moves to the next PPC.
216
- * If all PPCs are exhausted, it returns null.
217
- * @warning Does not check for staleness. That is left up to the user.
218
- */
219
- next ( ) {
220
- if ( this . currentPpc >= this . ppcList . length ) {
221
- return null ;
222
- }
223
-
224
- let next = null ;
225
- while ( next === null && this . currentPpc < this . ppcList . length ) {
226
- next = this . ppcList [ this . currentPpc ] . next ( ) ;
227
- if ( next !== null )
228
- break ;
229
- this . currentPpc ++ ;
230
- }
231
- return next ; // Caller is responsible for triggering fetch logic here if next == null.
232
- }
233
-
234
- /**
235
- * Clears cache completely.
236
- */
237
- clear ( ) {
238
- for ( const cache of this . ppcList ) {
239
- cache . clear ( ) ;
240
- }
241
- this . ppcList = [ ] ;
242
- this . currentPpc = 0 ;
243
- this . maxSize = 1 ;
244
- this . increaseCount = 0 ;
245
- this . stale = false ;
246
- this . cachedTime = hrtime ( ) ;
247
- }
248
- }
249
-
250
38
class Consumer {
251
39
/**
252
40
* The config supplied by the user.
@@ -312,7 +100,6 @@ class Consumer {
312
100
/**
313
101
* A map of topic+partition to the offset that was last consumed.
314
102
* The keys are of the type "<topic>|<partition>".
315
- * This is only populated when we're in the kafkaJS compatibility mode.
316
103
* @type {Map<string, number> }
317
104
*/
318
105
#lastConsumedOffsets = new Map ( ) ;
@@ -358,25 +145,25 @@ class Consumer {
358
145
}
359
146
360
147
/**
361
- * Clear the message cache.
362
- * For simplicity, this always clears the entire message cache rather than being selective.
148
+ * Clear the message cache, and reset to stored positions.
363
149
*
364
- * @param {boolean } seek - whether to seek to the stored offsets after clearing the cache.
365
- * this should be set to true if partitions are retained after this operation.
150
+ * @param {Array<{topic: string, partition: number}>|null } topicPartitions to clear the cache for, if null, then clear all assigned.
366
151
*/
367
- async #clearCacheAndResetPositions( seek = true ) {
368
- /* Seek to stored offset for each topic partition so that if
369
- * we've gotten further along then they have, we can come back. */
370
- if ( seek ) {
371
- const assignment = this . assignment ( ) ;
372
- const seekPromises = [ ] ;
373
- for ( const topicPartitionOffset of assignment ) {
374
- const key = partitionKey ( topicPartitionOffset ) ;
375
- if ( ! this . #lastConsumedOffsets. has ( key ) )
376
- continue ;
152
+ async #clearCacheAndResetPositions( topicPartitions = null ) {
153
+ /* Seek to stored offset for each topic partition. It's possible that we've
154
+ * consumed messages upto N from the internalClient, but the user has stale'd the cache
155
+ * after consuming just k (< N) messages. We seek to k+1. */
156
+
157
+ const clearPartitions = topicPartitions ? topicPartitions : this . assignment ( ) ;
158
+ const seekPromises = [ ] ;
159
+ for ( const topicPartitionOffset of clearPartitions ) {
160
+ const key = partitionKey ( topicPartitionOffset ) ;
161
+ if ( ! this . #lastConsumedOffsets. has ( key ) )
162
+ continue ;
377
163
378
- /* Fire off a seek */
379
- const seekPromise = new Promise ( ( resolve , reject ) => this . #internalClient. seek ( {
164
+ /* Fire off a seek */
165
+ const seekPromise = new Promise ( ( resolve , reject ) => {
166
+ this . #internalClient. seek ( {
380
167
topic : topicPartitionOffset . topic ,
381
168
partition : topicPartitionOffset . partition ,
382
169
offset : + this . #lastConsumedOffsets. get ( key )
@@ -386,18 +173,24 @@ class Consumer {
386
173
} else {
387
174
resolve ( ) ;
388
175
}
389
- } ) ) ;
390
- seekPromises . push ( seekPromise ) ;
391
- }
176
+ } ) ;
392
177
393
- /* TODO: we should cry more about this and render the consumer unusable. */
394
- await Promise . all ( seekPromises ) . catch ( err => this . #logger. error ( "Seek error. This is effectively a fatal error:" + err ) ) ;
178
+ this . #lastConsumedOffsets. delete ( key ) ;
179
+ } ) ;
180
+ seekPromises . push ( seekPromise ) ;
395
181
}
396
182
397
- /* Clear the cache. */
398
- this . #messageCache. clear ( ) ;
399
- /* Clear the offsets - no need to keep them around. */
400
- this . #lastConsumedOffsets. clear ( ) ;
183
+ /* TODO: we should cry more about this and render the consumer unusable. */
184
+ await Promise . all ( seekPromises ) . catch ( err => this . #logger. error ( "Seek error. This is effectively a fatal error:" + err ) ) ;
185
+
186
+
187
+ /* Clear the cache and stored offsets.
188
+ * We need to do this only if topicPartitions = null (global cache expiry).
189
+ * This is because in case of a local cache expiry, MessageCache handles
190
+ * skipping that (and clearing that later before getting new messages). */
191
+ if ( ! topicPartitions ) {
192
+ this . #messageCache. clear ( ) ;
193
+ }
401
194
}
402
195
403
196
/**
@@ -1044,9 +837,14 @@ class Consumer {
1044
837
if ( ! ( await acquireOrLog ( this . #lock, this . #logger) ) )
1045
838
continue ;
1046
839
1047
- /* Invalidate the message cache if needed. */
1048
- if ( this . #messageCache. isStale ( ) ) {
1049
- await this . #clearCacheAndResetPositions( true ) ;
840
+ /* Invalidate the message cache if needed */
841
+ const locallyStale = this . #messageCache. popLocallyStale ( ) ;
842
+ if ( this . #messageCache. isStale ( ) ) { /* global staleness */
843
+ await this . #clearCacheAndResetPositions( ) ;
844
+ await this . #lock. release ( ) ;
845
+ continue ;
846
+ } else if ( locallyStale . length !== 0 ) { /* local staleness */
847
+ await this . #clearCacheAndResetPositions( locallyStale ) ;
1050
848
await this . #lock. release ( ) ;
1051
849
continue ;
1052
850
}
@@ -1153,9 +951,14 @@ class Consumer {
1153
951
if ( ! ( await acquireOrLog ( this . #lock, this . #logger) ) )
1154
952
continue ;
1155
953
1156
- /* Invalidate the message cache if needed. */
1157
- if ( this . #messageCache. isStale ( ) ) {
1158
- await this . #clearCacheAndResetPositions( true ) ;
954
+ /* Invalidate the message cache if needed */
955
+ const locallyStale = this . #messageCache. popLocallyStale ( ) ;
956
+ if ( this . #messageCache. isStale ( ) ) { /* global staleness */
957
+ await this . #clearCacheAndResetPositions( ) ;
958
+ await this . #lock. release ( ) ;
959
+ continue ;
960
+ } else if ( locallyStale . length !== 0 ) { /* local staleness */
961
+ await this . #clearCacheAndResetPositions( locallyStale ) ;
1159
962
await this . #lock. release ( ) ;
1160
963
continue ;
1161
964
}
@@ -1441,14 +1244,21 @@ class Consumer {
1441
1244
offset
1442
1245
} ;
1443
1246
1444
- /* We need a complete reset of the cache if we're seeking to a different offset even for one partition.
1445
- * At a later point, this may be improved at the cost of added complexity of maintaining message generation,
1446
- * or else purging the cache of just those partitions which are seeked. */
1447
- await this . #clearCacheAndResetPositions( true ) ;
1247
+ /* The ideal sequence of events here is to:
1248
+ * 1. Mark the cache as stale so we don't consume from it any further.
1249
+ * 2. Call clearCacheAndResetPositions() for the topic partition, which is supposed
1250
+ * to be called after each cache invalidation.
1251
+ *
1252
+ * However, what (2) does is to pop lastConsumedOffsets[topic partition], and seeks to
1253
+ * the said popped value. Seeking is redundant since we seek here anyway. So, we can skip
1254
+ * the seek by just clearing the lastConsumedOffsets[topic partition].
1255
+ */
1256
+ this . #messageCache. markStale ( [ topicPartition ] ) ;
1257
+ this . #lastConsumedOffsets. delete ( key ) ;
1448
1258
1449
1259
/* It's assumed that topicPartition is already assigned, and thus can be seeked to and committed to.
1450
1260
* Errors are logged to detect bugs in the internal code. */
1451
- /* TODO: is it work awaiting seeks to finish? */
1261
+ /* TODO: is it worth awaiting seeks to finish? */
1452
1262
this . #internalClient. seek ( topicPartitionOffset , 0 , err => err ? this . #logger. error ( err ) : null ) ;
1453
1263
offsetsToCommit . push ( {
1454
1264
topic : topicPartition . topic ,
@@ -1567,8 +1377,9 @@ class Consumer {
1567
1377
}
1568
1378
this . #internalClient. pause ( topics ) ;
1569
1379
1570
- // TODO: make this staleness per-partition, not on a global cache level.
1571
- this . #messageCache. stale = true ;
1380
+ /* Mark the messages in the cache as stale, runInternal* will deal with
1381
+ * making it unusable. */
1382
+ this . #messageCache. markStale ( topics ) ;
1572
1383
1573
1384
topics . map ( JSON . stringify ) . forEach ( topicPartition => this . #pausedPartitions. add ( topicPartition ) ) ;
1574
1385
0 commit comments