kafka-ts 0.0.1-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/.prettierrc +7 -0
- package/LICENSE +24 -0
- package/README.md +88 -0
- package/certs/ca.crt +29 -0
- package/certs/ca.key +52 -0
- package/certs/ca.srl +1 -0
- package/certs/kafka.crt +29 -0
- package/certs/kafka.csr +26 -0
- package/certs/kafka.key +52 -0
- package/certs/kafka.keystore.jks +0 -0
- package/certs/kafka.truststore.jks +0 -0
- 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 +77 -0
- package/dist/api/fetch.js +106 -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 +573 -0
- package/dist/api/index.js +164 -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 +53 -0
- package/dist/api/produce.js +129 -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/broker.d.ts +29 -0
- package/dist/broker.js +60 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +36 -0
- package/dist/cluster.d.ts +24 -0
- package/dist/cluster.js +72 -0
- package/dist/connection.d.ts +25 -0
- package/dist/connection.js +155 -0
- package/dist/consumer/consumer-group.d.ts +36 -0
- package/dist/consumer/consumer-group.js +182 -0
- package/dist/consumer/consumer-metadata.d.ts +7 -0
- package/dist/consumer/consumer-metadata.js +14 -0
- package/dist/consumer/consumer.d.ts +37 -0
- package/dist/consumer/consumer.js +178 -0
- package/dist/consumer/metadata.d.ts +24 -0
- package/dist/consumer/metadata.js +64 -0
- package/dist/consumer/offset-manager.d.ts +22 -0
- package/dist/consumer/offset-manager.js +56 -0
- package/dist/distributors/assignments-to-replicas.d.ts +17 -0
- package/dist/distributors/assignments-to-replicas.js +60 -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/examples/src/replicator.js +34 -0
- package/dist/examples/src/utils/json.js +5 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +19 -0
- package/dist/metadata.d.ts +24 -0
- package/dist/metadata.js +89 -0
- package/dist/producer/producer.d.ts +19 -0
- package/dist/producer/producer.js +111 -0
- package/dist/request-handler.d.ts +16 -0
- package/dist/request-handler.js +67 -0
- package/dist/request-handler.test.d.ts +1 -0
- package/dist/request-handler.test.js +340 -0
- package/dist/src/api/api-versions.js +18 -0
- package/dist/src/api/create-topics.js +46 -0
- package/dist/src/api/delete-topics.js +26 -0
- package/dist/src/api/fetch.js +95 -0
- package/dist/src/api/find-coordinator.js +34 -0
- package/dist/src/api/heartbeat.js +22 -0
- package/dist/src/api/index.js +38 -0
- package/dist/src/api/init-producer-id.js +24 -0
- package/dist/src/api/join-group.js +48 -0
- package/dist/src/api/leave-group.js +30 -0
- package/dist/src/api/list-offsets.js +39 -0
- package/dist/src/api/metadata.js +47 -0
- package/dist/src/api/offset-commit.js +39 -0
- package/dist/src/api/offset-fetch.js +44 -0
- package/dist/src/api/produce.js +119 -0
- package/dist/src/api/sync-group.js +31 -0
- package/dist/src/broker.js +35 -0
- package/dist/src/connection.js +21 -0
- package/dist/src/consumer/consumer-group.js +131 -0
- package/dist/src/consumer/consumer.js +103 -0
- package/dist/src/consumer/metadata.js +52 -0
- package/dist/src/consumer/offset-manager.js +23 -0
- package/dist/src/index.js +19 -0
- package/dist/src/producer/producer.js +84 -0
- package/dist/src/request-handler.js +57 -0
- package/dist/src/request-handler.test.js +321 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/api.js +5 -0
- package/dist/src/utils/decoder.js +161 -0
- package/dist/src/utils/encoder.js +137 -0
- package/dist/src/utils/error.js +10 -0
- package/dist/types.d.ts +9 -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/debug.d.ts +2 -0
- package/dist/utils/debug.js +11 -0
- package/dist/utils/decoder.d.ts +29 -0
- package/dist/utils/decoder.js +147 -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 +122 -0
- package/dist/utils/error.d.ts +11 -0
- package/dist/utils/error.js +27 -0
- package/dist/utils/memo.d.ts +1 -0
- package/dist/utils/memo.js +16 -0
- package/dist/utils/retrier.d.ts +10 -0
- package/dist/utils/retrier.js +22 -0
- package/dist/utils/tracer.d.ts +1 -0
- package/dist/utils/tracer.js +26 -0
- package/docker-compose.yml +104 -0
- package/examples/node_modules/.package-lock.json +22 -0
- package/examples/package-lock.json +30 -0
- package/examples/package.json +14 -0
- package/examples/src/client.ts +9 -0
- package/examples/src/consumer.ts +17 -0
- package/examples/src/create-topic.ts +37 -0
- package/examples/src/producer.ts +24 -0
- package/examples/src/replicator.ts +25 -0
- package/examples/src/utils/json.ts +1 -0
- package/examples/tsconfig.json +7 -0
- package/log4j.properties +95 -0
- package/package.json +17 -0
- package/scripts/generate-certs.sh +24 -0
- package/src/__snapshots__/request-handler.test.ts.snap +1687 -0
- package/src/api/api-versions.ts +21 -0
- package/src/api/create-topics.ts +78 -0
- package/src/api/delete-topics.ts +42 -0
- package/src/api/fetch.ts +143 -0
- package/src/api/find-coordinator.ts +39 -0
- package/src/api/heartbeat.ts +33 -0
- package/src/api/index.ts +164 -0
- package/src/api/init-producer-id.ts +35 -0
- package/src/api/join-group.ts +67 -0
- package/src/api/leave-group.ts +48 -0
- package/src/api/list-offsets.ts +65 -0
- package/src/api/metadata.ts +66 -0
- package/src/api/offset-commit.ts +67 -0
- package/src/api/offset-fetch.ts +74 -0
- package/src/api/produce.ts +173 -0
- package/src/api/sasl-authenticate.ts +21 -0
- package/src/api/sasl-handshake.ts +16 -0
- package/src/api/sync-group.ts +54 -0
- package/src/broker.ts +74 -0
- package/src/client.ts +47 -0
- package/src/cluster.ts +87 -0
- package/src/connection.ts +141 -0
- package/src/consumer/consumer-group.ts +209 -0
- package/src/consumer/consumer-metadata.ts +14 -0
- package/src/consumer/consumer.ts +229 -0
- package/src/consumer/offset-manager.ts +93 -0
- package/src/distributors/assignments-to-replicas.test.ts +43 -0
- package/src/distributors/assignments-to-replicas.ts +85 -0
- package/src/distributors/messages-to-topic-partition-leaders.test.ts +32 -0
- package/src/distributors/messages-to-topic-partition-leaders.ts +19 -0
- package/src/index.ts +3 -0
- package/src/metadata.ts +122 -0
- package/src/producer/producer.ts +132 -0
- package/src/request-handler.test.ts +366 -0
- package/src/types.ts +9 -0
- package/src/utils/api.ts +11 -0
- package/src/utils/debug.ts +9 -0
- package/src/utils/decoder.ts +168 -0
- package/src/utils/delay.ts +1 -0
- package/src/utils/encoder.ts +141 -0
- package/src/utils/error.ts +21 -0
- package/src/utils/memo.ts +12 -0
- package/src/utils/retrier.ts +39 -0
- package/src/utils/tracer.ts +28 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
|
|
8
|
+
type OffsetManagerOptions = {
|
|
9
|
+
cluster: Cluster;
|
|
10
|
+
metadata: ConsumerMetadata;
|
|
11
|
+
isolationLevel: IsolationLevel;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class OffsetManager {
|
|
15
|
+
private currentOffsets: Record<string, Record<number, bigint>> = {};
|
|
16
|
+
public pendingOffsets: Record<string, Record<number, bigint>> = {};
|
|
17
|
+
|
|
18
|
+
constructor(private options: OffsetManagerOptions) {}
|
|
19
|
+
|
|
20
|
+
public getCurrentOffset(topic: string, partition: number) {
|
|
21
|
+
return this.currentOffsets[topic]?.[partition] ?? 0n;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public resolve(topic: string, partition: number, offset: bigint) {
|
|
25
|
+
this.pendingOffsets[topic] ??= {};
|
|
26
|
+
this.pendingOffsets[topic][partition] = offset;
|
|
27
|
+
|
|
28
|
+
this.currentOffsets[topic] ??= {};
|
|
29
|
+
this.currentOffsets[topic][partition] = offset;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public flush() {
|
|
33
|
+
this.pendingOffsets = {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async fetchOffsets(options: { fromBeginning: boolean }) {
|
|
37
|
+
const { metadata } = this.options;
|
|
38
|
+
|
|
39
|
+
const topicPartitions = Object.entries(metadata.getAssignment()).flatMap(([topic, partitions]) =>
|
|
40
|
+
partitions.map((partition) => ({ topic, partition })),
|
|
41
|
+
);
|
|
42
|
+
const nodeTopicPartitions = distributeMessagesToTopicPartitionLeaders(
|
|
43
|
+
topicPartitions,
|
|
44
|
+
metadata.getTopicPartitionLeaderIds(),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await Promise.all(
|
|
48
|
+
Object.entries(nodeTopicPartitions).map(([nodeId, topicPartitions]) =>
|
|
49
|
+
this.listOffsets({
|
|
50
|
+
...options,
|
|
51
|
+
nodeId: parseInt(nodeId),
|
|
52
|
+
nodeAssignment: Object.fromEntries(
|
|
53
|
+
Object.entries(topicPartitions).map(
|
|
54
|
+
([topicName, partitions]) =>
|
|
55
|
+
[topicName, Object.keys(partitions).map(Number)] as [string, number[]],
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
this.flush();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async listOffsets({
|
|
65
|
+
nodeId,
|
|
66
|
+
nodeAssignment,
|
|
67
|
+
fromBeginning,
|
|
68
|
+
}: {
|
|
69
|
+
nodeId: number;
|
|
70
|
+
nodeAssignment: Assignment;
|
|
71
|
+
fromBeginning: boolean;
|
|
72
|
+
}) {
|
|
73
|
+
const { cluster, isolationLevel } = this.options;
|
|
74
|
+
|
|
75
|
+
const offsets = await cluster.sendRequestToNode(nodeId)(API.LIST_OFFSETS, {
|
|
76
|
+
replicaId: -1,
|
|
77
|
+
isolationLevel,
|
|
78
|
+
topics: Object.entries(nodeAssignment)
|
|
79
|
+
.flatMap(([topic, partitions]) => partitions.map((partition) => ({ topic, partition })))
|
|
80
|
+
.map(({ topic, partition }) => ({
|
|
81
|
+
name: topic,
|
|
82
|
+
partitions: [{ partitionIndex: partition, currentLeaderEpoch: -1, timestamp: -1n }],
|
|
83
|
+
})),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
offsets.topics.forEach(({ name, partitions }) => {
|
|
87
|
+
partitions.forEach(({ partitionIndex, offset }) => {
|
|
88
|
+
this.resolve(name, partitionIndex, fromBeginning ? 0n : offset);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
this.flush();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { distributeAssignmentsToNodesBalanced, distributeAssignmentsToNodesOptimized } from "./assignments-to-replicas";
|
|
3
|
+
|
|
4
|
+
describe("Distribute assignments to replica ids", () => {
|
|
5
|
+
describe("distributeAssignmentsToNodesBalanced", () => {
|
|
6
|
+
it("smoke", () => {
|
|
7
|
+
const result = distributeAssignmentsToNodesBalanced({ topic: [0, 1] }, { topic: { 0: [0, 1], 1: [1, 2] } });
|
|
8
|
+
expect(result).toMatchInlineSnapshot(`
|
|
9
|
+
{
|
|
10
|
+
"1": {
|
|
11
|
+
"topic": [
|
|
12
|
+
0,
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
"2": {
|
|
16
|
+
"topic": [
|
|
17
|
+
1,
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
`);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("distributeAssignmentsToNodesOptimized", () => {
|
|
26
|
+
it("smoke", () => {
|
|
27
|
+
const result = distributeAssignmentsToNodesOptimized(
|
|
28
|
+
{ topic: [0, 1] },
|
|
29
|
+
{ topic: { 0: [0, 1], 1: [1, 2] } },
|
|
30
|
+
);
|
|
31
|
+
expect(result).toMatchInlineSnapshot(`
|
|
32
|
+
{
|
|
33
|
+
"1": {
|
|
34
|
+
"topic": [
|
|
35
|
+
0,
|
|
36
|
+
1,
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
`);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
type Assignment = { [topicName: string]: number[] };
|
|
2
|
+
type TopicPartitionReplicaIds = { [topicName: string]: { [partition: number]: number[] } };
|
|
3
|
+
export type NodeAssignment = { [replicaId: number]: Assignment };
|
|
4
|
+
|
|
5
|
+
/** From replica ids pick the one with fewest assignments to balance the load across brokers */
|
|
6
|
+
export const distributeAssignmentsToNodesBalanced = (
|
|
7
|
+
assignment: Assignment,
|
|
8
|
+
topicPartitionReplicaIds: TopicPartitionReplicaIds,
|
|
9
|
+
) => {
|
|
10
|
+
const replicaPartitions = getPartitionsByReplica(assignment, topicPartitionReplicaIds);
|
|
11
|
+
|
|
12
|
+
const result: NodeAssignment = {};
|
|
13
|
+
|
|
14
|
+
for (const [topicName, partitions] of Object.entries(assignment)) {
|
|
15
|
+
for (const partition of partitions) {
|
|
16
|
+
const replicaIds = topicPartitionReplicaIds[topicName][partition];
|
|
17
|
+
const replicaId = replicaIds.reduce((prev, curr) => {
|
|
18
|
+
if (!prev) {
|
|
19
|
+
return curr;
|
|
20
|
+
}
|
|
21
|
+
return (replicaPartitions[prev]?.length ?? 0) < (replicaPartitions[curr]?.length ?? 0) ? prev : curr;
|
|
22
|
+
});
|
|
23
|
+
result[replicaId] ??= {};
|
|
24
|
+
result[replicaId][topicName] ??= [];
|
|
25
|
+
result[replicaId][topicName].push(partition);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Minimize the total number of replicas in the result to reduce the number of requests to different brokers */
|
|
33
|
+
export const distributeAssignmentsToNodesOptimized = (
|
|
34
|
+
assignment: Assignment,
|
|
35
|
+
topicPartitionReplicaIds: TopicPartitionReplicaIds,
|
|
36
|
+
) => {
|
|
37
|
+
const result: NodeAssignment = {};
|
|
38
|
+
|
|
39
|
+
const sortFn = ([, partitionsA]: [string, string[]], [, partitionsB]: [string, string[]]) =>
|
|
40
|
+
partitionsB.length - partitionsA.length;
|
|
41
|
+
|
|
42
|
+
let replicaPartitions = getPartitionsByReplica(assignment, topicPartitionReplicaIds);
|
|
43
|
+
|
|
44
|
+
while (replicaPartitions.length) {
|
|
45
|
+
replicaPartitions.sort(sortFn);
|
|
46
|
+
|
|
47
|
+
const [replicaId, partitions] = replicaPartitions.shift()!;
|
|
48
|
+
if (!partitions.length) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
result[parseInt(replicaId)] = partitions.reduce((acc, partition) => {
|
|
53
|
+
const [topicName, partitionId] = partition.split(":");
|
|
54
|
+
acc[topicName] ??= [];
|
|
55
|
+
acc[topicName].push(parseInt(partitionId));
|
|
56
|
+
return acc;
|
|
57
|
+
}, {} as Assignment);
|
|
58
|
+
|
|
59
|
+
replicaPartitions = replicaPartitions.map(
|
|
60
|
+
([replicaId, replicaPartitions]) =>
|
|
61
|
+
[replicaId, replicaPartitions.filter((partition) => !partitions.includes(partition))] as [
|
|
62
|
+
string,
|
|
63
|
+
string[],
|
|
64
|
+
],
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getPartitionsByReplica = (assignment: Assignment, topicPartitionReplicaIds: TopicPartitionReplicaIds) => {
|
|
72
|
+
const partitionsByReplicaId: { [replicaId: number]: string[] } = {};
|
|
73
|
+
for (const [topicName, partitions] of Object.entries(assignment)) {
|
|
74
|
+
for (const partition of partitions) {
|
|
75
|
+
const replicaIds = topicPartitionReplicaIds[topicName][partition];
|
|
76
|
+
for (const replicaId of replicaIds) {
|
|
77
|
+
partitionsByReplicaId[replicaId] ??= [];
|
|
78
|
+
partitionsByReplicaId[replicaId].push(`${topicName}:${partition}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return Object.entries(partitionsByReplicaId);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const distributeAssignmentsToNodes = distributeAssignmentsToNodesBalanced;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { distributeMessagesToTopicPartitionLeaders } from "./messages-to-topic-partition-leaders";
|
|
3
|
+
|
|
4
|
+
describe("Distribute messages to partition leader ids", () => {
|
|
5
|
+
describe("distributeMessagesToTopicPartitionLeaders", () => {
|
|
6
|
+
it("snoke", () => {
|
|
7
|
+
const result = distributeMessagesToTopicPartitionLeaders(
|
|
8
|
+
[{ topic: "topic", partition: 0, key: null, value: null, offset: 0n, timestamp: 0n, headers: {} }],
|
|
9
|
+
{ topic: { 0: 1 } },
|
|
10
|
+
);
|
|
11
|
+
expect(result).toMatchInlineSnapshot(`
|
|
12
|
+
{
|
|
13
|
+
"1": {
|
|
14
|
+
"topic": {
|
|
15
|
+
"0": [
|
|
16
|
+
{
|
|
17
|
+
"headers": {},
|
|
18
|
+
"key": null,
|
|
19
|
+
"offset": 0n,
|
|
20
|
+
"partition": 0,
|
|
21
|
+
"timestamp": 0n,
|
|
22
|
+
"topic": "topic",
|
|
23
|
+
"value": null,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
`);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type TopicPartitionLeader = { [topicName: string]: { [partitionId: number]: number } };
|
|
2
|
+
type MessagesByNodeTopicPartition<T> = {
|
|
3
|
+
[nodeId: number]: { [topicName: string]: { [partitionId: number]: T[] } };
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const distributeMessagesToTopicPartitionLeaders = <T extends { topic: string; partition: number }>(
|
|
7
|
+
messages: T[],
|
|
8
|
+
topicPartitionLeader: TopicPartitionLeader,
|
|
9
|
+
) => {
|
|
10
|
+
const result: MessagesByNodeTopicPartition<T> = {};
|
|
11
|
+
messages.forEach((message) => {
|
|
12
|
+
const leaderId = topicPartitionLeader[message.topic][message.partition];
|
|
13
|
+
result[leaderId] ??= {};
|
|
14
|
+
result[leaderId][message.topic] ??= {};
|
|
15
|
+
result[leaderId][message.topic][message.partition] ??= [];
|
|
16
|
+
result[leaderId][message.topic][message.partition].push(message);
|
|
17
|
+
});
|
|
18
|
+
return result;
|
|
19
|
+
};
|
package/src/index.ts
ADDED
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { API, API_ERROR } from "./api";
|
|
2
|
+
import { Cluster } from "./cluster";
|
|
3
|
+
import { delay } from "./utils/delay";
|
|
4
|
+
import { KafkaTSApiError } from "./utils/error";
|
|
5
|
+
|
|
6
|
+
type MetadataOptions = {
|
|
7
|
+
cluster: Cluster;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class Metadata {
|
|
11
|
+
private topicPartitions: Record<string, number[]> = {};
|
|
12
|
+
private topicNameById: Record<string, string> = {};
|
|
13
|
+
private topicIdByName: Record<string, string> = {};
|
|
14
|
+
private leaderIdByTopicPartition: Record<string, Record<number, number>> = {};
|
|
15
|
+
private isrNodesByTopicPartition: Record<string, Record<number, number[]>> = {};
|
|
16
|
+
|
|
17
|
+
constructor(private options: MetadataOptions) {}
|
|
18
|
+
|
|
19
|
+
public getTopicPartitionLeaderIds() {
|
|
20
|
+
return this.leaderIdByTopicPartition;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public getTopicPartitionReplicaIds() {
|
|
24
|
+
return this.isrNodesByTopicPartition;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public getTopicPartitions() {
|
|
28
|
+
return this.topicPartitions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public getTopicIdByName(name: string) {
|
|
32
|
+
return this.topicIdByName[name];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public getTopicNameById(id: string) {
|
|
36
|
+
return this.topicNameById[id];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async fetchMetadataIfNecessary({
|
|
40
|
+
topics,
|
|
41
|
+
allowTopicAutoCreation,
|
|
42
|
+
}: {
|
|
43
|
+
topics: string[];
|
|
44
|
+
allowTopicAutoCreation: boolean;
|
|
45
|
+
}) {
|
|
46
|
+
const missingTopics = topics.filter((topic) => !this.topicPartitions[topic]);
|
|
47
|
+
if (!missingTopics.length) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return await this.fetchMetadata({ topics: missingTopics, allowTopicAutoCreation });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (
|
|
55
|
+
error instanceof KafkaTSApiError &&
|
|
56
|
+
error.errorCode === API_ERROR.UNKNOWN_TOPIC_OR_PARTITION &&
|
|
57
|
+
allowTopicAutoCreation
|
|
58
|
+
) {
|
|
59
|
+
// TODO: investigate if we can avoid the delay
|
|
60
|
+
await delay(1000);
|
|
61
|
+
return await this.fetchMetadata({ topics: missingTopics, allowTopicAutoCreation });
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async fetchMetadata({
|
|
68
|
+
topics,
|
|
69
|
+
allowTopicAutoCreation,
|
|
70
|
+
}: {
|
|
71
|
+
topics: string[] | null;
|
|
72
|
+
allowTopicAutoCreation: boolean;
|
|
73
|
+
}) {
|
|
74
|
+
const { cluster } = this.options;
|
|
75
|
+
|
|
76
|
+
const response = await cluster.sendRequest(API.METADATA, {
|
|
77
|
+
allowTopicAutoCreation,
|
|
78
|
+
includeTopicAuthorizedOperations: false,
|
|
79
|
+
topics: topics?.map((name) => ({ id: null, name })) ?? null,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.topicPartitions = {
|
|
83
|
+
...this.topicPartitions,
|
|
84
|
+
...Object.fromEntries(
|
|
85
|
+
response.topics.map((topic) => [
|
|
86
|
+
topic.name,
|
|
87
|
+
topic.partitions.map((partition) => partition.partitionIndex),
|
|
88
|
+
]),
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
this.topicNameById = {
|
|
92
|
+
...this.topicNameById,
|
|
93
|
+
...Object.fromEntries(response.topics.map((topic) => [topic.topicId, topic.name])),
|
|
94
|
+
};
|
|
95
|
+
this.topicIdByName = {
|
|
96
|
+
...this.topicIdByName,
|
|
97
|
+
...Object.fromEntries(response.topics.map((topic) => [topic.name, topic.topicId])),
|
|
98
|
+
};
|
|
99
|
+
this.leaderIdByTopicPartition = {
|
|
100
|
+
...this.leaderIdByTopicPartition,
|
|
101
|
+
...Object.fromEntries(
|
|
102
|
+
response.topics.map((topic) => [
|
|
103
|
+
topic.name,
|
|
104
|
+
Object.fromEntries(
|
|
105
|
+
topic.partitions.map((partition) => [partition.partitionIndex, partition.leaderId]),
|
|
106
|
+
),
|
|
107
|
+
]),
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
this.isrNodesByTopicPartition = {
|
|
111
|
+
...this.isrNodesByTopicPartition,
|
|
112
|
+
...Object.fromEntries(
|
|
113
|
+
response.topics.map((topic) => [
|
|
114
|
+
topic.name,
|
|
115
|
+
Object.fromEntries(
|
|
116
|
+
topic.partitions.map((partition) => [partition.partitionIndex, partition.isrNodes]),
|
|
117
|
+
),
|
|
118
|
+
]),
|
|
119
|
+
),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { API, API_ERROR } from "../api";
|
|
2
|
+
import { Cluster } from "../cluster";
|
|
3
|
+
import { distributeMessagesToTopicPartitionLeaders } from "../distributors/messages-to-topic-partition-leaders";
|
|
4
|
+
import { Metadata } from "../metadata";
|
|
5
|
+
import { Message } from "../types";
|
|
6
|
+
import { delay } from "../utils/delay";
|
|
7
|
+
import { KafkaTSApiError } from "../utils/error";
|
|
8
|
+
import { memo } from "../utils/memo";
|
|
9
|
+
|
|
10
|
+
export type ProducerOptions = {
|
|
11
|
+
allowTopicAutoCreation?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class Producer {
|
|
15
|
+
private options: Required<ProducerOptions>;
|
|
16
|
+
private metadata: Metadata;
|
|
17
|
+
private producerId = 0n;
|
|
18
|
+
private producerEpoch = 0;
|
|
19
|
+
private sequences: Record<string, Record<number, number>> = {};
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private cluster: Cluster,
|
|
23
|
+
options: ProducerOptions,
|
|
24
|
+
) {
|
|
25
|
+
this.options = {
|
|
26
|
+
...options,
|
|
27
|
+
allowTopicAutoCreation: options.allowTopicAutoCreation ?? false,
|
|
28
|
+
};
|
|
29
|
+
this.metadata = new Metadata({ cluster });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async send(messages: Message[]) {
|
|
33
|
+
await this.ensureConnected();
|
|
34
|
+
|
|
35
|
+
const { allowTopicAutoCreation } = this.options;
|
|
36
|
+
const defaultTimestamp = BigInt(Date.now());
|
|
37
|
+
|
|
38
|
+
const topics = Array.from(new Set(messages.map((message) => message.topic)));
|
|
39
|
+
await this.metadata.fetchMetadataIfNecessary({ topics, allowTopicAutoCreation });
|
|
40
|
+
|
|
41
|
+
const nodeTopicPartitionMessages = distributeMessagesToTopicPartitionLeaders(
|
|
42
|
+
messages,
|
|
43
|
+
this.metadata.getTopicPartitionLeaderIds(),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
await Promise.all(
|
|
47
|
+
Object.entries(nodeTopicPartitionMessages).map(async ([nodeId, topicPartitionMessages]) => {
|
|
48
|
+
await this.cluster.sendRequestToNode(parseInt(nodeId))(API.PRODUCE, {
|
|
49
|
+
transactionalId: null,
|
|
50
|
+
acks: 1,
|
|
51
|
+
timeoutMs: 5000,
|
|
52
|
+
topicData: Object.entries(topicPartitionMessages).map(([topic, partitionMessages]) => ({
|
|
53
|
+
name: topic,
|
|
54
|
+
partitionData: Object.entries(partitionMessages).map(([partition, messages]) => {
|
|
55
|
+
let baseTimestamp: bigint | undefined;
|
|
56
|
+
let maxTimestamp: bigint | undefined;
|
|
57
|
+
|
|
58
|
+
messages.forEach(({ timestamp = defaultTimestamp }) => {
|
|
59
|
+
if (!baseTimestamp || timestamp < baseTimestamp) {
|
|
60
|
+
baseTimestamp = timestamp;
|
|
61
|
+
}
|
|
62
|
+
if (!maxTimestamp || timestamp > maxTimestamp) {
|
|
63
|
+
maxTimestamp = timestamp;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const baseSequence = this.nextSequence(topic, parseInt(partition), messages.length);
|
|
68
|
+
return {
|
|
69
|
+
index: parseInt(partition),
|
|
70
|
+
baseOffset: 0n,
|
|
71
|
+
partitionLeaderEpoch: -1,
|
|
72
|
+
attributes: 0,
|
|
73
|
+
lastOffsetDelta: messages.length - 1,
|
|
74
|
+
baseTimestamp: baseTimestamp ?? 0n,
|
|
75
|
+
maxTimestamp: maxTimestamp ?? 0n,
|
|
76
|
+
producerId: this.producerId,
|
|
77
|
+
producerEpoch: 0,
|
|
78
|
+
baseSequence,
|
|
79
|
+
records: messages.map((message, index) => ({
|
|
80
|
+
attributes: 0,
|
|
81
|
+
timestampDelta: (message.timestamp ?? defaultTimestamp) - (baseTimestamp ?? 0n),
|
|
82
|
+
offsetDelta: index,
|
|
83
|
+
key: message.key,
|
|
84
|
+
value: message.value,
|
|
85
|
+
headers: Object.entries(message.headers ?? {}).map(([key, value]) => ({
|
|
86
|
+
key,
|
|
87
|
+
value,
|
|
88
|
+
})),
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
}),
|
|
92
|
+
})),
|
|
93
|
+
});
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public async close() {
|
|
99
|
+
await this.cluster.disconnect();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private ensureConnected = memo(async () => {
|
|
103
|
+
await this.cluster.connect();
|
|
104
|
+
await this.initProducerId();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
private async initProducerId(): Promise<void> {
|
|
108
|
+
try {
|
|
109
|
+
const result = await this.cluster.sendRequest(API.INIT_PRODUCER_ID, {
|
|
110
|
+
transactionalId: null,
|
|
111
|
+
transactionTimeoutMs: 0,
|
|
112
|
+
producerId: this.producerId,
|
|
113
|
+
producerEpoch: this.producerEpoch,
|
|
114
|
+
});
|
|
115
|
+
this.producerId = result.producerId;
|
|
116
|
+
this.producerEpoch = result.producerEpoch;
|
|
117
|
+
this.sequences = {};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if ((error as KafkaTSApiError).errorCode === API_ERROR.COORDINATOR_LOAD_IN_PROGRESS) {
|
|
120
|
+
await delay(100);
|
|
121
|
+
return this.initProducerId();
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private nextSequence(topic: string, partition: number, messagesCount: number) {
|
|
128
|
+
this.sequences[topic] ??= {};
|
|
129
|
+
this.sequences[topic][partition] ??= 0;
|
|
130
|
+
return (this.sequences[topic][partition] += messagesCount || 1);
|
|
131
|
+
}
|
|
132
|
+
}
|