kafka-ts 0.0.1-beta.3 → 0.0.1-beta.6
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 +19 -6
- package/README.md +15 -21
- 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/docker-compose.yml +3 -3
- package/examples/package-lock.json +3501 -3
- package/examples/package.json +8 -1
- package/examples/src/benchmark/common.ts +98 -0
- package/examples/src/benchmark/kafka-ts.ts +67 -0
- package/examples/src/benchmark/kafkajs.ts +51 -0
- package/examples/src/client.ts +4 -1
- package/examples/src/consumer.ts +7 -1
- package/examples/src/create-topic.ts +3 -3
- package/examples/src/opentelemetry.ts +46 -0
- package/examples/src/producer.ts +11 -11
- package/examples/src/replicator.ts +2 -1
- package/package.json +4 -2
- package/scripts/create-scram-user.sh +4 -2
- package/scripts/generate-certs.sh +2 -0
- package/src/__snapshots__/cluster.test.ts.snap +160 -53
- package/src/api/fetch.ts +83 -28
- package/src/api/index.ts +3 -1
- package/src/api/metadata.ts +1 -1
- package/src/api/produce.ts +7 -10
- package/src/cluster.test.ts +10 -7
- package/src/cluster.ts +36 -38
- 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 +31 -17
- package/src/consumer/consumer-group.ts +43 -21
- package/src/consumer/consumer.ts +58 -37
- package/src/consumer/fetch-manager.ts +36 -46
- package/src/consumer/fetcher.ts +20 -13
- package/src/consumer/offset-manager.ts +18 -7
- package/src/consumer/processor.ts +14 -8
- package/src/distributors/assignments-to-replicas.ts +1 -3
- package/src/index.ts +2 -0
- package/src/metadata.ts +4 -0
- package/src/producer/producer.ts +14 -9
- package/src/utils/api.ts +1 -1
- package/src/utils/decoder.ts +9 -3
- package/src/utils/encoder.ts +26 -19
- package/src/utils/logger.ts +37 -0
- package/src/utils/tracer.ts +40 -22
- 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/src/utils/debug.ts +0 -9
package/src/cluster.test.ts
CHANGED
|
@@ -8,20 +8,21 @@ import { createKafkaClient } from './client';
|
|
|
8
8
|
import { Cluster } from './cluster';
|
|
9
9
|
import { KafkaTSApiError } from './utils/error';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const kafka = createKafkaClient({
|
|
12
12
|
clientId: 'kafka-ts',
|
|
13
13
|
bootstrapServers: [{ host: 'localhost', port: 9092 }],
|
|
14
14
|
sasl: saslPlain({ username: 'admin', password: 'admin' }),
|
|
15
15
|
ssl: { ca: readFileSync('./certs/ca.crt').toString() },
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
describe.sequential('
|
|
18
|
+
describe.sequential('Low-level API', () => {
|
|
19
19
|
const groupId = randomBytes(16).toString('hex');
|
|
20
20
|
|
|
21
21
|
let cluster: Cluster;
|
|
22
22
|
|
|
23
23
|
beforeAll(async () => {
|
|
24
|
-
cluster = await kafka.createCluster()
|
|
24
|
+
cluster = await kafka.createCluster();
|
|
25
|
+
await cluster.connect();
|
|
25
26
|
|
|
26
27
|
const metadataResult = await cluster.sendRequest(API.METADATA, {
|
|
27
28
|
topics: null,
|
|
@@ -52,8 +53,8 @@ describe.sequential('Request handler', () => {
|
|
|
52
53
|
topics: [
|
|
53
54
|
{
|
|
54
55
|
name: 'kafka-ts-test-topic',
|
|
55
|
-
numPartitions:
|
|
56
|
-
replicationFactor:
|
|
56
|
+
numPartitions: 10,
|
|
57
|
+
replicationFactor: 3,
|
|
57
58
|
assignments: [],
|
|
58
59
|
configs: [],
|
|
59
60
|
},
|
|
@@ -89,6 +90,7 @@ describe.sequential('Request handler', () => {
|
|
|
89
90
|
expect(result).toMatchSnapshot();
|
|
90
91
|
});
|
|
91
92
|
|
|
93
|
+
let partitionIndex = 0;
|
|
92
94
|
let leaderId = 0;
|
|
93
95
|
|
|
94
96
|
it('should request metadata for a topic', async () => {
|
|
@@ -97,6 +99,7 @@ describe.sequential('Request handler', () => {
|
|
|
97
99
|
allowTopicAutoCreation: false,
|
|
98
100
|
includeTopicAuthorizedOperations: false,
|
|
99
101
|
});
|
|
102
|
+
partitionIndex = result.topics[0].partitions[0].partitionIndex;
|
|
100
103
|
leaderId = result.topics[0].partitions[0].leaderId;
|
|
101
104
|
result.controllerId = 0;
|
|
102
105
|
result.topics.forEach((topic) => {
|
|
@@ -134,7 +137,7 @@ describe.sequential('Request handler', () => {
|
|
|
134
137
|
name: 'kafka-ts-test-topic',
|
|
135
138
|
partitionData: [
|
|
136
139
|
{
|
|
137
|
-
index:
|
|
140
|
+
index: partitionIndex,
|
|
138
141
|
baseOffset: 0n,
|
|
139
142
|
partitionLeaderEpoch: 0,
|
|
140
143
|
attributes: 0,
|
|
@@ -180,7 +183,7 @@ describe.sequential('Request handler', () => {
|
|
|
180
183
|
topicId,
|
|
181
184
|
partitions: [
|
|
182
185
|
{
|
|
183
|
-
partition:
|
|
186
|
+
partition: partitionIndex,
|
|
184
187
|
currentLeaderEpoch: -1,
|
|
185
188
|
fetchOffset: 0n,
|
|
186
189
|
lastFetchedEpoch: 0,
|
package/src/cluster.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { TcpSocketConnectOpts } from 'net';
|
|
2
2
|
import { TLSSocketOptions } from 'tls';
|
|
3
3
|
import { API } from './api';
|
|
4
|
+
import { Metadata } from './api/metadata';
|
|
4
5
|
import { Broker, SASLProvider } from './broker';
|
|
5
6
|
import { SendRequest } from './connection';
|
|
6
|
-
import {
|
|
7
|
+
import { KafkaTSError } from './utils/error';
|
|
8
|
+
import { log } from './utils/logger';
|
|
7
9
|
|
|
8
10
|
type ClusterOptions = {
|
|
9
11
|
clientId: string | null;
|
|
@@ -13,73 +15,69 @@ type ClusterOptions = {
|
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export class Cluster {
|
|
16
|
-
private seedBroker:
|
|
18
|
+
private seedBroker = new Broker({ clientId: null, sasl: null, ssl: null, options: { port: 9092 } });
|
|
17
19
|
private brokerById: Record<number, Broker> = {};
|
|
20
|
+
private brokerMetadata: Record<number, Metadata['brokers'][number]> = {};
|
|
18
21
|
|
|
19
|
-
constructor(private options: ClusterOptions) {
|
|
20
|
-
this.seedBroker = new Broker({
|
|
21
|
-
clientId: this.options.clientId,
|
|
22
|
-
sasl: this.options.sasl,
|
|
23
|
-
ssl: this.options.ssl,
|
|
24
|
-
options: this.options.bootstrapServers[0],
|
|
25
|
-
});
|
|
26
|
-
}
|
|
22
|
+
constructor(private options: ClusterOptions) {}
|
|
27
23
|
|
|
28
24
|
public async connect() {
|
|
29
|
-
await this.
|
|
25
|
+
this.seedBroker = await this.findSeedBroker();
|
|
26
|
+
this.brokerById = {};
|
|
27
|
+
|
|
30
28
|
const metadata = await this.sendRequest(API.METADATA, {
|
|
31
29
|
allowTopicAutoCreation: false,
|
|
32
30
|
includeTopicAuthorizedOperations: false,
|
|
33
31
|
topics: [],
|
|
34
32
|
});
|
|
35
|
-
|
|
36
|
-
this.brokerById = Object.fromEntries(
|
|
37
|
-
metadata.brokers.map(({ nodeId, ...options }) => [
|
|
38
|
-
nodeId,
|
|
39
|
-
new Broker({
|
|
40
|
-
clientId: this.options.clientId,
|
|
41
|
-
sasl: this.options.sasl,
|
|
42
|
-
ssl: this.options.ssl,
|
|
43
|
-
options,
|
|
44
|
-
}),
|
|
45
|
-
]),
|
|
46
|
-
);
|
|
47
|
-
return this;
|
|
33
|
+
this.brokerMetadata = Object.fromEntries(metadata.brokers.map((options) => [options.nodeId, options]));
|
|
48
34
|
}
|
|
49
35
|
|
|
50
36
|
public async disconnect() {
|
|
51
|
-
await Promise.all([
|
|
52
|
-
this.seedBroker.disconnect(),
|
|
53
|
-
...Object.values(this.brokerById).map((broker) => broker.disconnect()),
|
|
54
|
-
]);
|
|
37
|
+
await Promise.all([this.seedBroker.disconnect(), ...Object.values(this.brokerById).map((x) => x.disconnect())]);
|
|
55
38
|
}
|
|
56
39
|
|
|
40
|
+
public setSeedBroker = async (nodeId: number) => {
|
|
41
|
+
await this.seedBroker.disconnect();
|
|
42
|
+
this.seedBroker = await this.acquireBroker(nodeId);
|
|
43
|
+
};
|
|
44
|
+
|
|
57
45
|
public sendRequest: SendRequest = (...args) => this.seedBroker.sendRequest(...args);
|
|
58
46
|
|
|
59
47
|
public sendRequestToNode =
|
|
60
48
|
(nodeId: number): SendRequest =>
|
|
61
49
|
async (...args) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
throw new ConnectionError(`Broker ${nodeId} is not available`);
|
|
50
|
+
if (!this.brokerById[nodeId]) {
|
|
51
|
+
this.brokerById[nodeId] = await this.acquireBroker(nodeId);
|
|
65
52
|
}
|
|
66
|
-
|
|
67
|
-
return broker.sendRequest(...args);
|
|
53
|
+
return this.brokerById[nodeId].sendRequest(...args);
|
|
68
54
|
};
|
|
69
55
|
|
|
70
|
-
|
|
56
|
+
public async acquireBroker(nodeId: number) {
|
|
57
|
+
const broker = new Broker({
|
|
58
|
+
clientId: this.options.clientId,
|
|
59
|
+
sasl: this.options.sasl,
|
|
60
|
+
ssl: this.options.ssl,
|
|
61
|
+
options: this.brokerMetadata[nodeId],
|
|
62
|
+
});
|
|
63
|
+
await broker.connect();
|
|
64
|
+
return broker;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async findSeedBroker() {
|
|
71
68
|
const randomizedBrokers = this.options.bootstrapServers.toSorted(() => Math.random() - 0.5);
|
|
72
69
|
for (const options of randomizedBrokers) {
|
|
73
70
|
try {
|
|
74
|
-
|
|
71
|
+
const broker = await new Broker({
|
|
75
72
|
clientId: this.options.clientId,
|
|
76
73
|
sasl: this.options.sasl,
|
|
77
74
|
ssl: this.options.ssl,
|
|
78
75
|
options,
|
|
79
|
-
})
|
|
80
|
-
|
|
76
|
+
});
|
|
77
|
+
await broker.connect();
|
|
78
|
+
return broker;
|
|
81
79
|
} catch (error) {
|
|
82
|
-
|
|
80
|
+
log.warn(`Failed to connect to seed broker ${options.host}:${options.port}`, error);
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
throw new KafkaTSError('No seed brokers found');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { gzip, unzip } from 'zlib';
|
|
2
|
+
import { Codec } from './types';
|
|
3
|
+
|
|
4
|
+
export const GZIP: Codec = {
|
|
5
|
+
compress: async (data) =>
|
|
6
|
+
new Promise<Buffer>((resolve, reject) => gzip(data, (err, result) => (err ? reject(err) : resolve(result)))),
|
|
7
|
+
decompress: async (data) =>
|
|
8
|
+
new Promise<Buffer>((resolve, reject) => unzip(data, (err, result) => (err ? reject(err) : resolve(result)))),
|
|
9
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { GZIP } from './gzip';
|
|
2
|
+
import { NONE } from './none';
|
|
3
|
+
import { Codec } from './types';
|
|
4
|
+
|
|
5
|
+
const codecs: Record<number, Codec> = {
|
|
6
|
+
0: NONE,
|
|
7
|
+
1: GZIP,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const findCodec = (type: number) => {
|
|
11
|
+
const codec = codecs[type];
|
|
12
|
+
if (!codec) {
|
|
13
|
+
throw new Error(`Unsupported codec: ${type}`);
|
|
14
|
+
}
|
|
15
|
+
return codec;
|
|
16
|
+
};
|
package/src/connection.ts
CHANGED
|
@@ -6,11 +6,12 @@ import { Api } from './utils/api';
|
|
|
6
6
|
import { Decoder } from './utils/decoder';
|
|
7
7
|
import { Encoder } from './utils/encoder';
|
|
8
8
|
import { ConnectionError } from './utils/error';
|
|
9
|
+
import { log } from './utils/logger';
|
|
9
10
|
import { createTracer } from './utils/tracer';
|
|
10
11
|
|
|
11
12
|
const trace = createTracer('Connection');
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
type ConnectionOptions = {
|
|
14
15
|
clientId: string | null;
|
|
15
16
|
connection: TcpSocketConnectOpts;
|
|
16
17
|
ssl: TLSSocketOptions | null;
|
|
@@ -24,14 +25,14 @@ export class Connection {
|
|
|
24
25
|
[correlationId: number]: { resolve: (response: RawResonse) => void; reject: (error: Error) => void };
|
|
25
26
|
} = {};
|
|
26
27
|
private lastCorrelationId = 0;
|
|
27
|
-
private
|
|
28
|
+
private chunks: Buffer[] = [];
|
|
28
29
|
|
|
29
30
|
constructor(private options: ConnectionOptions) {}
|
|
30
31
|
|
|
31
32
|
@trace()
|
|
32
33
|
public async connect() {
|
|
33
34
|
this.queue = {};
|
|
34
|
-
this.
|
|
35
|
+
this.chunks = [];
|
|
35
36
|
|
|
36
37
|
await new Promise<void>((resolve, reject) => {
|
|
37
38
|
const { ssl, connection } = this.options;
|
|
@@ -59,6 +60,7 @@ export class Connection {
|
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
@trace()
|
|
62
64
|
public disconnect() {
|
|
63
65
|
this.socket.removeAllListeners();
|
|
64
66
|
return new Promise<void>((resolve) => {
|
|
@@ -69,9 +71,10 @@ export class Connection {
|
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
@trace((api, body) => ({
|
|
74
|
+
@trace((api, body) => ({ message: getApiName(api), body }))
|
|
73
75
|
public async sendRequest<Request, Response>(api: Api<Request, Response>, body: Request): Promise<Response> {
|
|
74
76
|
const correlationId = this.nextCorrelationId();
|
|
77
|
+
const apiName = getApiName(api);
|
|
75
78
|
|
|
76
79
|
const encoder = new Encoder()
|
|
77
80
|
.writeInt16(api.apiKey)
|
|
@@ -79,18 +82,25 @@ export class Connection {
|
|
|
79
82
|
.writeInt32(correlationId)
|
|
80
83
|
.writeString(this.options.clientId);
|
|
81
84
|
|
|
82
|
-
const request = api.request(encoder, body)
|
|
83
|
-
const requestEncoder = new Encoder().writeInt32(request.
|
|
85
|
+
const request = api.request(encoder, body);
|
|
86
|
+
const requestEncoder = new Encoder().writeInt32(request.getByteLength()).writeEncoder(request);
|
|
84
87
|
|
|
88
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
85
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
|
+
|
|
86
95
|
try {
|
|
87
|
-
await this.write(requestEncoder.value());
|
|
88
96
|
this.queue[correlationId] = { resolve, reject };
|
|
97
|
+
await this.write(requestEncoder.value());
|
|
89
98
|
} catch (error) {
|
|
90
99
|
reject(error);
|
|
91
100
|
}
|
|
92
101
|
});
|
|
93
|
-
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
const response = await api.response(responseDecoder);
|
|
94
104
|
|
|
95
105
|
assert(
|
|
96
106
|
responseDecoder.getOffset() - 4 === responseSize,
|
|
@@ -115,12 +125,13 @@ export class Connection {
|
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
private handleData(buffer: Buffer) {
|
|
118
|
-
this.
|
|
119
|
-
|
|
128
|
+
this.chunks.push(buffer);
|
|
129
|
+
|
|
130
|
+
const decoder = new Decoder(Buffer.concat(this.chunks));
|
|
131
|
+
if (decoder.getBufferLength() < 4) {
|
|
120
132
|
return;
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
const decoder = new Decoder(this.buffer);
|
|
124
135
|
const size = decoder.readInt32();
|
|
125
136
|
if (size !== decoder.getBufferLength() - 4) {
|
|
126
137
|
return;
|
|
@@ -128,15 +139,18 @@ export class Connection {
|
|
|
128
139
|
|
|
129
140
|
const correlationId = decoder.readInt32();
|
|
130
141
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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 = [];
|
|
136
150
|
}
|
|
137
151
|
|
|
138
152
|
private nextCorrelationId() {
|
|
139
|
-
return
|
|
153
|
+
return this.lastCorrelationId++;
|
|
140
154
|
}
|
|
141
155
|
}
|
|
142
156
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
1
2
|
import { API, API_ERROR } from '../api';
|
|
2
3
|
import { KEY_TYPE } from '../api/find-coordinator';
|
|
3
4
|
import { Assignment, MemberAssignment } from '../api/sync-group';
|
|
4
5
|
import { Cluster } from '../cluster';
|
|
5
6
|
import { KafkaTSApiError, KafkaTSError } from '../utils/error';
|
|
7
|
+
import { createTracer } from '../utils/tracer';
|
|
6
8
|
import { ConsumerMetadata } from './consumer-metadata';
|
|
7
9
|
import { OffsetManager } from './offset-manager';
|
|
8
10
|
|
|
11
|
+
const trace = createTracer('ConsumerGroup');
|
|
12
|
+
|
|
9
13
|
type ConsumerGroupOptions = {
|
|
10
14
|
cluster: Cluster;
|
|
11
15
|
topics: string[];
|
|
@@ -17,7 +21,7 @@ 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
26
|
private memberId = '';
|
|
23
27
|
private generationId = -1;
|
|
@@ -26,10 +30,16 @@ export class ConsumerGroup {
|
|
|
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,7 +84,7 @@ 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,
|
|
@@ -113,7 +127,7 @@ 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,
|
|
@@ -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
|
});
|