kafka-ts 0.0.2-beta → 0.0.2
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/.prettierrc +3 -2
- package/README.md +109 -39
- package/dist/api/api-versions.d.ts +9 -0
- package/dist/api/api-versions.js +24 -0
- package/dist/api/create-topics.d.ts +38 -0
- package/dist/api/create-topics.js +53 -0
- package/dist/api/delete-topics.d.ts +18 -0
- package/dist/api/delete-topics.js +33 -0
- package/dist/api/fetch.d.ts +84 -0
- package/dist/api/fetch.js +142 -0
- package/dist/api/find-coordinator.d.ts +21 -0
- package/dist/api/find-coordinator.js +39 -0
- package/dist/api/heartbeat.d.ts +11 -0
- package/dist/api/heartbeat.js +27 -0
- package/dist/api/index.d.ts +578 -0
- package/dist/api/index.js +165 -0
- package/dist/api/init-producer-id.d.ts +13 -0
- package/dist/api/init-producer-id.js +29 -0
- package/dist/api/join-group.d.ts +34 -0
- package/dist/api/join-group.js +51 -0
- package/dist/api/leave-group.d.ts +19 -0
- package/dist/api/leave-group.js +39 -0
- package/dist/api/list-offsets.d.ts +29 -0
- package/dist/api/list-offsets.js +48 -0
- package/dist/api/metadata.d.ts +40 -0
- package/dist/api/metadata.js +58 -0
- package/dist/api/offset-commit.d.ts +28 -0
- package/dist/api/offset-commit.js +48 -0
- package/dist/api/offset-fetch.d.ts +33 -0
- package/dist/api/offset-fetch.js +57 -0
- package/dist/api/produce.d.ts +54 -0
- package/dist/api/produce.js +126 -0
- package/dist/api/sasl-authenticate.d.ts +11 -0
- package/dist/api/sasl-authenticate.js +23 -0
- package/dist/api/sasl-handshake.d.ts +6 -0
- package/dist/api/sasl-handshake.js +19 -0
- package/dist/api/sync-group.d.ts +24 -0
- package/dist/api/sync-group.js +36 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/plain.d.ts +5 -0
- package/dist/auth/plain.js +12 -0
- package/dist/auth/scram.d.ts +9 -0
- package/dist/auth/scram.js +40 -0
- package/dist/broker.d.ts +30 -0
- package/dist/broker.js +55 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +36 -0
- package/dist/cluster.d.ts +27 -0
- package/dist/cluster.js +70 -0
- package/dist/cluster.test.d.ts +1 -0
- package/dist/cluster.test.js +345 -0
- package/dist/codecs/gzip.d.ts +2 -0
- package/dist/codecs/gzip.js +8 -0
- package/dist/codecs/index.d.ts +2 -0
- package/dist/codecs/index.js +17 -0
- package/dist/codecs/none.d.ts +2 -0
- package/dist/codecs/none.js +7 -0
- package/dist/codecs/types.d.ts +5 -0
- package/dist/codecs/types.js +2 -0
- package/dist/connection.d.ts +26 -0
- package/dist/connection.js +175 -0
- package/dist/consumer/consumer-group.d.ts +41 -0
- package/dist/consumer/consumer-group.js +217 -0
- package/dist/consumer/consumer-metadata.d.ts +7 -0
- package/dist/consumer/consumer-metadata.js +14 -0
- package/dist/consumer/consumer.d.ts +44 -0
- package/dist/consumer/consumer.js +225 -0
- package/dist/consumer/fetch-manager.d.ts +33 -0
- package/dist/consumer/fetch-manager.js +140 -0
- package/dist/consumer/fetcher.d.ts +25 -0
- package/dist/consumer/fetcher.js +64 -0
- package/dist/consumer/offset-manager.d.ts +22 -0
- package/dist/consumer/offset-manager.js +66 -0
- package/dist/consumer/processor.d.ts +19 -0
- package/dist/consumer/processor.js +59 -0
- package/dist/distributors/assignments-to-replicas.d.ts +16 -0
- package/dist/distributors/assignments-to-replicas.js +59 -0
- package/dist/distributors/assignments-to-replicas.test.d.ts +1 -0
- package/dist/distributors/assignments-to-replicas.test.js +40 -0
- package/dist/distributors/messages-to-topic-partition-leaders.d.ts +17 -0
- package/dist/distributors/messages-to-topic-partition-leaders.js +15 -0
- package/dist/distributors/messages-to-topic-partition-leaders.test.d.ts +1 -0
- package/dist/distributors/messages-to-topic-partition-leaders.test.js +30 -0
- package/dist/distributors/partitioner.d.ts +7 -0
- package/dist/distributors/partitioner.js +23 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +26 -0
- package/dist/metadata.d.ts +24 -0
- package/dist/metadata.js +106 -0
- package/dist/producer/producer.d.ts +24 -0
- package/dist/producer/producer.js +131 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +2 -0
- package/dist/utils/api.d.ts +9 -0
- package/dist/utils/api.js +5 -0
- package/dist/utils/crypto.d.ts +8 -0
- package/dist/utils/crypto.js +18 -0
- package/dist/utils/decoder.d.ts +30 -0
- package/dist/utils/decoder.js +152 -0
- package/dist/utils/delay.d.ts +1 -0
- package/dist/utils/delay.js +5 -0
- package/dist/utils/encoder.d.ts +28 -0
- package/dist/utils/encoder.js +125 -0
- package/dist/utils/error.d.ts +11 -0
- package/dist/utils/error.js +27 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/memo.d.ts +1 -0
- package/dist/utils/memo.js +16 -0
- package/dist/utils/murmur2.d.ts +3 -0
- package/dist/utils/murmur2.js +40 -0
- package/dist/utils/retrier.d.ts +10 -0
- package/dist/utils/retrier.js +22 -0
- package/dist/utils/tracer.d.ts +5 -0
- package/dist/utils/tracer.js +39 -0
- package/package.json +30 -19
- package/src/__snapshots__/{request-handler.test.ts.snap → cluster.test.ts.snap} +329 -26
- 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 +86 -31
- package/src/api/find-coordinator.ts +2 -2
- package/src/api/heartbeat.ts +2 -2
- package/src/api/index.ts +21 -19
- 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 +17 -20
- 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/auth/index.ts +2 -0
- package/src/auth/plain.ts +10 -0
- package/src/auth/scram.ts +52 -0
- package/src/broker.ts +12 -14
- package/src/client.ts +7 -7
- package/src/cluster.test.ts +78 -74
- package/src/cluster.ts +43 -45
- package/src/codecs/gzip.ts +9 -0
- package/src/codecs/index.ts +16 -0
- package/src/codecs/none.ts +6 -0
- package/src/codecs/types.ts +4 -0
- package/src/connection.ts +49 -33
- package/src/consumer/consumer-group.ts +57 -35
- package/src/consumer/consumer-metadata.ts +2 -2
- package/src/consumer/consumer.ts +115 -92
- package/src/consumer/fetch-manager.ts +169 -0
- package/src/consumer/fetcher.ts +64 -0
- package/src/consumer/offset-manager.ts +24 -13
- package/src/consumer/processor.ts +53 -0
- package/src/distributors/assignments-to-replicas.test.ts +7 -7
- package/src/distributors/assignments-to-replicas.ts +2 -4
- package/src/distributors/messages-to-topic-partition-leaders.test.ts +6 -6
- package/src/distributors/partitioner.ts +27 -0
- package/src/index.ts +9 -3
- package/src/metadata.ts +8 -4
- package/src/producer/producer.ts +30 -20
- package/src/types.ts +5 -3
- package/src/utils/api.ts +5 -5
- package/src/utils/crypto.ts +15 -0
- package/src/utils/decoder.ts +14 -8
- package/src/utils/encoder.ts +34 -27
- package/src/utils/error.ts +3 -3
- package/src/utils/logger.ts +37 -0
- package/src/utils/murmur2.ts +44 -0
- package/src/utils/retrier.ts +1 -1
- package/src/utils/tracer.ts +41 -20
- package/tsconfig.json +16 -16
- package/.github/workflows/release.yml +0 -17
- package/certs/ca.crt +0 -29
- package/certs/ca.key +0 -52
- package/certs/ca.srl +0 -1
- package/certs/kafka.crt +0 -29
- package/certs/kafka.csr +0 -26
- package/certs/kafka.key +0 -52
- package/certs/kafka.keystore.jks +0 -0
- package/certs/kafka.truststore.jks +0 -0
- package/docker-compose.yml +0 -104
- package/examples/package-lock.json +0 -31
- package/examples/package.json +0 -14
- package/examples/src/client.ts +0 -9
- package/examples/src/consumer.ts +0 -17
- package/examples/src/create-topic.ts +0 -37
- package/examples/src/producer.ts +0 -24
- package/examples/src/replicator.ts +0 -25
- package/examples/src/utils/json.ts +0 -1
- package/examples/tsconfig.json +0 -7
- package/log4j.properties +0 -95
- package/scripts/generate-certs.sh +0 -24
- package/src/utils/debug.ts +0 -9
package/src/connection.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
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 {
|
|
10
|
-
|
|
11
|
-
|
|
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 { log } from './utils/logger';
|
|
10
|
+
import { createTracer } from './utils/tracer';
|
|
11
|
+
|
|
12
|
+
const trace = createTracer('Connection');
|
|
13
|
+
|
|
14
|
+
type ConnectionOptions = {
|
|
12
15
|
clientId: string | null;
|
|
13
16
|
connection: TcpSocketConnectOpts;
|
|
14
17
|
ssl: TLSSocketOptions | null;
|
|
@@ -22,14 +25,14 @@ export class Connection {
|
|
|
22
25
|
[correlationId: number]: { resolve: (response: RawResonse) => void; reject: (error: Error) => void };
|
|
23
26
|
} = {};
|
|
24
27
|
private lastCorrelationId = 0;
|
|
25
|
-
private
|
|
28
|
+
private chunks: Buffer[] = [];
|
|
26
29
|
|
|
27
30
|
constructor(private options: ConnectionOptions) {}
|
|
28
31
|
|
|
29
32
|
@trace()
|
|
30
33
|
public async connect() {
|
|
31
34
|
this.queue = {};
|
|
32
|
-
this.
|
|
35
|
+
this.chunks = [];
|
|
33
36
|
|
|
34
37
|
await new Promise<void>((resolve, reject) => {
|
|
35
38
|
const { ssl, connection } = this.options;
|
|
@@ -44,19 +47,20 @@ export class Connection {
|
|
|
44
47
|
resolve,
|
|
45
48
|
)
|
|
46
49
|
: net.connect(connection, resolve);
|
|
47
|
-
this.socket.once(
|
|
50
|
+
this.socket.once('error', reject);
|
|
48
51
|
});
|
|
49
|
-
this.socket.removeAllListeners(
|
|
52
|
+
this.socket.removeAllListeners('error');
|
|
50
53
|
|
|
51
|
-
this.socket.on(
|
|
52
|
-
this.socket.once(
|
|
54
|
+
this.socket.on('data', (data) => this.handleData(data));
|
|
55
|
+
this.socket.once('close', async () => {
|
|
53
56
|
Object.values(this.queue).forEach(({ reject }) => {
|
|
54
|
-
reject(new ConnectionError(
|
|
57
|
+
reject(new ConnectionError('Socket closed unexpectedly'));
|
|
55
58
|
});
|
|
56
59
|
this.queue = {};
|
|
57
60
|
});
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
@trace()
|
|
60
64
|
public disconnect() {
|
|
61
65
|
this.socket.removeAllListeners();
|
|
62
66
|
return new Promise<void>((resolve) => {
|
|
@@ -67,9 +71,10 @@ export class Connection {
|
|
|
67
71
|
});
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
@trace((api, body) => ({
|
|
74
|
+
@trace((api, body) => ({ message: getApiName(api), body }))
|
|
71
75
|
public async sendRequest<Request, Response>(api: Api<Request, Response>, body: Request): Promise<Response> {
|
|
72
76
|
const correlationId = this.nextCorrelationId();
|
|
77
|
+
const apiName = getApiName(api);
|
|
73
78
|
|
|
74
79
|
const encoder = new Encoder()
|
|
75
80
|
.writeInt16(api.apiKey)
|
|
@@ -77,18 +82,25 @@ export class Connection {
|
|
|
77
82
|
.writeInt32(correlationId)
|
|
78
83
|
.writeString(this.options.clientId);
|
|
79
84
|
|
|
80
|
-
const request = api.request(encoder, body)
|
|
81
|
-
const requestEncoder = new Encoder().writeInt32(request.
|
|
85
|
+
const request = api.request(encoder, body);
|
|
86
|
+
const requestEncoder = new Encoder().writeInt32(request.getByteLength()).writeEncoder(request);
|
|
82
87
|
|
|
88
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
83
89
|
const { responseDecoder, responseSize } = await new Promise<RawResonse>(async (resolve, reject) => {
|
|
90
|
+
timeout = setTimeout(() => {
|
|
91
|
+
delete this.queue[correlationId];
|
|
92
|
+
reject(new ConnectionError(`${apiName} timed out`));
|
|
93
|
+
}, 30_000);
|
|
94
|
+
|
|
84
95
|
try {
|
|
85
|
-
await this.write(requestEncoder.value());
|
|
86
96
|
this.queue[correlationId] = { resolve, reject };
|
|
97
|
+
await this.write(requestEncoder.value());
|
|
87
98
|
} catch (error) {
|
|
88
99
|
reject(error);
|
|
89
100
|
}
|
|
90
101
|
});
|
|
91
|
-
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
const response = await api.response(responseDecoder);
|
|
92
104
|
|
|
93
105
|
assert(
|
|
94
106
|
responseDecoder.getOffset() - 4 === responseSize,
|
|
@@ -100,7 +112,7 @@ export class Connection {
|
|
|
100
112
|
|
|
101
113
|
private write(buffer: Buffer) {
|
|
102
114
|
return new Promise<void>((resolve, reject) => {
|
|
103
|
-
const { stack } = new Error(
|
|
115
|
+
const { stack } = new Error('Write error');
|
|
104
116
|
this.socket.write(buffer, (error) => {
|
|
105
117
|
if (error) {
|
|
106
118
|
const err = new ConnectionError(error.message);
|
|
@@ -113,12 +125,13 @@ export class Connection {
|
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
private handleData(buffer: Buffer) {
|
|
116
|
-
this.
|
|
117
|
-
|
|
128
|
+
this.chunks.push(buffer);
|
|
129
|
+
|
|
130
|
+
const decoder = new Decoder(Buffer.concat(this.chunks));
|
|
131
|
+
if (decoder.getBufferLength() < 4) {
|
|
118
132
|
return;
|
|
119
133
|
}
|
|
120
134
|
|
|
121
|
-
const decoder = new Decoder(this.buffer);
|
|
122
135
|
const size = decoder.readInt32();
|
|
123
136
|
if (size !== decoder.getBufferLength() - 4) {
|
|
124
137
|
return;
|
|
@@ -126,15 +139,18 @@ export class Connection {
|
|
|
126
139
|
|
|
127
140
|
const correlationId = decoder.readInt32();
|
|
128
141
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
const context = this.queue[correlationId];
|
|
143
|
+
if (context) {
|
|
144
|
+
delete this.queue[correlationId];
|
|
145
|
+
context.resolve({ responseDecoder: decoder, responseSize: size });
|
|
146
|
+
} else {
|
|
147
|
+
log.debug('Could not find pending request for correlationId', { correlationId });
|
|
148
|
+
}
|
|
149
|
+
this.chunks = [];
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
private nextCorrelationId() {
|
|
137
|
-
return
|
|
153
|
+
return this.lastCorrelationId++;
|
|
138
154
|
}
|
|
139
155
|
}
|
|
140
156
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import { API, API_ERROR } from '../api';
|
|
3
|
+
import { KEY_TYPE } from '../api/find-coordinator';
|
|
4
|
+
import { Assignment, MemberAssignment } from '../api/sync-group';
|
|
5
|
+
import { Cluster } from '../cluster';
|
|
6
|
+
import { KafkaTSApiError, KafkaTSError } from '../utils/error';
|
|
7
|
+
import { createTracer } from '../utils/tracer';
|
|
8
|
+
import { ConsumerMetadata } from './consumer-metadata';
|
|
9
|
+
import { OffsetManager } from './offset-manager';
|
|
10
|
+
|
|
11
|
+
const trace = createTracer('ConsumerGroup');
|
|
8
12
|
|
|
9
13
|
type ConsumerGroupOptions = {
|
|
10
14
|
cluster: Cluster;
|
|
@@ -17,19 +21,25 @@ type ConsumerGroupOptions = {
|
|
|
17
21
|
offsetManager: OffsetManager;
|
|
18
22
|
};
|
|
19
23
|
|
|
20
|
-
export class ConsumerGroup {
|
|
24
|
+
export class ConsumerGroup extends EventEmitter<{ offsetCommit: [] }> {
|
|
21
25
|
private coordinatorId = -1;
|
|
22
|
-
private memberId =
|
|
26
|
+
private memberId = '';
|
|
23
27
|
private generationId = -1;
|
|
24
|
-
private leaderId =
|
|
28
|
+
private leaderId = '';
|
|
25
29
|
private memberIds: string[] = [];
|
|
26
30
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
|
27
31
|
private heartbeatError: KafkaTSError | null = null;
|
|
28
32
|
|
|
29
|
-
constructor(private options: ConsumerGroupOptions) {
|
|
33
|
+
constructor(private options: ConsumerGroupOptions) {
|
|
34
|
+
super();
|
|
35
|
+
}
|
|
30
36
|
|
|
37
|
+
@trace()
|
|
31
38
|
public async join() {
|
|
32
39
|
await this.findCoordinator();
|
|
40
|
+
await this.options.cluster.setSeedBroker(this.coordinatorId);
|
|
41
|
+
|
|
42
|
+
this.memberId = '';
|
|
33
43
|
await this.joinGroup();
|
|
34
44
|
await this.syncGroup();
|
|
35
45
|
await this.offsetFetch();
|
|
@@ -53,12 +63,16 @@ export class ConsumerGroup {
|
|
|
53
63
|
}
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
public
|
|
66
|
+
public handleLastHeartbeat() {
|
|
57
67
|
if (this.heartbeatError) {
|
|
58
68
|
throw this.heartbeatError;
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
public resetHeartbeat() {
|
|
73
|
+
this.heartbeatError = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
private async findCoordinator() {
|
|
63
77
|
const { coordinators } = await this.options.cluster.sendRequest(API.FIND_COORDINATOR, {
|
|
64
78
|
keyType: KEY_TYPE.GROUP,
|
|
@@ -70,14 +84,14 @@ export class ConsumerGroup {
|
|
|
70
84
|
private async joinGroup(): Promise<void> {
|
|
71
85
|
const { cluster, groupId, groupInstanceId, sessionTimeoutMs, rebalanceTimeoutMs, topics } = this.options;
|
|
72
86
|
try {
|
|
73
|
-
const response = await cluster.
|
|
87
|
+
const response = await cluster.sendRequest(API.JOIN_GROUP, {
|
|
74
88
|
groupId,
|
|
75
89
|
groupInstanceId,
|
|
76
90
|
memberId: this.memberId,
|
|
77
91
|
sessionTimeoutMs,
|
|
78
92
|
rebalanceTimeoutMs,
|
|
79
|
-
protocolType:
|
|
80
|
-
protocols: [{ name:
|
|
93
|
+
protocolType: 'consumer',
|
|
94
|
+
protocols: [{ name: 'RoundRobinAssigner', metadata: { version: 0, topics } }],
|
|
81
95
|
reason: null,
|
|
82
96
|
});
|
|
83
97
|
this.memberId = response.memberId;
|
|
@@ -113,16 +127,16 @@ export class ConsumerGroup {
|
|
|
113
127
|
assignments = Object.entries(memberAssignments).map(([memberId, assignment]) => ({ memberId, assignment }));
|
|
114
128
|
}
|
|
115
129
|
|
|
116
|
-
const response = await cluster.
|
|
130
|
+
const response = await cluster.sendRequest(API.SYNC_GROUP, {
|
|
117
131
|
groupId,
|
|
118
132
|
groupInstanceId,
|
|
119
133
|
memberId: this.memberId,
|
|
120
134
|
generationId: this.generationId,
|
|
121
|
-
protocolType:
|
|
122
|
-
protocolName:
|
|
135
|
+
protocolType: 'consumer',
|
|
136
|
+
protocolName: 'RoundRobinAssigner',
|
|
123
137
|
assignments,
|
|
124
138
|
});
|
|
125
|
-
metadata.setAssignment(JSON.parse(response.assignments ||
|
|
139
|
+
metadata.setAssignment(JSON.parse(response.assignments || '{}') as Assignment);
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
private async offsetFetch() {
|
|
@@ -144,31 +158,35 @@ export class ConsumerGroup {
|
|
|
144
158
|
};
|
|
145
159
|
if (!request.groups.length) return;
|
|
146
160
|
|
|
147
|
-
const response = await cluster.
|
|
161
|
+
const response = await cluster.sendRequest(API.OFFSET_FETCH, request);
|
|
162
|
+
|
|
163
|
+
const topicPartitions: Record<string, Set<number>> = {};
|
|
148
164
|
response.groups.forEach((group) => {
|
|
149
165
|
group.topics.forEach((topic) => {
|
|
150
|
-
topic.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
topicPartitions[topic.name] ??= new Set();
|
|
167
|
+
topic.partitions.forEach(({ partitionIndex, committedOffset }) => {
|
|
168
|
+
if (committedOffset >= 0) {
|
|
169
|
+
topicPartitions[topic.name].add(partitionIndex);
|
|
170
|
+
offsetManager.resolve(topic.name, partitionIndex, committedOffset);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
155
173
|
});
|
|
156
174
|
});
|
|
157
|
-
offsetManager.flush();
|
|
175
|
+
offsetManager.flush(topicPartitions);
|
|
158
176
|
}
|
|
159
177
|
|
|
160
|
-
public async offsetCommit() {
|
|
178
|
+
public async offsetCommit(topicPartitions: Record<string, Set<number>>) {
|
|
161
179
|
const { cluster, groupId, groupInstanceId, offsetManager } = this.options;
|
|
162
180
|
const request = {
|
|
163
181
|
groupId,
|
|
164
182
|
groupInstanceId,
|
|
165
183
|
memberId: this.memberId,
|
|
166
184
|
generationIdOrMemberEpoch: this.generationId,
|
|
167
|
-
topics: Object.entries(
|
|
185
|
+
topics: Object.entries(topicPartitions).map(([topic, partitions]) => ({
|
|
168
186
|
name: topic,
|
|
169
|
-
partitions:
|
|
170
|
-
partitionIndex
|
|
171
|
-
committedOffset:
|
|
187
|
+
partitions: [...partitions].map((partitionIndex) => ({
|
|
188
|
+
partitionIndex,
|
|
189
|
+
committedOffset: offsetManager.pendingOffsets[topic][partitionIndex],
|
|
172
190
|
committedLeaderEpoch: -1,
|
|
173
191
|
committedMetadata: null,
|
|
174
192
|
})),
|
|
@@ -177,13 +195,13 @@ export class ConsumerGroup {
|
|
|
177
195
|
if (!request.topics.length) {
|
|
178
196
|
return;
|
|
179
197
|
}
|
|
180
|
-
await cluster.
|
|
181
|
-
|
|
198
|
+
await cluster.sendRequest(API.OFFSET_COMMIT, request);
|
|
199
|
+
this.emit('offsetCommit');
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
public async heartbeat() {
|
|
185
203
|
const { cluster, groupId, groupInstanceId } = this.options;
|
|
186
|
-
await cluster.
|
|
204
|
+
await cluster.sendRequest(API.HEARTBEAT, {
|
|
187
205
|
groupId,
|
|
188
206
|
groupInstanceId,
|
|
189
207
|
memberId: this.memberId,
|
|
@@ -192,10 +210,14 @@ export class ConsumerGroup {
|
|
|
192
210
|
}
|
|
193
211
|
|
|
194
212
|
public async leaveGroup() {
|
|
213
|
+
if (this.coordinatorId === -1) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
195
217
|
const { cluster, groupId, groupInstanceId } = this.options;
|
|
196
218
|
this.stopHeartbeater();
|
|
197
219
|
try {
|
|
198
|
-
await cluster.
|
|
220
|
+
await cluster.sendRequest(API.LEAVE_GROUP, {
|
|
199
221
|
groupId,
|
|
200
222
|
members: [{ memberId: this.memberId, groupInstanceId, reason: null }],
|
|
201
223
|
});
|
|
@@ -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,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import { API, API_ERROR } from '../api';
|
|
3
|
+
import { IsolationLevel } from '../api/fetch';
|
|
4
|
+
import { Assignment } from '../api/sync-group';
|
|
5
|
+
import { Cluster } from '../cluster';
|
|
6
|
+
import { distributeMessagesToTopicPartitionLeaders } from '../distributors/messages-to-topic-partition-leaders';
|
|
7
|
+
import { Message } from '../types';
|
|
8
|
+
import { delay } from '../utils/delay';
|
|
9
|
+
import { ConnectionError, KafkaTSApiError } from '../utils/error';
|
|
10
|
+
import { log } from '../utils/logger';
|
|
11
|
+
import { createTracer } from '../utils/tracer';
|
|
12
|
+
import { ConsumerGroup } from './consumer-group';
|
|
13
|
+
import { ConsumerMetadata } from './consumer-metadata';
|
|
14
|
+
import { BatchGranularity, FetchManager } from './fetch-manager';
|
|
15
|
+
import { OffsetManager } from './offset-manager';
|
|
16
|
+
|
|
17
|
+
const trace = createTracer('Consumer');
|
|
13
18
|
|
|
14
19
|
export type ConsumerOptions = {
|
|
15
20
|
topics: string[];
|
|
@@ -25,35 +30,40 @@ export type ConsumerOptions = {
|
|
|
25
30
|
partitionMaxBytes?: number;
|
|
26
31
|
allowTopicAutoCreation?: boolean;
|
|
27
32
|
fromBeginning?: boolean;
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
batchGranularity?: BatchGranularity;
|
|
34
|
+
concurrency?: number;
|
|
35
|
+
} & ({ onBatch: (messages: Required<Message>[]) => unknown } | { onMessage: (message: Required<Message>) => unknown });
|
|
30
36
|
|
|
31
|
-
export class Consumer {
|
|
37
|
+
export class Consumer extends EventEmitter<{ offsetCommit: [] }> {
|
|
32
38
|
private options: Required<ConsumerOptions>;
|
|
33
39
|
private metadata: ConsumerMetadata;
|
|
34
40
|
private consumerGroup: ConsumerGroup | undefined;
|
|
35
41
|
private offsetManager: OffsetManager;
|
|
42
|
+
private fetchManager?: FetchManager;
|
|
36
43
|
private stopHook: (() => void) | undefined;
|
|
37
44
|
|
|
38
45
|
constructor(
|
|
39
46
|
private cluster: Cluster,
|
|
40
47
|
options: ConsumerOptions,
|
|
41
48
|
) {
|
|
49
|
+
super();
|
|
50
|
+
|
|
42
51
|
this.options = {
|
|
43
52
|
...options,
|
|
44
53
|
groupId: options.groupId ?? null,
|
|
45
54
|
groupInstanceId: options.groupInstanceId ?? null,
|
|
46
|
-
rackId: options.rackId ??
|
|
55
|
+
rackId: options.rackId ?? '',
|
|
47
56
|
sessionTimeoutMs: options.sessionTimeoutMs ?? 30_000,
|
|
48
57
|
rebalanceTimeoutMs: options.rebalanceTimeoutMs ?? 60_000,
|
|
49
58
|
maxWaitMs: options.maxWaitMs ?? 5000,
|
|
50
59
|
minBytes: options.minBytes ?? 1,
|
|
51
|
-
maxBytes: options.maxBytes ??
|
|
52
|
-
partitionMaxBytes: options.partitionMaxBytes ??
|
|
60
|
+
maxBytes: options.maxBytes ?? 1_048_576,
|
|
61
|
+
partitionMaxBytes: options.partitionMaxBytes ?? 1_048_576,
|
|
53
62
|
isolationLevel: options.isolationLevel ?? IsolationLevel.READ_UNCOMMITTED,
|
|
54
63
|
allowTopicAutoCreation: options.allowTopicAutoCreation ?? false,
|
|
55
64
|
fromBeginning: options.fromBeginning ?? false,
|
|
56
|
-
|
|
65
|
+
batchGranularity: options.batchGranularity ?? 'partition',
|
|
66
|
+
concurrency: options.concurrency ?? 1,
|
|
57
67
|
};
|
|
58
68
|
|
|
59
69
|
this.metadata = new ConsumerMetadata({ cluster: this.cluster });
|
|
@@ -74,8 +84,10 @@ export class Consumer {
|
|
|
74
84
|
offsetManager: this.offsetManager,
|
|
75
85
|
})
|
|
76
86
|
: undefined;
|
|
87
|
+
this.consumerGroup?.on('offsetCommit', () => this.emit('offsetCommit'));
|
|
77
88
|
}
|
|
78
89
|
|
|
90
|
+
@trace()
|
|
79
91
|
public async start(): Promise<void> {
|
|
80
92
|
const { topics, allowTopicAutoCreation, fromBeginning } = this.options;
|
|
81
93
|
|
|
@@ -88,88 +100,82 @@ export class Consumer {
|
|
|
88
100
|
await this.offsetManager.fetchOffsets({ fromBeginning });
|
|
89
101
|
await this.consumerGroup?.join();
|
|
90
102
|
} catch (error) {
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
log.warn('Failed to start consumer', error);
|
|
104
|
+
log.debug(`Restarting consumer in 1 second...`);
|
|
93
105
|
await delay(1000);
|
|
94
106
|
|
|
95
107
|
if (this.stopHook) return (this.stopHook as () => void)();
|
|
96
108
|
return this.close(true).then(() => this.start());
|
|
97
109
|
}
|
|
98
|
-
this.
|
|
110
|
+
this.startFetchManager();
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
@trace()
|
|
114
|
+
public async close(force = false): Promise<void> {
|
|
115
|
+
if (!force) {
|
|
116
|
+
await new Promise<void>(async (resolve) => {
|
|
117
|
+
this.stopHook = resolve;
|
|
118
|
+
await this.fetchManager?.stop();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
await this.consumerGroup?.leaveGroup().catch((error) => log.debug(`Failed to leave group: ${error.message}`));
|
|
122
|
+
await this.cluster.disconnect().catch((error) => log.debug(`Failed to disconnect: ${error.message}`));
|
|
123
|
+
}
|
|
104
124
|
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
private async startFetchManager() {
|
|
126
|
+
const { batchGranularity, concurrency } = this.options;
|
|
107
127
|
|
|
108
128
|
while (!this.stopHook) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
129
|
+
this.consumerGroup?.resetHeartbeat();
|
|
130
|
+
|
|
131
|
+
// TODO: If leader is not available, find another read replica
|
|
132
|
+
const nodeAssignments = Object.entries(
|
|
133
|
+
distributeMessagesToTopicPartitionLeaders(
|
|
134
|
+
Object.entries(this.metadata.getAssignment()).flatMap(([topic, partitions]) =>
|
|
135
|
+
partitions.map((partition) => ({ topic, partition })),
|
|
114
136
|
),
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
this.metadata.getTopicPartitionLeaderIds(),
|
|
138
|
+
),
|
|
139
|
+
).map(([nodeId, assignment]) => ({
|
|
140
|
+
nodeId: parseInt(nodeId),
|
|
141
|
+
assignment: Object.fromEntries(
|
|
142
|
+
Object.entries(assignment).map(([topic, partitions]) => [
|
|
143
|
+
topic,
|
|
144
|
+
Object.keys(partitions).map(Number),
|
|
145
|
+
]),
|
|
146
|
+
),
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
const numPartitions = Object.values(this.metadata.getAssignment()).flat().length;
|
|
150
|
+
const numProcessors = Math.min(concurrency, numPartitions);
|
|
151
|
+
|
|
152
|
+
this.fetchManager = new FetchManager({
|
|
153
|
+
fetch: this.fetch.bind(this),
|
|
154
|
+
process: this.process.bind(this),
|
|
155
|
+
metadata: this.metadata,
|
|
156
|
+
consumerGroup: this.consumerGroup,
|
|
157
|
+
nodeAssignments,
|
|
158
|
+
batchGranularity,
|
|
159
|
+
concurrency: numProcessors,
|
|
160
|
+
});
|
|
118
161
|
|
|
119
162
|
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
|
-
}
|
|
163
|
+
await this.fetchManager.start();
|
|
159
164
|
|
|
160
165
|
if (!nodeAssignments.length) {
|
|
161
|
-
|
|
166
|
+
log.debug('No partitions assigned. Waiting for reassignment...');
|
|
162
167
|
await delay(this.options.maxWaitMs);
|
|
163
|
-
|
|
168
|
+
this.consumerGroup?.handleLastHeartbeat();
|
|
164
169
|
}
|
|
165
170
|
} catch (error) {
|
|
171
|
+
await this.fetchManager.stop();
|
|
172
|
+
|
|
166
173
|
if ((error as KafkaTSApiError).errorCode === API_ERROR.REBALANCE_IN_PROGRESS) {
|
|
167
|
-
|
|
168
|
-
shouldReassign = true;
|
|
174
|
+
log.debug('Rebalance in progress...');
|
|
169
175
|
continue;
|
|
170
176
|
}
|
|
171
177
|
if ((error as KafkaTSApiError).errorCode === API_ERROR.FENCED_INSTANCE_ID) {
|
|
172
|
-
|
|
178
|
+
log.debug('New consumer with the same groupInstanceId joined. Exiting the consumer...');
|
|
173
179
|
this.close();
|
|
174
180
|
break;
|
|
175
181
|
}
|
|
@@ -177,28 +183,45 @@ export class Consumer {
|
|
|
177
183
|
error instanceof ConnectionError ||
|
|
178
184
|
(error instanceof KafkaTSApiError && error.errorCode === API_ERROR.NOT_COORDINATOR)
|
|
179
185
|
) {
|
|
180
|
-
|
|
186
|
+
log.debug(`${error.message}. Restarting consumer...`);
|
|
181
187
|
this.close().then(() => this.start());
|
|
182
188
|
break;
|
|
183
189
|
}
|
|
184
|
-
|
|
185
|
-
|
|
190
|
+
log.error((error as Error).message, error);
|
|
191
|
+
this.close();
|
|
186
192
|
break;
|
|
187
193
|
}
|
|
188
194
|
}
|
|
189
195
|
this.stopHook?.();
|
|
190
|
-
}
|
|
196
|
+
}
|
|
191
197
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
@trace((messages) => ({ count: messages.length }))
|
|
199
|
+
private async process(messages: Required<Message>[]) {
|
|
200
|
+
const { options } = this;
|
|
201
|
+
|
|
202
|
+
const topicPartitions: Record<string, Set<number>> = {};
|
|
203
|
+
for (const { topic, partition } of messages) {
|
|
204
|
+
topicPartitions[topic] ??= new Set();
|
|
205
|
+
topicPartitions[topic].add(partition);
|
|
197
206
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
|
|
208
|
+
if ('onBatch' in options) {
|
|
209
|
+
await options.onBatch(messages);
|
|
210
|
+
|
|
211
|
+
messages.forEach(({ topic, partition, offset }) =>
|
|
212
|
+
this.offsetManager.resolve(topic, partition, offset + 1n),
|
|
213
|
+
);
|
|
214
|
+
} else if ('onMessage' in options) {
|
|
215
|
+
for (const message of messages) {
|
|
216
|
+
await options.onMessage(message);
|
|
217
|
+
|
|
218
|
+
const { topic, partition, offset } = message;
|
|
219
|
+
this.offsetManager.resolve(topic, partition, offset + 1n);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await this.consumerGroup?.offsetCommit(topicPartitions);
|
|
224
|
+
this.offsetManager.flush(topicPartitions);
|
|
202
225
|
}
|
|
203
226
|
|
|
204
227
|
private fetch(nodeId: number, assignment: Assignment) {
|