kafka-ts 0.0.1-beta → 0.0.3-beta
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/.github/workflows/release.yml +17 -0
- package/.prettierrc +3 -2
- package/LICENSE +1 -1
- package/README.md +48 -35
- package/docker-compose.yml +102 -102
- package/examples/package-lock.json +28 -27
- package/examples/package.json +12 -12
- package/examples/src/client.ts +6 -6
- package/examples/src/consumer.ts +9 -8
- package/examples/src/create-topic.ts +23 -16
- package/examples/src/producer.ts +7 -7
- package/examples/src/replicator.ts +4 -4
- package/examples/src/utils/delay.ts +1 -0
- package/examples/src/utils/json.ts +1 -1
- package/examples/tsconfig.json +2 -2
- package/package.json +21 -15
- package/src/__snapshots__/request-handler.test.ts.snap +9 -718
- package/src/api/api-versions.ts +2 -2
- package/src/api/create-topics.ts +2 -2
- package/src/api/delete-topics.ts +2 -2
- package/src/api/fetch.ts +3 -3
- package/src/api/find-coordinator.ts +2 -2
- package/src/api/heartbeat.ts +2 -2
- package/src/api/index.ts +18 -18
- package/src/api/init-producer-id.ts +2 -2
- package/src/api/join-group.ts +3 -3
- package/src/api/leave-group.ts +2 -2
- package/src/api/list-offsets.ts +3 -3
- package/src/api/metadata.ts +3 -3
- package/src/api/offset-commit.ts +2 -2
- package/src/api/offset-fetch.ts +2 -2
- package/src/api/produce.ts +3 -3
- package/src/api/sasl-authenticate.ts +2 -2
- package/src/api/sasl-handshake.ts +2 -2
- package/src/api/sync-group.ts +2 -2
- package/src/broker.ts +9 -9
- package/src/client.ts +6 -6
- package/src/{request-handler.test.ts → cluster.test.ts} +72 -69
- package/src/cluster.ts +7 -7
- package/src/connection.ts +17 -15
- package/src/consumer/consumer-group.ts +14 -14
- package/src/consumer/consumer-metadata.ts +2 -2
- package/src/consumer/consumer.ts +84 -82
- package/src/consumer/fetch-manager.ts +179 -0
- package/src/consumer/fetcher.ts +57 -0
- package/src/consumer/offset-manager.ts +6 -6
- package/src/consumer/processor.ts +47 -0
- package/src/distributors/assignments-to-replicas.test.ts +7 -7
- package/src/distributors/assignments-to-replicas.ts +1 -1
- package/src/distributors/messages-to-topic-partition-leaders.test.ts +6 -6
- package/src/index.ts +4 -3
- package/src/metadata.ts +4 -4
- package/src/producer/producer.ts +8 -8
- package/src/types.ts +2 -0
- package/src/utils/api.ts +4 -4
- package/src/utils/debug.ts +2 -2
- package/src/utils/decoder.ts +4 -4
- package/src/utils/encoder.ts +6 -6
- package/src/utils/error.ts +3 -3
- package/src/utils/retrier.ts +1 -1
- package/src/utils/tracer.ts +7 -4
- package/tsconfig.json +16 -16
- package/dist/api/api-versions.d.ts +0 -9
- package/dist/api/api-versions.js +0 -24
- package/dist/api/create-topics.d.ts +0 -38
- package/dist/api/create-topics.js +0 -53
- package/dist/api/delete-topics.d.ts +0 -18
- package/dist/api/delete-topics.js +0 -33
- package/dist/api/fetch.d.ts +0 -77
- package/dist/api/fetch.js +0 -106
- package/dist/api/find-coordinator.d.ts +0 -21
- package/dist/api/find-coordinator.js +0 -39
- package/dist/api/heartbeat.d.ts +0 -11
- package/dist/api/heartbeat.js +0 -27
- package/dist/api/index.d.ts +0 -573
- package/dist/api/index.js +0 -164
- package/dist/api/init-producer-id.d.ts +0 -13
- package/dist/api/init-producer-id.js +0 -29
- package/dist/api/join-group.d.ts +0 -34
- package/dist/api/join-group.js +0 -51
- package/dist/api/leave-group.d.ts +0 -19
- package/dist/api/leave-group.js +0 -39
- package/dist/api/list-offsets.d.ts +0 -29
- package/dist/api/list-offsets.js +0 -48
- package/dist/api/metadata.d.ts +0 -40
- package/dist/api/metadata.js +0 -58
- package/dist/api/offset-commit.d.ts +0 -28
- package/dist/api/offset-commit.js +0 -48
- package/dist/api/offset-fetch.d.ts +0 -33
- package/dist/api/offset-fetch.js +0 -57
- package/dist/api/produce.d.ts +0 -53
- package/dist/api/produce.js +0 -129
- package/dist/api/sasl-authenticate.d.ts +0 -11
- package/dist/api/sasl-authenticate.js +0 -23
- package/dist/api/sasl-handshake.d.ts +0 -6
- package/dist/api/sasl-handshake.js +0 -19
- package/dist/api/sync-group.d.ts +0 -24
- package/dist/api/sync-group.js +0 -36
- package/dist/broker.d.ts +0 -29
- package/dist/broker.js +0 -60
- package/dist/client.d.ts +0 -23
- package/dist/client.js +0 -36
- package/dist/cluster.d.ts +0 -24
- package/dist/cluster.js +0 -72
- package/dist/connection.d.ts +0 -25
- package/dist/connection.js +0 -155
- package/dist/consumer/consumer-group.d.ts +0 -36
- package/dist/consumer/consumer-group.js +0 -182
- package/dist/consumer/consumer-metadata.d.ts +0 -7
- package/dist/consumer/consumer-metadata.js +0 -14
- package/dist/consumer/consumer.d.ts +0 -37
- package/dist/consumer/consumer.js +0 -178
- package/dist/consumer/metadata.d.ts +0 -24
- package/dist/consumer/metadata.js +0 -64
- package/dist/consumer/offset-manager.d.ts +0 -22
- package/dist/consumer/offset-manager.js +0 -56
- package/dist/distributors/assignments-to-replicas.d.ts +0 -17
- package/dist/distributors/assignments-to-replicas.js +0 -60
- package/dist/distributors/assignments-to-replicas.test.d.ts +0 -1
- package/dist/distributors/assignments-to-replicas.test.js +0 -40
- package/dist/distributors/messages-to-topic-partition-leaders.d.ts +0 -17
- package/dist/distributors/messages-to-topic-partition-leaders.js +0 -15
- package/dist/distributors/messages-to-topic-partition-leaders.test.d.ts +0 -1
- package/dist/distributors/messages-to-topic-partition-leaders.test.js +0 -30
- package/dist/examples/src/replicator.js +0 -34
- package/dist/examples/src/utils/json.js +0 -5
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -19
- package/dist/metadata.d.ts +0 -24
- package/dist/metadata.js +0 -89
- package/dist/producer/producer.d.ts +0 -19
- package/dist/producer/producer.js +0 -111
- package/dist/request-handler.d.ts +0 -16
- package/dist/request-handler.js +0 -67
- package/dist/request-handler.test.d.ts +0 -1
- package/dist/request-handler.test.js +0 -340
- package/dist/src/api/api-versions.js +0 -18
- package/dist/src/api/create-topics.js +0 -46
- package/dist/src/api/delete-topics.js +0 -26
- package/dist/src/api/fetch.js +0 -95
- package/dist/src/api/find-coordinator.js +0 -34
- package/dist/src/api/heartbeat.js +0 -22
- package/dist/src/api/index.js +0 -38
- package/dist/src/api/init-producer-id.js +0 -24
- package/dist/src/api/join-group.js +0 -48
- package/dist/src/api/leave-group.js +0 -30
- package/dist/src/api/list-offsets.js +0 -39
- package/dist/src/api/metadata.js +0 -47
- package/dist/src/api/offset-commit.js +0 -39
- package/dist/src/api/offset-fetch.js +0 -44
- package/dist/src/api/produce.js +0 -119
- package/dist/src/api/sync-group.js +0 -31
- package/dist/src/broker.js +0 -35
- package/dist/src/connection.js +0 -21
- package/dist/src/consumer/consumer-group.js +0 -131
- package/dist/src/consumer/consumer.js +0 -103
- package/dist/src/consumer/metadata.js +0 -52
- package/dist/src/consumer/offset-manager.js +0 -23
- package/dist/src/index.js +0 -19
- package/dist/src/producer/producer.js +0 -84
- package/dist/src/request-handler.js +0 -57
- package/dist/src/request-handler.test.js +0 -321
- package/dist/src/types.js +0 -2
- package/dist/src/utils/api.js +0 -5
- package/dist/src/utils/decoder.js +0 -161
- package/dist/src/utils/encoder.js +0 -137
- package/dist/src/utils/error.js +0 -10
- package/dist/types.d.ts +0 -9
- package/dist/types.js +0 -2
- package/dist/utils/api.d.ts +0 -9
- package/dist/utils/api.js +0 -5
- package/dist/utils/debug.d.ts +0 -2
- package/dist/utils/debug.js +0 -11
- package/dist/utils/decoder.d.ts +0 -29
- package/dist/utils/decoder.js +0 -147
- package/dist/utils/delay.d.ts +0 -1
- package/dist/utils/delay.js +0 -5
- package/dist/utils/encoder.d.ts +0 -28
- package/dist/utils/encoder.js +0 -122
- package/dist/utils/error.d.ts +0 -11
- package/dist/utils/error.js +0 -27
- package/dist/utils/memo.d.ts +0 -1
- package/dist/utils/memo.js +0 -16
- package/dist/utils/retrier.d.ts +0 -10
- package/dist/utils/retrier.js +0 -22
- package/dist/utils/tracer.d.ts +0 -1
- package/dist/utils/tracer.js +0 -26
- package/examples/node_modules/.package-lock.json +0 -22
package/src/cluster.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { TcpSocketConnectOpts } from
|
|
2
|
-
import { TLSSocketOptions } from
|
|
3
|
-
import { API } from
|
|
4
|
-
import { Broker, SASLOptions } from
|
|
5
|
-
import { SendRequest } from
|
|
6
|
-
import { ConnectionError, KafkaTSError } from
|
|
1
|
+
import { TcpSocketConnectOpts } from 'net';
|
|
2
|
+
import { TLSSocketOptions } from 'tls';
|
|
3
|
+
import { API } from './api';
|
|
4
|
+
import { Broker, SASLOptions } from './broker';
|
|
5
|
+
import { SendRequest } from './connection';
|
|
6
|
+
import { ConnectionError, KafkaTSError } from './utils/error';
|
|
7
7
|
|
|
8
8
|
type ClusterOptions = {
|
|
9
9
|
clientId: string | null;
|
|
@@ -82,6 +82,6 @@ export class Cluster {
|
|
|
82
82
|
console.warn(`Failed to connect to seed broker ${options.host}:${options.port}`, error);
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
throw new KafkaTSError(
|
|
85
|
+
throw new KafkaTSError('No seed brokers found');
|
|
86
86
|
}
|
|
87
87
|
}
|
package/src/connection.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import assert from
|
|
2
|
-
import net, { isIP, Socket, TcpSocketConnectOpts } from
|
|
3
|
-
import tls, { TLSSocketOptions } from
|
|
4
|
-
import { getApiName } from
|
|
5
|
-
import { Api } from
|
|
6
|
-
import { Decoder } from
|
|
7
|
-
import { Encoder } from
|
|
8
|
-
import { ConnectionError } from
|
|
9
|
-
import {
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import net, { isIP, Socket, TcpSocketConnectOpts } from 'net';
|
|
3
|
+
import tls, { TLSSocketOptions } from 'tls';
|
|
4
|
+
import { getApiName } from './api';
|
|
5
|
+
import { Api } from './utils/api';
|
|
6
|
+
import { Decoder } from './utils/decoder';
|
|
7
|
+
import { Encoder } from './utils/encoder';
|
|
8
|
+
import { ConnectionError } from './utils/error';
|
|
9
|
+
import { createTracer } from './utils/tracer';
|
|
10
|
+
|
|
11
|
+
const trace = createTracer('Connection');
|
|
10
12
|
|
|
11
13
|
export type ConnectionOptions = {
|
|
12
14
|
clientId: string | null;
|
|
@@ -44,14 +46,14 @@ export class Connection {
|
|
|
44
46
|
resolve,
|
|
45
47
|
)
|
|
46
48
|
: net.connect(connection, resolve);
|
|
47
|
-
this.socket.once(
|
|
49
|
+
this.socket.once('error', reject);
|
|
48
50
|
});
|
|
49
|
-
this.socket.removeAllListeners(
|
|
51
|
+
this.socket.removeAllListeners('error');
|
|
50
52
|
|
|
51
|
-
this.socket.on(
|
|
52
|
-
this.socket.once(
|
|
53
|
+
this.socket.on('data', (data) => this.handleData(data));
|
|
54
|
+
this.socket.once('close', async () => {
|
|
53
55
|
Object.values(this.queue).forEach(({ reject }) => {
|
|
54
|
-
reject(new ConnectionError(
|
|
56
|
+
reject(new ConnectionError('Socket closed unexpectedly'));
|
|
55
57
|
});
|
|
56
58
|
this.queue = {};
|
|
57
59
|
});
|
|
@@ -100,7 +102,7 @@ export class Connection {
|
|
|
100
102
|
|
|
101
103
|
private write(buffer: Buffer) {
|
|
102
104
|
return new Promise<void>((resolve, reject) => {
|
|
103
|
-
const { stack } = new Error(
|
|
105
|
+
const { stack } = new Error('Write error');
|
|
104
106
|
this.socket.write(buffer, (error) => {
|
|
105
107
|
if (error) {
|
|
106
108
|
const err = new ConnectionError(error.message);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { API, API_ERROR } from
|
|
2
|
-
import { KEY_TYPE } from
|
|
3
|
-
import { Assignment, MemberAssignment } from
|
|
4
|
-
import { Cluster } from
|
|
5
|
-
import { KafkaTSApiError, KafkaTSError } from
|
|
6
|
-
import { ConsumerMetadata } from
|
|
7
|
-
import { OffsetManager } from
|
|
1
|
+
import { API, API_ERROR } from '../api';
|
|
2
|
+
import { KEY_TYPE } from '../api/find-coordinator';
|
|
3
|
+
import { Assignment, MemberAssignment } from '../api/sync-group';
|
|
4
|
+
import { Cluster } from '../cluster';
|
|
5
|
+
import { KafkaTSApiError, KafkaTSError } from '../utils/error';
|
|
6
|
+
import { ConsumerMetadata } from './consumer-metadata';
|
|
7
|
+
import { OffsetManager } from './offset-manager';
|
|
8
8
|
|
|
9
9
|
type ConsumerGroupOptions = {
|
|
10
10
|
cluster: Cluster;
|
|
@@ -19,9 +19,9 @@ type ConsumerGroupOptions = {
|
|
|
19
19
|
|
|
20
20
|
export class ConsumerGroup {
|
|
21
21
|
private coordinatorId = -1;
|
|
22
|
-
private memberId =
|
|
22
|
+
private memberId = '';
|
|
23
23
|
private generationId = -1;
|
|
24
|
-
private leaderId =
|
|
24
|
+
private leaderId = '';
|
|
25
25
|
private memberIds: string[] = [];
|
|
26
26
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
|
27
27
|
private heartbeatError: KafkaTSError | null = null;
|
|
@@ -76,8 +76,8 @@ export class ConsumerGroup {
|
|
|
76
76
|
memberId: this.memberId,
|
|
77
77
|
sessionTimeoutMs,
|
|
78
78
|
rebalanceTimeoutMs,
|
|
79
|
-
protocolType:
|
|
80
|
-
protocols: [{ name:
|
|
79
|
+
protocolType: 'consumer',
|
|
80
|
+
protocols: [{ name: 'RoundRobinAssigner', metadata: { version: 0, topics } }],
|
|
81
81
|
reason: null,
|
|
82
82
|
});
|
|
83
83
|
this.memberId = response.memberId;
|
|
@@ -118,11 +118,11 @@ export class ConsumerGroup {
|
|
|
118
118
|
groupInstanceId,
|
|
119
119
|
memberId: this.memberId,
|
|
120
120
|
generationId: this.generationId,
|
|
121
|
-
protocolType:
|
|
122
|
-
protocolName:
|
|
121
|
+
protocolType: 'consumer',
|
|
122
|
+
protocolName: 'RoundRobinAssigner',
|
|
123
123
|
assignments,
|
|
124
124
|
});
|
|
125
|
-
metadata.setAssignment(JSON.parse(response.assignments ||
|
|
125
|
+
metadata.setAssignment(JSON.parse(response.assignments || '{}') as Assignment);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
private async offsetFetch() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Assignment } from
|
|
2
|
-
import { Metadata } from
|
|
1
|
+
import { Assignment } from '../api/sync-group';
|
|
2
|
+
import { Metadata } from '../metadata';
|
|
3
3
|
|
|
4
4
|
export class ConsumerMetadata extends Metadata {
|
|
5
5
|
private assignment: Assignment = {};
|
package/src/consumer/consumer.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { API, API_ERROR } from
|
|
2
|
-
import { IsolationLevel } from
|
|
3
|
-
import { Assignment } from
|
|
4
|
-
import { Cluster } from
|
|
5
|
-
import { distributeAssignmentsToNodes } from
|
|
6
|
-
import { Message } from
|
|
7
|
-
import { delay } from
|
|
8
|
-
import { ConnectionError, KafkaTSApiError } from
|
|
9
|
-
import { defaultRetrier, Retrier } from
|
|
10
|
-
import { ConsumerGroup } from
|
|
11
|
-
import { ConsumerMetadata } from
|
|
12
|
-
import {
|
|
1
|
+
import { API, API_ERROR } from '../api';
|
|
2
|
+
import { IsolationLevel } from '../api/fetch';
|
|
3
|
+
import { Assignment } from '../api/sync-group';
|
|
4
|
+
import { Cluster } from '../cluster';
|
|
5
|
+
import { distributeAssignmentsToNodes } from '../distributors/assignments-to-replicas';
|
|
6
|
+
import { Message } from '../types';
|
|
7
|
+
import { delay } from '../utils/delay';
|
|
8
|
+
import { ConnectionError, KafkaTSApiError } from '../utils/error';
|
|
9
|
+
import { defaultRetrier, Retrier } from '../utils/retrier';
|
|
10
|
+
import { ConsumerGroup } from './consumer-group';
|
|
11
|
+
import { ConsumerMetadata } from './consumer-metadata';
|
|
12
|
+
import { FetchManager, Granularity } from './fetch-manager';
|
|
13
|
+
import { OffsetManager } from './offset-manager';
|
|
13
14
|
|
|
14
15
|
export type ConsumerOptions = {
|
|
15
16
|
topics: string[];
|
|
@@ -26,13 +27,16 @@ export type ConsumerOptions = {
|
|
|
26
27
|
allowTopicAutoCreation?: boolean;
|
|
27
28
|
fromBeginning?: boolean;
|
|
28
29
|
retrier?: Retrier;
|
|
29
|
-
|
|
30
|
+
granularity?: Granularity;
|
|
31
|
+
concurrency?: number;
|
|
32
|
+
} & ({ onBatch: (messages: Required<Message>[]) => unknown } | { onMessage: (message: Required<Message>) => unknown });
|
|
30
33
|
|
|
31
34
|
export class Consumer {
|
|
32
35
|
private options: Required<ConsumerOptions>;
|
|
33
36
|
private metadata: ConsumerMetadata;
|
|
34
37
|
private consumerGroup: ConsumerGroup | undefined;
|
|
35
38
|
private offsetManager: OffsetManager;
|
|
39
|
+
private fetchManager?: FetchManager;
|
|
36
40
|
private stopHook: (() => void) | undefined;
|
|
37
41
|
|
|
38
42
|
constructor(
|
|
@@ -43,7 +47,7 @@ export class Consumer {
|
|
|
43
47
|
...options,
|
|
44
48
|
groupId: options.groupId ?? null,
|
|
45
49
|
groupInstanceId: options.groupInstanceId ?? null,
|
|
46
|
-
rackId: options.rackId ??
|
|
50
|
+
rackId: options.rackId ?? '',
|
|
47
51
|
sessionTimeoutMs: options.sessionTimeoutMs ?? 30_000,
|
|
48
52
|
rebalanceTimeoutMs: options.rebalanceTimeoutMs ?? 60_000,
|
|
49
53
|
maxWaitMs: options.maxWaitMs ?? 5000,
|
|
@@ -54,6 +58,8 @@ export class Consumer {
|
|
|
54
58
|
allowTopicAutoCreation: options.allowTopicAutoCreation ?? false,
|
|
55
59
|
fromBeginning: options.fromBeginning ?? false,
|
|
56
60
|
retrier: options.retrier ?? defaultRetrier,
|
|
61
|
+
granularity: options.granularity ?? 'broker',
|
|
62
|
+
concurrency: options.concurrency ?? 1,
|
|
57
63
|
};
|
|
58
64
|
|
|
59
65
|
this.metadata = new ConsumerMetadata({ cluster: this.cluster });
|
|
@@ -95,81 +101,63 @@ export class Consumer {
|
|
|
95
101
|
if (this.stopHook) return (this.stopHook as () => void)();
|
|
96
102
|
return this.close(true).then(() => this.start());
|
|
97
103
|
}
|
|
98
|
-
this.
|
|
104
|
+
setImmediate(() => this.startFetchManager());
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
public async close(force = false): Promise<void> {
|
|
108
|
+
if (!force) {
|
|
109
|
+
await new Promise<void>(async (resolve) => {
|
|
110
|
+
this.stopHook = resolve;
|
|
111
|
+
await this.fetchManager?.stop();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
await this.consumerGroup
|
|
115
|
+
?.leaveGroup()
|
|
116
|
+
.catch((error) => console.warn(`Failed to leave group: ${error.message}`));
|
|
117
|
+
await this.cluster.disconnect().catch((error) => console.warn(`Failed to disconnect: ${error.message}`));
|
|
118
|
+
}
|
|
104
119
|
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
private startFetchManager = async () => {
|
|
121
|
+
const { granularity, concurrency } = this.options;
|
|
107
122
|
|
|
108
123
|
while (!this.stopHook) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
const nodeAssignments = Object.entries(
|
|
125
|
+
distributeAssignmentsToNodes(
|
|
126
|
+
this.metadata.getAssignment(),
|
|
127
|
+
this.metadata.getTopicPartitionReplicaIds(),
|
|
128
|
+
),
|
|
129
|
+
).map(([nodeId, assignment]) => ({ nodeId: parseInt(nodeId), assignment }));
|
|
130
|
+
|
|
131
|
+
const numPartitions = Object.values(this.metadata.getAssignment()).flat().length;
|
|
132
|
+
const numProcessors = Math.min(concurrency, numPartitions);
|
|
133
|
+
|
|
134
|
+
this.fetchManager = new FetchManager({
|
|
135
|
+
fetch: this.fetch.bind(this),
|
|
136
|
+
process: this.process.bind(this),
|
|
137
|
+
metadata: this.metadata,
|
|
138
|
+
consumerGroup: this.consumerGroup,
|
|
139
|
+
nodeAssignments,
|
|
140
|
+
granularity,
|
|
141
|
+
concurrency: numProcessors,
|
|
142
|
+
});
|
|
118
143
|
|
|
119
144
|
try {
|
|
120
|
-
|
|
121
|
-
const batch = await this.fetch(nodeId, assignment);
|
|
122
|
-
const messages = batch.responses.flatMap(({ topicId, partitions }) =>
|
|
123
|
-
partitions.flatMap(({ partitionIndex, records }) =>
|
|
124
|
-
records.flatMap(({ baseTimestamp, baseOffset, records }) =>
|
|
125
|
-
records.map(
|
|
126
|
-
(message): Required<Message> => ({
|
|
127
|
-
topic: this.metadata.getTopicNameById(topicId),
|
|
128
|
-
partition: partitionIndex,
|
|
129
|
-
key: message.key ?? null,
|
|
130
|
-
value: message.value ?? null,
|
|
131
|
-
headers: Object.fromEntries(
|
|
132
|
-
message.headers.map(({ key, value }) => [key, value]),
|
|
133
|
-
),
|
|
134
|
-
timestamp: baseTimestamp + BigInt(message.timestampDelta),
|
|
135
|
-
offset: baseOffset + BigInt(message.offsetDelta),
|
|
136
|
-
}),
|
|
137
|
-
),
|
|
138
|
-
),
|
|
139
|
-
),
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
if ("onBatch" in options) {
|
|
143
|
-
await retrier(() => options.onBatch(messages));
|
|
144
|
-
|
|
145
|
-
messages.forEach(({ topic, partition, offset }) =>
|
|
146
|
-
this.offsetManager.resolve(topic, partition, offset + 1n),
|
|
147
|
-
);
|
|
148
|
-
} else if ("onMessage" in options) {
|
|
149
|
-
for (const message of messages) {
|
|
150
|
-
await retrier(() => options.onMessage(message));
|
|
151
|
-
|
|
152
|
-
const { topic, partition, offset } = message;
|
|
153
|
-
this.offsetManager.resolve(topic, partition, offset + 1n);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
await this.consumerGroup?.offsetCommit();
|
|
157
|
-
await this.consumerGroup?.handleLastHeartbeat();
|
|
158
|
-
}
|
|
145
|
+
await this.fetchManager.start();
|
|
159
146
|
|
|
160
147
|
if (!nodeAssignments.length) {
|
|
161
|
-
console.debug(
|
|
148
|
+
console.debug('No partitions assigned. Waiting for reassignment...');
|
|
162
149
|
await delay(this.options.maxWaitMs);
|
|
163
150
|
await this.consumerGroup?.handleLastHeartbeat();
|
|
164
151
|
}
|
|
165
152
|
} catch (error) {
|
|
153
|
+
await this.fetchManager.stop();
|
|
154
|
+
|
|
166
155
|
if ((error as KafkaTSApiError).errorCode === API_ERROR.REBALANCE_IN_PROGRESS) {
|
|
167
|
-
console.debug(
|
|
168
|
-
shouldReassign = true;
|
|
156
|
+
console.debug('Rebalance in progress...');
|
|
169
157
|
continue;
|
|
170
158
|
}
|
|
171
159
|
if ((error as KafkaTSApiError).errorCode === API_ERROR.FENCED_INSTANCE_ID) {
|
|
172
|
-
console.debug(
|
|
160
|
+
console.debug('New consumer with the same groupInstanceId joined. Exiting the consumer...');
|
|
173
161
|
this.close();
|
|
174
162
|
break;
|
|
175
163
|
}
|
|
@@ -182,23 +170,37 @@ export class Consumer {
|
|
|
182
170
|
break;
|
|
183
171
|
}
|
|
184
172
|
console.error(error);
|
|
185
|
-
|
|
173
|
+
this.close();
|
|
186
174
|
break;
|
|
187
175
|
}
|
|
188
176
|
}
|
|
189
177
|
this.stopHook?.();
|
|
190
178
|
};
|
|
191
179
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
180
|
+
private async process(messages: Required<Message>[]) {
|
|
181
|
+
const { options } = this;
|
|
182
|
+
const { retrier } = options;
|
|
183
|
+
|
|
184
|
+
if ('onBatch' in options) {
|
|
185
|
+
await retrier(() => options.onBatch(messages));
|
|
186
|
+
|
|
187
|
+
messages.forEach(({ topic, partition, offset }) =>
|
|
188
|
+
this.offsetManager.resolve(topic, partition, offset + 1n),
|
|
189
|
+
);
|
|
190
|
+
} else if ('onMessage' in options) {
|
|
191
|
+
try {
|
|
192
|
+
for (const message of messages) {
|
|
193
|
+
await retrier(() => options.onMessage(message));
|
|
194
|
+
|
|
195
|
+
const { topic, partition, offset } = message;
|
|
196
|
+
this.offsetManager.resolve(topic, partition, offset + 1n);
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
await this.consumerGroup?.offsetCommit().catch(() => {});
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
197
202
|
}
|
|
198
|
-
await this.consumerGroup
|
|
199
|
-
?.leaveGroup()
|
|
200
|
-
.catch((error) => console.warn(`Failed to leave group: ${error.message}`));
|
|
201
|
-
await this.cluster.disconnect().catch((error) => console.warn(`Failed to disconnect: ${error.message}`));
|
|
203
|
+
await this.consumerGroup?.offsetCommit();
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
private fetch(nodeId: number, assignment: Assignment) {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import { API } from '../api';
|
|
3
|
+
import { Assignment } from '../api/sync-group';
|
|
4
|
+
import { Metadata } from '../metadata';
|
|
5
|
+
import { Batch, Message } from '../types';
|
|
6
|
+
import { KafkaTSError } from '../utils/error';
|
|
7
|
+
import { createTracer } from '../utils/tracer';
|
|
8
|
+
import { ConsumerGroup } from './consumer-group';
|
|
9
|
+
import { Fetcher } from './fetcher';
|
|
10
|
+
import { Processor } from './processor';
|
|
11
|
+
|
|
12
|
+
const trace = createTracer('FetchManager');
|
|
13
|
+
|
|
14
|
+
export type Granularity = 'partition' | 'topic' | 'broker';
|
|
15
|
+
|
|
16
|
+
type FetchManagerOptions = {
|
|
17
|
+
fetch: (nodeId: number, assignment: Assignment) => Promise<ReturnType<(typeof API.FETCH)['response']>>;
|
|
18
|
+
process: (batch: Batch) => Promise<void>;
|
|
19
|
+
metadata: Metadata;
|
|
20
|
+
consumerGroup?: ConsumerGroup;
|
|
21
|
+
nodeAssignments: { nodeId: number; assignment: Assignment }[];
|
|
22
|
+
granularity: Granularity;
|
|
23
|
+
concurrency: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type Checkpoint = { kind: 'checkpoint'; fetcherId: number };
|
|
27
|
+
type Entry = Batch | Checkpoint;
|
|
28
|
+
|
|
29
|
+
export class FetchManager extends EventEmitter<{ data: []; checkpoint: [number]; stop: [] }> {
|
|
30
|
+
private queue: Entry[] = [];
|
|
31
|
+
private isRunning = false;
|
|
32
|
+
private fetchers: Fetcher[];
|
|
33
|
+
private processors: Processor[];
|
|
34
|
+
|
|
35
|
+
constructor(private options: FetchManagerOptions) {
|
|
36
|
+
super();
|
|
37
|
+
|
|
38
|
+
const { fetch, process, consumerGroup, nodeAssignments, concurrency } = this.options;
|
|
39
|
+
|
|
40
|
+
this.fetchers = nodeAssignments.map(
|
|
41
|
+
({ nodeId, assignment }, index) =>
|
|
42
|
+
new Fetcher(index, {
|
|
43
|
+
nodeId,
|
|
44
|
+
assignment,
|
|
45
|
+
consumerGroup,
|
|
46
|
+
fetch,
|
|
47
|
+
onResponse: this.onResponse.bind(this),
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
this.processors = Array.from({ length: concurrency }).map(
|
|
51
|
+
() => new Processor({ process, poll: this.poll.bind(this) }),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async start() {
|
|
56
|
+
this.queue = [];
|
|
57
|
+
this.isRunning = true;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await Promise.all([
|
|
61
|
+
...this.fetchers.map((fetcher) => fetcher.loop()),
|
|
62
|
+
...this.processors.map((processor) => processor.loop()),
|
|
63
|
+
]);
|
|
64
|
+
} finally {
|
|
65
|
+
this.isRunning = false;
|
|
66
|
+
this.emit('stop');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@trace()
|
|
71
|
+
public async stop() {
|
|
72
|
+
this.isRunning = false;
|
|
73
|
+
this.emit('stop');
|
|
74
|
+
|
|
75
|
+
await Promise.all([
|
|
76
|
+
...this.fetchers.map((fetcher) => fetcher.stop()),
|
|
77
|
+
...this.processors.map((processor) => processor.stop()),
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@trace()
|
|
82
|
+
public async poll(): Promise<Batch> {
|
|
83
|
+
if (!this.isRunning) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const batch = this.queue.shift();
|
|
88
|
+
if (!batch) {
|
|
89
|
+
await new Promise<void>((resolve) => {
|
|
90
|
+
const onData = () => {
|
|
91
|
+
this.removeListener('stop', onStop);
|
|
92
|
+
resolve();
|
|
93
|
+
};
|
|
94
|
+
const onStop = () => {
|
|
95
|
+
this.removeListener('data', onData);
|
|
96
|
+
resolve();
|
|
97
|
+
};
|
|
98
|
+
this.once('data', onData);
|
|
99
|
+
this.once('stop', onStop);
|
|
100
|
+
});
|
|
101
|
+
return this.poll();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if ('kind' in batch && batch.kind === 'checkpoint') {
|
|
105
|
+
this.emit('checkpoint', batch.fetcherId);
|
|
106
|
+
return this.poll();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return batch as Exclude<Entry, Checkpoint>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async onResponse(fetcherId: number, response: ReturnType<(typeof API.FETCH)['response']>) {
|
|
113
|
+
const { metadata, granularity } = this.options;
|
|
114
|
+
|
|
115
|
+
const batches = fetchResponseToBatches(response, granularity, metadata);
|
|
116
|
+
if (batches.length) {
|
|
117
|
+
this.queue.push(...batches);
|
|
118
|
+
this.queue.push({ kind: 'checkpoint', fetcherId });
|
|
119
|
+
|
|
120
|
+
this.emit('data');
|
|
121
|
+
await new Promise<void>((resolve) => {
|
|
122
|
+
const onCheckpoint = (id: number) => {
|
|
123
|
+
if (id === fetcherId) {
|
|
124
|
+
this.removeListener('stop', onStop);
|
|
125
|
+
resolve();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const onStop = () => {
|
|
129
|
+
this.removeListener('checkpoint', onCheckpoint);
|
|
130
|
+
resolve();
|
|
131
|
+
};
|
|
132
|
+
this.once('checkpoint', onCheckpoint);
|
|
133
|
+
this.once('stop', onStop);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const fetchResponseToBatches = (
|
|
140
|
+
batch: ReturnType<typeof API.FETCH.response>,
|
|
141
|
+
granularity: Granularity,
|
|
142
|
+
metadata: Metadata,
|
|
143
|
+
): Batch[] => {
|
|
144
|
+
const brokerTopics = batch.responses.map(({ topicId, partitions }) =>
|
|
145
|
+
partitions.map(({ partitionIndex, records }) =>
|
|
146
|
+
records.flatMap(({ baseTimestamp, baseOffset, records }) =>
|
|
147
|
+
records.map(
|
|
148
|
+
(message): Required<Message> => ({
|
|
149
|
+
topic: metadata.getTopicNameById(topicId),
|
|
150
|
+
partition: partitionIndex,
|
|
151
|
+
key: message.key ?? null,
|
|
152
|
+
value: message.value ?? null,
|
|
153
|
+
headers: Object.fromEntries(message.headers.map(({ key, value }) => [key, value])),
|
|
154
|
+
timestamp: baseTimestamp + BigInt(message.timestampDelta),
|
|
155
|
+
offset: baseOffset + BigInt(message.offsetDelta),
|
|
156
|
+
}),
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
switch (granularity) {
|
|
163
|
+
case 'broker':
|
|
164
|
+
const messages = brokerTopics.flatMap((topicPartition) =>
|
|
165
|
+
topicPartition.flatMap((partitionMessages) => partitionMessages),
|
|
166
|
+
);
|
|
167
|
+
return messages.length ? [messages] : [];
|
|
168
|
+
case 'topic':
|
|
169
|
+
return brokerTopics
|
|
170
|
+
.map((topicPartition) => topicPartition.flatMap((partitionMessages) => partitionMessages))
|
|
171
|
+
.filter((messages) => messages.length);
|
|
172
|
+
case 'partition':
|
|
173
|
+
return brokerTopics.flatMap((topicPartition) =>
|
|
174
|
+
topicPartition.map((partitionMessages) => partitionMessages),
|
|
175
|
+
);
|
|
176
|
+
default:
|
|
177
|
+
throw new KafkaTSError(`Unhandled batch granularity: ${granularity}`);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { EventEmitter } from 'stream';
|
|
2
|
+
import { API } from '../api';
|
|
3
|
+
import { Assignment } from '../api/sync-group';
|
|
4
|
+
import { createTracer } from '../utils/tracer';
|
|
5
|
+
import { ConsumerGroup } from './consumer-group';
|
|
6
|
+
|
|
7
|
+
const trace = createTracer('Fetcher');
|
|
8
|
+
|
|
9
|
+
type FetcherOptions = {
|
|
10
|
+
nodeId: number;
|
|
11
|
+
assignment: Assignment;
|
|
12
|
+
consumerGroup?: ConsumerGroup;
|
|
13
|
+
fetch: (nodeId: number, assignment: Assignment) => Promise<ReturnType<(typeof API.FETCH)['response']>>;
|
|
14
|
+
onResponse: (fetcherId: number, response: ReturnType<(typeof API.FETCH)['response']>) => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class Fetcher extends EventEmitter<{ stop: []; stopped: []; data: []; drain: [] }> {
|
|
18
|
+
private isRunning = false;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private fetcherId: number,
|
|
22
|
+
private options: FetcherOptions,
|
|
23
|
+
) {
|
|
24
|
+
super();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async loop() {
|
|
28
|
+
const { nodeId, assignment, consumerGroup, fetch, onResponse } = this.options;
|
|
29
|
+
|
|
30
|
+
this.isRunning = true;
|
|
31
|
+
this.once('stop', () => (this.isRunning = false));
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
while (this.isRunning) {
|
|
35
|
+
const response = await fetch(nodeId, assignment);
|
|
36
|
+
await consumerGroup?.handleLastHeartbeat();
|
|
37
|
+
await onResponse(this.fetcherId, response);
|
|
38
|
+
await consumerGroup?.handleLastHeartbeat();
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
this.isRunning = false;
|
|
42
|
+
this.emit('stopped');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@trace()
|
|
47
|
+
public async stop() {
|
|
48
|
+
if (!this.isRunning) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.emit('stop');
|
|
53
|
+
return new Promise<void>((resolve) => {
|
|
54
|
+
this.once('stopped', resolve);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { API } from
|
|
2
|
-
import { IsolationLevel } from
|
|
3
|
-
import { Assignment } from
|
|
4
|
-
import { Cluster } from
|
|
5
|
-
import { distributeMessagesToTopicPartitionLeaders } from
|
|
6
|
-
import { ConsumerMetadata } from
|
|
1
|
+
import { API } from '../api';
|
|
2
|
+
import { IsolationLevel } from '../api/fetch';
|
|
3
|
+
import { Assignment } from '../api/sync-group';
|
|
4
|
+
import { Cluster } from '../cluster';
|
|
5
|
+
import { distributeMessagesToTopicPartitionLeaders } from '../distributors/messages-to-topic-partition-leaders';
|
|
6
|
+
import { ConsumerMetadata } from './consumer-metadata';
|
|
7
7
|
|
|
8
8
|
type OffsetManagerOptions = {
|
|
9
9
|
cluster: Cluster;
|