@platformatic/kafka 2.10.0-alpha.1 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apis/metadata/api-versions-v3.js +8 -1
- package/dist/clients/admin/admin.js +24 -11
- package/dist/clients/admin/options.d.ts +58 -2
- package/dist/clients/admin/options.js +38 -7
- package/dist/clients/admin/types.d.ts +7 -1
- package/dist/clients/base/base.js +26 -8
- package/dist/clients/consumer/consumer.d.ts +2 -1
- package/dist/clients/consumer/consumer.js +107 -5
- package/dist/clients/consumer/messages-stream.js +59 -9
- package/dist/clients/consumer/options.d.ts +20 -0
- package/dist/clients/consumer/options.js +1 -0
- package/dist/clients/consumer/types.d.ts +7 -0
- package/dist/clients/producer/producer.js +48 -42
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +4 -0
- package/dist/network/utils.js +26 -8
- package/dist/protocol/records.d.ts +3 -0
- package/dist/registries/abstract.js +27 -8
- package/dist/registries/confluent-schema-registry.js +3 -2
- package/dist/symbols.d.ts +1 -0
- package/dist/symbols.js +1 -0
- package/dist/typescript-4/dist/clients/admin/options.d.ts +58 -2
- package/dist/typescript-4/dist/clients/admin/types.d.ts +7 -1
- package/dist/typescript-4/dist/clients/consumer/consumer.d.ts +2 -1
- package/dist/typescript-4/dist/clients/consumer/options.d.ts +20 -0
- package/dist/typescript-4/dist/clients/consumer/types.d.ts +7 -0
- package/dist/typescript-4/dist/errors.d.ts +1 -0
- package/dist/typescript-4/dist/protocol/records.d.ts +3 -0
- package/dist/typescript-4/dist/symbols.d.ts +1 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -32,8 +32,15 @@ export function createRequest(clientSoftwareName, clientSoftwareVersion) {
|
|
|
32
32
|
zk_migration_ready (3) => BOOLEAN
|
|
33
33
|
*/
|
|
34
34
|
export function parseResponse(_correlationId, apiKey, apiVersion, reader) {
|
|
35
|
+
const errorCode = reader.readInt16();
|
|
36
|
+
// Kafka returns an ApiVersions v0-framed response when v3 is unsupported.
|
|
37
|
+
// Stop before reading the compact array, which would interpret the legacy
|
|
38
|
+
// array length as flexible protocol data and desynchronize the response.
|
|
39
|
+
if (errorCode !== 0) {
|
|
40
|
+
throw new ResponseError(apiKey, apiVersion, { '/': [errorCode, null] }, { errorCode, apiKeys: [], throttleTimeMs: 0 });
|
|
41
|
+
}
|
|
35
42
|
const response = {
|
|
36
|
-
errorCode
|
|
43
|
+
errorCode,
|
|
37
44
|
apiKeys: reader.readArray(r => {
|
|
38
45
|
const apiKey = r.readInt16();
|
|
39
46
|
const api = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
|
|
2
2
|
import { ConfigResourceTypes, FetchIsolationLevels, FindCoordinatorKeyTypes, legacyConsumerGroupStates } from "../../apis/enumerations.js";
|
|
3
3
|
import { adminAclsChannel, adminClientQuotasChannel, adminConfigsChannel, adminConsumerGroupOffsetsChannel, adminGroupsChannel, adminLogDirsChannel, adminOffsetsChannel, adminTopicsChannel, createDiagnosticContext } from "../../diagnostic.js";
|
|
4
|
-
import { findErrorBy, MultipleErrors, UserError } from "../../errors.js";
|
|
4
|
+
import { findErrorBy, MultipleErrors, NetworkError, UserError } from "../../errors.js";
|
|
5
5
|
import { Reader } from "../../protocol/reader.js";
|
|
6
6
|
import { Base, kAfterCreate, kCheckNotClosed, kConnections, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js";
|
|
7
7
|
import { adminListOffsetsOptionsValidator, alterClientQuotasOptionsValidator, alterConfigsOptionsValidator, alterConsumerGroupOffsetsOptionsValidator, createAclsOptionsValidator, createPartitionsOptionsValidator, createTopicsOptionsValidator, deleteAclsOptionsValidator, deleteConsumerGroupOffsetsOptionsValidator, deleteGroupsOptionsValidator, deleteRecordsOptionsValidator, deleteTopicsOptionsValidator, describeAclsOptionsValidator, describeClientQuotasOptionsValidator, describeConfigsOptionsValidator, describeGroupsOptionsValidator, describeLogDirsOptionsValidator, findCoordinatorOptionsValidator, incrementalAlterConfigsOptionsValidator, listConsumerGroupOffsetsOptionsValidator, listGroupsOptionsValidator, listTopicsOptionsValidator, removeMembersFromConsumerGroupOptionsValidator } from "./options.js";
|
|
@@ -366,7 +366,12 @@ export class Admin extends Base {
|
|
|
366
366
|
}
|
|
367
367
|
#getControllerConnection(callback) {
|
|
368
368
|
if (this.#controller) {
|
|
369
|
-
this[kConnections].get(this.#controller,
|
|
369
|
+
this[kConnections].get(this.#controller, (error, connection) => {
|
|
370
|
+
if (NetworkError.isRetryable(error)) {
|
|
371
|
+
this.#controller = null;
|
|
372
|
+
}
|
|
373
|
+
callback(error, connection);
|
|
374
|
+
});
|
|
370
375
|
}
|
|
371
376
|
else {
|
|
372
377
|
this[kGetBootstrapConnection](callback);
|
|
@@ -422,26 +427,34 @@ export class Admin extends Base {
|
|
|
422
427
|
}, 0);
|
|
423
428
|
}, callback);
|
|
424
429
|
}
|
|
430
|
+
#mapTopicAssignments(assignments) {
|
|
431
|
+
const mapped = [];
|
|
432
|
+
for (const { partition, brokers } of assignments ?? []) {
|
|
433
|
+
mapped.push({ partitionIndex: partition, brokerIds: brokers });
|
|
434
|
+
}
|
|
435
|
+
return mapped;
|
|
436
|
+
}
|
|
425
437
|
#createTopics(options, callback) {
|
|
426
438
|
// -1 is required if manual assignments are used. If no manual assignments are used, -1 will default to broker settings.
|
|
427
439
|
const numPartitions = options.partitions ?? -1;
|
|
428
440
|
const replicationFactor = options.replicas ?? -1;
|
|
429
|
-
const assignments =
|
|
441
|
+
const assignments = this.#mapTopicAssignments(options.assignments);
|
|
430
442
|
const configs = options.configs ?? [];
|
|
431
|
-
for (const { partition, brokers } of options.assignments ?? []) {
|
|
432
|
-
assignments.push({ partitionIndex: partition, brokerIds: brokers });
|
|
433
|
-
}
|
|
434
443
|
const requests = [];
|
|
435
444
|
for (const topic of options.topics) {
|
|
445
|
+
if (typeof topic === 'string') {
|
|
446
|
+
requests.push({ name: topic, numPartitions, replicationFactor, assignments, configs });
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
436
449
|
requests.push({
|
|
437
|
-
name: topic,
|
|
438
|
-
numPartitions,
|
|
439
|
-
replicationFactor,
|
|
440
|
-
assignments,
|
|
450
|
+
name: topic.topic,
|
|
451
|
+
numPartitions: topic.partitions ?? numPartitions,
|
|
452
|
+
replicationFactor: topic.replicas ?? replicationFactor,
|
|
453
|
+
assignments: topic.assignments ? this.#mapTopicAssignments(topic.assignments) : assignments,
|
|
441
454
|
configs
|
|
442
455
|
});
|
|
443
456
|
}
|
|
444
|
-
this[kPerformDeduplicated](`createTopics-${options.topics.join(',')}`, deduplicateCallback => {
|
|
457
|
+
this[kPerformDeduplicated](`createTopics-${options.topics.map(topic => (typeof topic === 'string' ? topic : topic.topic)).join(',')}`, deduplicateCallback => {
|
|
445
458
|
this[kPerformWithRetry]('createTopics', retryCallback => {
|
|
446
459
|
this.#getControllerConnection((error, connection) => {
|
|
447
460
|
if (error) {
|
|
@@ -14,8 +14,48 @@ export declare const createTopicOptionsSchema: {
|
|
|
14
14
|
topics: {
|
|
15
15
|
type: string;
|
|
16
16
|
items: {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
oneOf: ({
|
|
18
|
+
type: string;
|
|
19
|
+
pattern: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: string;
|
|
22
|
+
properties: {
|
|
23
|
+
topic: {
|
|
24
|
+
type: string;
|
|
25
|
+
pattern: string;
|
|
26
|
+
};
|
|
27
|
+
partitions: {
|
|
28
|
+
type: string;
|
|
29
|
+
};
|
|
30
|
+
replicas: {
|
|
31
|
+
type: string;
|
|
32
|
+
};
|
|
33
|
+
assignments: {
|
|
34
|
+
type: string;
|
|
35
|
+
items: {
|
|
36
|
+
type: string;
|
|
37
|
+
properties: {
|
|
38
|
+
partition: {
|
|
39
|
+
type: string;
|
|
40
|
+
minimum: number;
|
|
41
|
+
};
|
|
42
|
+
brokers: {
|
|
43
|
+
type: string;
|
|
44
|
+
items: {
|
|
45
|
+
type: string;
|
|
46
|
+
};
|
|
47
|
+
minItems: number;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
required: string[];
|
|
51
|
+
additionalProperties: boolean;
|
|
52
|
+
};
|
|
53
|
+
minItems: number;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
required: string[];
|
|
57
|
+
additionalProperties: boolean;
|
|
58
|
+
})[];
|
|
19
59
|
};
|
|
20
60
|
};
|
|
21
61
|
partitions: {
|
|
@@ -46,6 +86,22 @@ export declare const createTopicOptionsSchema: {
|
|
|
46
86
|
};
|
|
47
87
|
minItems: number;
|
|
48
88
|
};
|
|
89
|
+
configs: {
|
|
90
|
+
type: string;
|
|
91
|
+
items: {
|
|
92
|
+
type: string;
|
|
93
|
+
properties: {
|
|
94
|
+
name: {
|
|
95
|
+
type: string;
|
|
96
|
+
};
|
|
97
|
+
value: {
|
|
98
|
+
type: string[];
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
required: string[];
|
|
102
|
+
additionalProperties: boolean;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
49
105
|
};
|
|
50
106
|
required: string[];
|
|
51
107
|
additionalProperties: boolean;
|
|
@@ -8,24 +8,55 @@ export const groupsProperties = {
|
|
|
8
8
|
minItems: 1
|
|
9
9
|
}
|
|
10
10
|
};
|
|
11
|
+
const createTopicAssignmentsSchema = {
|
|
12
|
+
type: 'array',
|
|
13
|
+
items: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
partition: { type: 'number', minimum: 0 },
|
|
17
|
+
brokers: { type: 'array', items: { type: 'number' }, minItems: 1 }
|
|
18
|
+
},
|
|
19
|
+
required: ['partition', 'brokers'],
|
|
20
|
+
additionalProperties: false
|
|
21
|
+
},
|
|
22
|
+
minItems: 1
|
|
23
|
+
};
|
|
11
24
|
export const createTopicOptionsSchema = {
|
|
12
25
|
type: 'object',
|
|
13
26
|
properties: {
|
|
14
|
-
topics: {
|
|
27
|
+
topics: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
items: {
|
|
30
|
+
oneOf: [
|
|
31
|
+
idProperty,
|
|
32
|
+
{
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
topic: idProperty,
|
|
36
|
+
partitions: { type: 'number' },
|
|
37
|
+
replicas: { type: 'number' },
|
|
38
|
+
assignments: createTopicAssignmentsSchema
|
|
39
|
+
},
|
|
40
|
+
required: ['topic'],
|
|
41
|
+
additionalProperties: false
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
15
46
|
partitions: { type: 'number' },
|
|
16
47
|
replicas: { type: 'number' },
|
|
17
|
-
assignments:
|
|
48
|
+
assignments: createTopicAssignmentsSchema,
|
|
49
|
+
configs: {
|
|
18
50
|
type: 'array',
|
|
19
51
|
items: {
|
|
20
52
|
type: 'object',
|
|
21
53
|
properties: {
|
|
22
|
-
|
|
23
|
-
|
|
54
|
+
name: { type: 'string' },
|
|
55
|
+
value: { type: ['string', 'null'] }
|
|
24
56
|
},
|
|
25
|
-
required: ['
|
|
57
|
+
required: ['name'],
|
|
26
58
|
additionalProperties: false
|
|
27
|
-
}
|
|
28
|
-
minItems: 1
|
|
59
|
+
}
|
|
29
60
|
}
|
|
30
61
|
},
|
|
31
62
|
required: ['topics'],
|
|
@@ -43,8 +43,14 @@ export interface Group extends Omit<GroupBase, 'groupType'> {
|
|
|
43
43
|
}
|
|
44
44
|
export interface AdminOptions extends BaseOptions {
|
|
45
45
|
}
|
|
46
|
+
export interface CreateTopicsTopicOptions {
|
|
47
|
+
topic: string;
|
|
48
|
+
partitions?: number;
|
|
49
|
+
replicas?: number;
|
|
50
|
+
assignments?: BrokerAssignment[];
|
|
51
|
+
}
|
|
46
52
|
export interface CreateTopicsOptions {
|
|
47
|
-
topics: string[];
|
|
53
|
+
topics: (string | CreateTopicsTopicOptions)[];
|
|
48
54
|
partitions?: number;
|
|
49
55
|
replicas?: number;
|
|
50
56
|
assignments?: BrokerAssignment[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
|
|
2
2
|
import * as apis from "../../apis/index.js";
|
|
3
3
|
import { api as apiVersionsV1 } from "../../apis/metadata/api-versions-v1.js";
|
|
4
|
+
import { api as apiVersionsV3 } from "../../apis/metadata/api-versions-v3.js";
|
|
4
5
|
import { baseApisChannel, baseMetadataChannel, createDiagnosticContext, notifyCreation } from "../../diagnostic.js";
|
|
5
6
|
import { findErrorBy, MultipleErrors, NetworkError, UnsupportedApiError, UserError } from "../../errors.js";
|
|
6
7
|
import { TypedEventEmitter } from "../../events.js";
|
|
@@ -47,6 +48,7 @@ export class Base extends TypedEventEmitter {
|
|
|
47
48
|
[kPrometheus];
|
|
48
49
|
#metadata;
|
|
49
50
|
#inflightDeduplications;
|
|
51
|
+
#apiVersionsVersions;
|
|
50
52
|
constructor(options) {
|
|
51
53
|
super();
|
|
52
54
|
this.setMaxListeners(0);
|
|
@@ -54,6 +56,7 @@ export class Base extends TypedEventEmitter {
|
|
|
54
56
|
this[kInstance] = currentInstance++;
|
|
55
57
|
this[kApis] = [];
|
|
56
58
|
this[kContext] = options.context;
|
|
59
|
+
this.#apiVersionsVersions = new WeakMap();
|
|
57
60
|
// Validate options
|
|
58
61
|
this[kOptions] = Object.assign({}, defaultBaseOptions);
|
|
59
62
|
for (const [key, value] of Object.entries(options)) {
|
|
@@ -212,14 +215,29 @@ export class Base extends TypedEventEmitter {
|
|
|
212
215
|
retryCallback(error);
|
|
213
216
|
return;
|
|
214
217
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
const apiVersionsVersion = this.#apiVersionsVersions.get(connection);
|
|
219
|
+
const listApisV1 = () => {
|
|
220
|
+
apiVersionsV1(connection, clientSoftwareName, clientSoftwareVersion, (error, response) => {
|
|
221
|
+
retryCallback(error, response);
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
if (apiVersionsVersion === 1) {
|
|
225
|
+
listApisV1();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
apiVersionsV3(connection, clientSoftwareName, clientSoftwareVersion, (error, response) => {
|
|
229
|
+
if (!error) {
|
|
230
|
+
this.#apiVersionsVersions.set(connection, 3);
|
|
231
|
+
retryCallback(null, response);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (findErrorBy(error, 'apiId', 'UNSUPPORTED_VERSION')) {
|
|
235
|
+
this.#apiVersionsVersions.set(connection, 1);
|
|
236
|
+
listApisV1();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
retryCallback(error);
|
|
240
|
+
});
|
|
223
241
|
}, attempt);
|
|
224
242
|
}, (error, metadata) => {
|
|
225
243
|
if (error) {
|
|
@@ -6,7 +6,7 @@ import { Base, type BaseEvents, kCreateConnectionPool } from '../base/base.ts';
|
|
|
6
6
|
import { type ClusterMetadata } from '../base/types.ts';
|
|
7
7
|
import { MessagesStream } from './messages-stream.ts';
|
|
8
8
|
import { TopicsMap } from './topics-map.ts';
|
|
9
|
-
import { type CommitOptions, type ConsumeOptions, type ConsumerGroupJoinPayload, type ConsumerGroupLeavePayload, type ConsumerGroupRebalancePayload, type ConsumerHeartbeatErrorPayload, type ConsumerHeartbeatPayload, type ConsumerOptions, type FetchOptions, type GetLagOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets, type OffsetsWithTimestamps } from './types.ts';
|
|
9
|
+
import { type CommitOptions, type ConsumeOptions, type ConsumerGroupJoinPayload, type ConsumerGroupLeavePayload, type ConsumerGroupRebalancePayload, type ConsumerHeartbeatErrorPayload, type ConsumerHeartbeatPayload, type ConsumerHeartbeatStalledPayload, type ConsumerOptions, type FetchOptions, type GetLagOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets, type OffsetsWithTimestamps } from './types.ts';
|
|
10
10
|
export interface ConsumerEvents extends BaseEvents {
|
|
11
11
|
'consumer:group:join': (payload: ConsumerGroupJoinPayload) => void;
|
|
12
12
|
'consumer:group:leave': (payload: ConsumerGroupLeavePayload) => void;
|
|
@@ -16,6 +16,7 @@ export interface ConsumerEvents extends BaseEvents {
|
|
|
16
16
|
'consumer:heartbeat:cancel': (payload: ConsumerHeartbeatPayload) => void;
|
|
17
17
|
'consumer:heartbeat:end': (payload?: ConsumerHeartbeatPayload) => void;
|
|
18
18
|
'consumer:heartbeat:error': (payload: ConsumerHeartbeatErrorPayload) => void;
|
|
19
|
+
'consumer:heartbeat:stalled': (payload: ConsumerHeartbeatStalledPayload) => void;
|
|
19
20
|
'consumer:lag': (lag: Offsets) => void;
|
|
20
21
|
'consumer:lag:error': (error: Error) => void;
|
|
21
22
|
}
|
|
@@ -32,6 +32,10 @@ export class Consumer extends Base {
|
|
|
32
32
|
#heartbeatInterval;
|
|
33
33
|
#lastHeartbeatIntervalMs;
|
|
34
34
|
#lastHeartbeat;
|
|
35
|
+
#heartbeatStallTimer;
|
|
36
|
+
#heartbeatStallStart;
|
|
37
|
+
#heartbeatStalled;
|
|
38
|
+
#heartbeatStallError;
|
|
35
39
|
#useConsumerGroupProtocol;
|
|
36
40
|
#memberEpoch;
|
|
37
41
|
#groupRemoteAssignor;
|
|
@@ -77,6 +81,10 @@ export class Consumer extends Base {
|
|
|
77
81
|
this.#heartbeatInterval = null;
|
|
78
82
|
this.#lastHeartbeatIntervalMs = 0;
|
|
79
83
|
this.#lastHeartbeat = null;
|
|
84
|
+
this.#heartbeatStallTimer = null;
|
|
85
|
+
this.#heartbeatStallStart = null;
|
|
86
|
+
this.#heartbeatStalled = false;
|
|
87
|
+
this.#heartbeatStallError = undefined;
|
|
80
88
|
this.#streams = new Set();
|
|
81
89
|
this.#lagMonitoring = null;
|
|
82
90
|
this.#memberEpoch = 0;
|
|
@@ -121,6 +129,7 @@ export class Consumer extends Base {
|
|
|
121
129
|
}
|
|
122
130
|
this[kClosed] = true;
|
|
123
131
|
clearTimeout(this.#lagMonitoring);
|
|
132
|
+
this.#resetHeartbeatStall();
|
|
124
133
|
let closer;
|
|
125
134
|
if (this.#useConsumerGroupProtocol) {
|
|
126
135
|
closer = this.#leaveGroupConsumerProtocol.bind(this);
|
|
@@ -353,6 +362,13 @@ export class Consumer extends Base {
|
|
|
353
362
|
throw validationError;
|
|
354
363
|
}
|
|
355
364
|
this.#lagMonitoring = setTimeout(() => {
|
|
365
|
+
// Skip this tick while mid-(re)join: listOffsets adds load to a coordinator/cluster that
|
|
366
|
+
// may already be struggling, and any lag figure computed against stale/absent assignments
|
|
367
|
+
// would be meaningless anyway. Just reschedule and try again next tick.
|
|
368
|
+
if (!this.isActive()) {
|
|
369
|
+
this.#lagMonitoring.refresh();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
356
372
|
this.getLag(options, () => this.#lagMonitoring.refresh());
|
|
357
373
|
}, interval);
|
|
358
374
|
}
|
|
@@ -424,6 +440,7 @@ export class Consumer extends Base {
|
|
|
424
440
|
return callback[kCallbackPromise];
|
|
425
441
|
}
|
|
426
442
|
this.#membershipActive = false;
|
|
443
|
+
this.#resetHeartbeatStall();
|
|
427
444
|
this.#leaveGroupClassicProtocol(force, error => {
|
|
428
445
|
if (error) {
|
|
429
446
|
this.#membershipActive = true;
|
|
@@ -486,12 +503,15 @@ export class Consumer extends Base {
|
|
|
486
503
|
}
|
|
487
504
|
#fetch(options, callback) {
|
|
488
505
|
const isolationLevel = options.isolationLevel ?? this[kOptions].isolationLevel;
|
|
506
|
+
let refreshMetadata = false;
|
|
489
507
|
this[kPerformWithRetry]('fetch', retryCallback => {
|
|
490
|
-
this[kMetadata]({ topics: this.topics.current }, (error, metadata) => {
|
|
508
|
+
this[kMetadata]({ topics: this.topics.current, forceUpdate: refreshMetadata }, (error, metadata) => {
|
|
491
509
|
if (error) {
|
|
510
|
+
refreshMetadata = NetworkError.isRetryable(error);
|
|
492
511
|
retryCallback(error);
|
|
493
512
|
return;
|
|
494
513
|
}
|
|
514
|
+
refreshMetadata = false;
|
|
495
515
|
const topicIds = this.#topicIdsById(metadata);
|
|
496
516
|
const node = this.#fetchNodeForRequest(metadata, options.node, options.topics, topicIds, Date.now());
|
|
497
517
|
const broker = metadata.brokers.get(node);
|
|
@@ -502,6 +522,7 @@ export class Consumer extends Base {
|
|
|
502
522
|
const pool = options.connectionPool ?? this[kConnections];
|
|
503
523
|
pool.get(broker, (error, connection) => {
|
|
504
524
|
if (error) {
|
|
525
|
+
refreshMetadata = NetworkError.isRetryable(error);
|
|
505
526
|
this.#clearPreferredReadReplicas(options.topics, topicIds);
|
|
506
527
|
this.#fetchSessions.delete(node);
|
|
507
528
|
// When a connection was not available (either interrupted or not available) we
|
|
@@ -517,6 +538,7 @@ export class Consumer extends Base {
|
|
|
517
538
|
}
|
|
518
539
|
this[kGetApi]('Fetch', (error, api) => {
|
|
519
540
|
if (error) {
|
|
541
|
+
refreshMetadata = NetworkError.isRetryable(error);
|
|
520
542
|
retryCallback(error);
|
|
521
543
|
return;
|
|
522
544
|
}
|
|
@@ -608,6 +630,7 @@ export class Consumer extends Base {
|
|
|
608
630
|
}
|
|
609
631
|
}
|
|
610
632
|
if (error) {
|
|
633
|
+
refreshMetadata = NetworkError.isRetryable(error);
|
|
611
634
|
this.#clearPreferredReadReplicas(options.topics, topicIds);
|
|
612
635
|
if (findErrorBy(error, 'apiId', 'FETCH_SESSION_ID_NOT_FOUND') ||
|
|
613
636
|
findErrorBy(error, 'apiId', 'INVALID_FETCH_SESSION_EPOCH') ||
|
|
@@ -697,12 +720,12 @@ export class Consumer extends Base {
|
|
|
697
720
|
callback(error);
|
|
698
721
|
});
|
|
699
722
|
}
|
|
700
|
-
#listOffsets(withTimestamps, options, callback) {
|
|
723
|
+
#listOffsets(withTimestamps, options, callback, forceUpdateMetadata = false) {
|
|
701
724
|
let topics = options.topics;
|
|
702
725
|
if (!topics || topics.length === 0) {
|
|
703
726
|
topics = this.topics.current;
|
|
704
727
|
}
|
|
705
|
-
this[kMetadata]({ topics }, (error, metadata) => {
|
|
728
|
+
this[kMetadata]({ topics, forceUpdate: forceUpdateMetadata }, (error, metadata) => {
|
|
706
729
|
if (error) {
|
|
707
730
|
callback(error);
|
|
708
731
|
return;
|
|
@@ -757,6 +780,11 @@ export class Consumer extends Base {
|
|
|
757
780
|
}, concurrentCallback, 0);
|
|
758
781
|
}, (error, responses) => {
|
|
759
782
|
if (error) {
|
|
783
|
+
if (!forceUpdateMetadata && NetworkError.isRetryable(error)) {
|
|
784
|
+
this.clearMetadata();
|
|
785
|
+
this.#listOffsets(withTimestamps, options, callback, true);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
760
788
|
callback(this.#handleError(error));
|
|
761
789
|
return;
|
|
762
790
|
}
|
|
@@ -881,6 +909,7 @@ export class Consumer extends Base {
|
|
|
881
909
|
return;
|
|
882
910
|
}
|
|
883
911
|
this.emitWithDebug('consumer:heartbeat', 'start', eventPayload);
|
|
912
|
+
this.#trackHeartbeatStall(options);
|
|
884
913
|
this[kGetApi]('Heartbeat', (error, api) => {
|
|
885
914
|
if (error) {
|
|
886
915
|
groupCallback(error);
|
|
@@ -897,21 +926,43 @@ export class Consumer extends Base {
|
|
|
897
926
|
if (error) {
|
|
898
927
|
this.#cancelHeartbeat();
|
|
899
928
|
if (this.#getRejoinError(error)) {
|
|
929
|
+
// A rejoin can legitimately block up to rebalanceTimeout:
|
|
930
|
+
// - extend the stall window by that bound rather than pausing it
|
|
931
|
+
// - a rejoin that never completes still surfaces as a stall
|
|
932
|
+
this.#resetHeartbeatStall();
|
|
933
|
+
this.#trackHeartbeatStall(options, error, options.rebalanceTimeout + this.#heartbeatStallTimeoutFor(options));
|
|
900
934
|
this[kPerformWithRetry]('rejoinGroup', retryCallback => {
|
|
901
935
|
this.#joinGroup(options, retryCallback);
|
|
902
936
|
}, error => {
|
|
903
937
|
if (error) {
|
|
904
938
|
this.emitWithDebug(null, 'error', error);
|
|
939
|
+
if (!this.#heartbeatStalled) {
|
|
940
|
+
// No more heartbeats will be scheduled: restart the stall window without
|
|
941
|
+
// the rejoin grace, keeping the triggering heartbeat error as lastError.
|
|
942
|
+
const heartbeatError = this.#heartbeatStallError;
|
|
943
|
+
this.#resetHeartbeatStall();
|
|
944
|
+
this.#trackHeartbeatStall(options, heartbeatError);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
// Rejoin succeeded: drop the extended window; the next heartbeat opens the normal one.
|
|
949
|
+
this.#resetHeartbeatStall();
|
|
905
950
|
}
|
|
906
951
|
this.emitWithDebug('consumer', 'rejoin');
|
|
907
952
|
}, 0);
|
|
908
953
|
return;
|
|
909
954
|
}
|
|
955
|
+
// The error was not group related, so we schedule another heartbeat.
|
|
956
|
+
// Do this before notifying listeners so they can still cancel it.
|
|
957
|
+
this.#heartbeatInterval = setTimeout(() => {
|
|
958
|
+
this.#heartbeat(options);
|
|
959
|
+
}, options.heartbeatInterval);
|
|
910
960
|
this.emitWithDebug('consumer:heartbeat', 'error', { ...eventPayload, error });
|
|
911
|
-
|
|
961
|
+
this.#trackHeartbeatStall(options, error);
|
|
912
962
|
}
|
|
913
963
|
else {
|
|
914
964
|
this.#lastHeartbeat = new Date();
|
|
965
|
+
this.#resetHeartbeatStall();
|
|
915
966
|
this.emitWithDebug('consumer:heartbeat', 'end', eventPayload);
|
|
916
967
|
}
|
|
917
968
|
this.#heartbeatInterval?.refresh();
|
|
@@ -921,6 +972,47 @@ export class Consumer extends Base {
|
|
|
921
972
|
clearTimeout(this.#heartbeatInterval);
|
|
922
973
|
this.#heartbeatInterval = null;
|
|
923
974
|
}
|
|
975
|
+
#heartbeatStallTimeoutFor(options) {
|
|
976
|
+
return (options.heartbeatStallTimeout ??
|
|
977
|
+
this[kOptions].heartbeatStallTimeout ??
|
|
978
|
+
options.rebalanceTimeout ??
|
|
979
|
+
this[kOptions].rebalanceTimeout ??
|
|
980
|
+
defaultConsumerOptions.rebalanceTimeout);
|
|
981
|
+
}
|
|
982
|
+
// Opens the stall window on the first heartbeat attempt after the last success.
|
|
983
|
+
// The window survives retries and only #resetHeartbeatStall closes it.
|
|
984
|
+
#trackHeartbeatStall(options, error, timeout) {
|
|
985
|
+
if (error) {
|
|
986
|
+
this.#heartbeatStallError = error;
|
|
987
|
+
}
|
|
988
|
+
if (this.#heartbeatStalled || this.#heartbeatStallTimer) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
timeout ??= this.#heartbeatStallTimeoutFor(options);
|
|
992
|
+
this.#heartbeatStallStart = Date.now();
|
|
993
|
+
this.#heartbeatStallTimer = setTimeout(() => {
|
|
994
|
+
this.#heartbeatStallTimer = null;
|
|
995
|
+
// Emit once per stall episode. The next successful heartbeat re-arms the tracking.
|
|
996
|
+
this.#heartbeatStalled = true;
|
|
997
|
+
if (this[kClosed] || !this.#membershipActive) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
this.emitWithDebug('consumer:heartbeat', 'stalled', {
|
|
1001
|
+
lastError: this.#heartbeatStallError,
|
|
1002
|
+
lastHeartbeat: this.#lastHeartbeat,
|
|
1003
|
+
stalledFor: Date.now() - this.#heartbeatStallStart
|
|
1004
|
+
});
|
|
1005
|
+
}, timeout);
|
|
1006
|
+
}
|
|
1007
|
+
#resetHeartbeatStall() {
|
|
1008
|
+
if (this.#heartbeatStallTimer) {
|
|
1009
|
+
clearTimeout(this.#heartbeatStallTimer);
|
|
1010
|
+
this.#heartbeatStallTimer = null;
|
|
1011
|
+
}
|
|
1012
|
+
this.#heartbeatStallStart = null;
|
|
1013
|
+
this.#heartbeatStalled = false;
|
|
1014
|
+
this.#heartbeatStallError = undefined;
|
|
1015
|
+
}
|
|
924
1016
|
#consumerGroupHeartbeat(options, callback) {
|
|
925
1017
|
options.rebalanceTimeout ??= this[kOptions].rebalanceTimeout;
|
|
926
1018
|
consumerHeartbeatChannel.traceCallback(this.#performConsumerGroupHeartbeat, 1, createDiagnosticContext({ client: this, operation: 'consumerGroupHeartbeat' }), this, options, callback);
|
|
@@ -928,6 +1020,7 @@ export class Consumer extends Base {
|
|
|
928
1020
|
#performConsumerGroupHeartbeat(options, callback) {
|
|
929
1021
|
this.#performGroupOperation('consumerGroupHeartbeat', (connection, groupCallback) => {
|
|
930
1022
|
this.emitWithDebug('consumer:heartbeat', 'start');
|
|
1023
|
+
this.#trackHeartbeatStall(options);
|
|
931
1024
|
this[kGetApi]('ConsumerGroupHeartbeat', (error, api) => {
|
|
932
1025
|
if (error) {
|
|
933
1026
|
groupCallback(error);
|
|
@@ -938,6 +1031,7 @@ export class Consumer extends Base {
|
|
|
938
1031
|
});
|
|
939
1032
|
}, (error, response) => {
|
|
940
1033
|
if (this[kClosed]) {
|
|
1034
|
+
this.#resetHeartbeatStall();
|
|
941
1035
|
this.emitWithDebug('consumer:heartbeat', 'end');
|
|
942
1036
|
callback(null);
|
|
943
1037
|
return;
|
|
@@ -945,13 +1039,19 @@ export class Consumer extends Base {
|
|
|
945
1039
|
if (error) {
|
|
946
1040
|
this.#cancelHeartbeat();
|
|
947
1041
|
this.emitWithDebug('consumer:heartbeat', 'error', { error });
|
|
1042
|
+
this.#trackHeartbeatStall(options, error);
|
|
948
1043
|
const fenced = error.response?.errorCode === protocolErrors.FENCED_MEMBER_EPOCH.code;
|
|
949
1044
|
if (fenced) {
|
|
950
1045
|
this.#assignments = [];
|
|
951
1046
|
this.assignments = [];
|
|
952
1047
|
this.#syncPreferredReadReplicas();
|
|
953
1048
|
this.#memberEpoch = 0;
|
|
954
|
-
|
|
1049
|
+
// Retry after a delay, not immediately: if many members get fenced around the same
|
|
1050
|
+
// rebalance, resending ConsumerGroupHeartbeat with no delay at all can flood the
|
|
1051
|
+
// coordinator faster than the group can converge.
|
|
1052
|
+
this.#heartbeatInterval = setTimeout(() => {
|
|
1053
|
+
this.#consumerGroupHeartbeat(options, () => { });
|
|
1054
|
+
}, this.#lastHeartbeatIntervalMs || 1000);
|
|
955
1055
|
callback(error);
|
|
956
1056
|
return;
|
|
957
1057
|
}
|
|
@@ -962,6 +1062,7 @@ export class Consumer extends Base {
|
|
|
962
1062
|
return;
|
|
963
1063
|
}
|
|
964
1064
|
this.#lastHeartbeat = new Date();
|
|
1065
|
+
this.#resetHeartbeatStall();
|
|
965
1066
|
this.#memberEpoch = response.memberEpoch;
|
|
966
1067
|
if (response.memberId) {
|
|
967
1068
|
const changed = this.memberId !== response.memberId;
|
|
@@ -1011,6 +1112,7 @@ export class Consumer extends Base {
|
|
|
1011
1112
|
if (toRevoke.length === 0) {
|
|
1012
1113
|
return;
|
|
1013
1114
|
}
|
|
1115
|
+
this.emitWithDebug('consumer', 'group:rebalance', { groupId: this.groupId });
|
|
1014
1116
|
for (const stream of this.#streams) {
|
|
1015
1117
|
stream.pause();
|
|
1016
1118
|
stream[kAutocommit]();
|