kafka-ts 0.0.3 → 0.0.5
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/README.md +36 -1
- package/dist/client.d.ts +1 -2
- package/dist/consumer/consumer.d.ts +2 -0
- package/dist/consumer/consumer.js +18 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/retrier.d.ts +3 -4
- package/dist/utils/retrier.js +19 -14
- package/package.json +1 -1
- package/.prettierrc +0 -8
- package/src/__snapshots__/cluster.test.ts.snap +0 -1281
- package/src/api/api-versions.ts +0 -21
- package/src/api/create-topics.ts +0 -78
- package/src/api/delete-topics.ts +0 -42
- package/src/api/fetch.ts +0 -198
- package/src/api/find-coordinator.ts +0 -39
- package/src/api/heartbeat.ts +0 -33
- package/src/api/index.ts +0 -166
- package/src/api/init-producer-id.ts +0 -35
- package/src/api/join-group.ts +0 -67
- package/src/api/leave-group.ts +0 -48
- package/src/api/list-offsets.ts +0 -65
- package/src/api/metadata.ts +0 -66
- package/src/api/offset-commit.ts +0 -67
- package/src/api/offset-fetch.ts +0 -70
- package/src/api/produce.ts +0 -170
- package/src/api/sasl-authenticate.ts +0 -21
- package/src/api/sasl-handshake.ts +0 -16
- package/src/api/sync-group.ts +0 -54
- package/src/auth/index.ts +0 -2
- package/src/auth/plain.ts +0 -10
- package/src/auth/scram.ts +0 -52
- package/src/broker.ts +0 -72
- package/src/client.ts +0 -47
- package/src/cluster.test.ts +0 -371
- package/src/cluster.ts +0 -85
- package/src/codecs/gzip.ts +0 -9
- package/src/codecs/index.ts +0 -16
- package/src/codecs/none.ts +0 -6
- package/src/codecs/types.ts +0 -4
- package/src/connection.ts +0 -157
- package/src/consumer/consumer-group.ts +0 -229
- package/src/consumer/consumer-metadata.ts +0 -14
- package/src/consumer/consumer.ts +0 -252
- package/src/consumer/fetch-manager.ts +0 -169
- package/src/consumer/fetcher.ts +0 -64
- package/src/consumer/offset-manager.ts +0 -104
- package/src/consumer/processor.ts +0 -53
- package/src/distributors/assignments-to-replicas.test.ts +0 -43
- package/src/distributors/assignments-to-replicas.ts +0 -83
- package/src/distributors/messages-to-topic-partition-leaders.test.ts +0 -32
- package/src/distributors/messages-to-topic-partition-leaders.ts +0 -19
- package/src/distributors/partitioner.ts +0 -27
- package/src/index.ts +0 -9
- package/src/metadata.ts +0 -126
- package/src/producer/producer.ts +0 -142
- package/src/types.ts +0 -11
- package/src/utils/api.ts +0 -11
- package/src/utils/crypto.ts +0 -15
- package/src/utils/decoder.ts +0 -174
- package/src/utils/delay.ts +0 -1
- package/src/utils/encoder.ts +0 -148
- package/src/utils/error.ts +0 -21
- package/src/utils/logger.ts +0 -37
- package/src/utils/memo.ts +0 -12
- package/src/utils/murmur2.ts +0 -44
- package/src/utils/retrier.ts +0 -39
- package/src/utils/tracer.ts +0 -49
- package/tsconfig.json +0 -17
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { FetchResponse } from '../api/fetch';
|
|
2
|
-
import { Assignment } from '../api/sync-group';
|
|
3
|
-
import { Metadata } from '../metadata';
|
|
4
|
-
import { Batch, Message } from '../types';
|
|
5
|
-
import { KafkaTSError } from '../utils/error';
|
|
6
|
-
import { createTracer } from '../utils/tracer';
|
|
7
|
-
import { ConsumerGroup } from './consumer-group';
|
|
8
|
-
import { Fetcher } from './fetcher';
|
|
9
|
-
import { Processor } from './processor';
|
|
10
|
-
|
|
11
|
-
const trace = createTracer('FetchManager');
|
|
12
|
-
|
|
13
|
-
export type BatchGranularity = 'partition' | 'topic' | 'broker';
|
|
14
|
-
|
|
15
|
-
type FetchManagerOptions = {
|
|
16
|
-
fetch: (nodeId: number, assignment: Assignment) => Promise<FetchResponse>;
|
|
17
|
-
process: (batch: Batch) => Promise<void>;
|
|
18
|
-
metadata: Metadata;
|
|
19
|
-
consumerGroup?: ConsumerGroup;
|
|
20
|
-
nodeAssignments: { nodeId: number; assignment: Assignment }[];
|
|
21
|
-
batchGranularity: BatchGranularity;
|
|
22
|
-
concurrency: number;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type Checkpoint = { kind: 'checkpoint'; fetcherId: number };
|
|
26
|
-
type Entry = Batch | Checkpoint;
|
|
27
|
-
|
|
28
|
-
export class FetchManager {
|
|
29
|
-
private queue: Entry[] = [];
|
|
30
|
-
private isRunning = false;
|
|
31
|
-
private fetchers: Fetcher[];
|
|
32
|
-
private processors: Processor[];
|
|
33
|
-
private pollQueue: (() => void)[] = [];
|
|
34
|
-
private fetcherCallbacks: Record<number, () => void> = {};
|
|
35
|
-
|
|
36
|
-
constructor(private options: FetchManagerOptions) {
|
|
37
|
-
const { fetch, process, consumerGroup, nodeAssignments, concurrency } = this.options;
|
|
38
|
-
|
|
39
|
-
this.fetchers = nodeAssignments.map(
|
|
40
|
-
({ nodeId, assignment }, index) =>
|
|
41
|
-
new Fetcher(index, {
|
|
42
|
-
nodeId,
|
|
43
|
-
assignment,
|
|
44
|
-
consumerGroup,
|
|
45
|
-
fetch,
|
|
46
|
-
onResponse: this.onResponse.bind(this),
|
|
47
|
-
}),
|
|
48
|
-
);
|
|
49
|
-
this.processors = Array.from({ length: concurrency }).map(
|
|
50
|
-
() => new Processor({ process, poll: this.poll.bind(this) }),
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
@trace(() => ({ root: true }))
|
|
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
|
-
await this.stop();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public async stop() {
|
|
70
|
-
this.isRunning = false;
|
|
71
|
-
|
|
72
|
-
const stopPromise = Promise.all([
|
|
73
|
-
...this.fetchers.map((fetcher) => fetcher.stop()),
|
|
74
|
-
...this.processors.map((processor) => processor.stop()),
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
this.pollQueue.forEach((resolve) => resolve());
|
|
78
|
-
this.pollQueue = [];
|
|
79
|
-
|
|
80
|
-
Object.values(this.fetcherCallbacks).forEach((callback) => callback());
|
|
81
|
-
this.fetcherCallbacks = {};
|
|
82
|
-
|
|
83
|
-
await stopPromise;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
@trace()
|
|
87
|
-
public async poll(): Promise<Batch> {
|
|
88
|
-
if (!this.isRunning) {
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const batch = this.queue.shift();
|
|
93
|
-
if (!batch) {
|
|
94
|
-
// wait until new data is available or fetch manager is requested to stop
|
|
95
|
-
await new Promise<void>((resolve) => {
|
|
96
|
-
this.pollQueue.push(resolve);
|
|
97
|
-
});
|
|
98
|
-
return this.poll();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if ('kind' in batch && batch.kind === 'checkpoint') {
|
|
102
|
-
this.fetcherCallbacks[batch.fetcherId]?.();
|
|
103
|
-
return this.poll();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this.pollQueue?.shift()?.();
|
|
107
|
-
|
|
108
|
-
return batch as Exclude<Entry, Checkpoint>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
@trace()
|
|
112
|
-
private async onResponse(fetcherId: number, response: FetchResponse) {
|
|
113
|
-
const { metadata, batchGranularity } = this.options;
|
|
114
|
-
|
|
115
|
-
const batches = fetchResponseToBatches(response, batchGranularity, metadata);
|
|
116
|
-
if (!batches.length) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// wait until all broker batches have been processed or fetch manager is requested to stop
|
|
121
|
-
await new Promise<void>((resolve) => {
|
|
122
|
-
this.fetcherCallbacks[fetcherId] = resolve;
|
|
123
|
-
this.queue.push(...batches, { kind: 'checkpoint', fetcherId });
|
|
124
|
-
this.pollQueue?.shift()?.();
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const fetchResponseToBatches = (
|
|
130
|
-
batch: FetchResponse,
|
|
131
|
-
batchGranularity: BatchGranularity,
|
|
132
|
-
metadata: Metadata,
|
|
133
|
-
): Batch[] => {
|
|
134
|
-
const brokerTopics = batch.responses.map(({ topicId, partitions }) =>
|
|
135
|
-
partitions.map(({ partitionIndex, records }) =>
|
|
136
|
-
records.flatMap(({ baseTimestamp, baseOffset, records }) =>
|
|
137
|
-
records.map(
|
|
138
|
-
(message): Required<Message> => ({
|
|
139
|
-
topic: metadata.getTopicNameById(topicId),
|
|
140
|
-
partition: partitionIndex,
|
|
141
|
-
key: message.key ?? null,
|
|
142
|
-
value: message.value ?? null,
|
|
143
|
-
headers: Object.fromEntries(message.headers.map(({ key, value }) => [key, value])),
|
|
144
|
-
timestamp: baseTimestamp + BigInt(message.timestampDelta),
|
|
145
|
-
offset: baseOffset + BigInt(message.offsetDelta),
|
|
146
|
-
}),
|
|
147
|
-
),
|
|
148
|
-
),
|
|
149
|
-
),
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
switch (batchGranularity) {
|
|
153
|
-
case 'broker':
|
|
154
|
-
const messages = brokerTopics.flatMap((topicPartition) =>
|
|
155
|
-
topicPartition.flatMap((partitionMessages) => partitionMessages),
|
|
156
|
-
);
|
|
157
|
-
return messages.length ? [messages] : [];
|
|
158
|
-
case 'topic':
|
|
159
|
-
return brokerTopics
|
|
160
|
-
.map((topicPartition) => topicPartition.flatMap((partitionMessages) => partitionMessages))
|
|
161
|
-
.filter((messages) => messages.length);
|
|
162
|
-
case 'partition':
|
|
163
|
-
return brokerTopics
|
|
164
|
-
.flatMap((topicPartition) => topicPartition.map((partitionMessages) => partitionMessages))
|
|
165
|
-
.filter((messages) => messages.length);
|
|
166
|
-
default:
|
|
167
|
-
throw new KafkaTSError(`Unhandled batch granularity: ${batchGranularity}`);
|
|
168
|
-
}
|
|
169
|
-
};
|
package/src/consumer/fetcher.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'stream';
|
|
2
|
-
import { FetchResponse } from '../api/fetch';
|
|
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<FetchResponse>;
|
|
14
|
-
onResponse: (fetcherId: number, response: FetchResponse) => Promise<void>;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export class Fetcher extends EventEmitter<{ stopped: [] }> {
|
|
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
|
-
this.isRunning = true;
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
while (this.isRunning) {
|
|
32
|
-
await this.step();
|
|
33
|
-
}
|
|
34
|
-
} finally {
|
|
35
|
-
this.isRunning = false;
|
|
36
|
-
this.emit('stopped');
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
@trace()
|
|
41
|
-
private async step() {
|
|
42
|
-
const { nodeId, assignment, consumerGroup, fetch, onResponse } = this.options;
|
|
43
|
-
|
|
44
|
-
const response = await fetch(nodeId, assignment);
|
|
45
|
-
if (!this.isRunning) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
consumerGroup?.handleLastHeartbeat();
|
|
49
|
-
await onResponse(this.fetcherId, response);
|
|
50
|
-
consumerGroup?.handleLastHeartbeat();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public async stop() {
|
|
54
|
-
if (!this.isRunning) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const stopPromise = new Promise<void>((resolve) => {
|
|
59
|
-
this.once('stopped', resolve);
|
|
60
|
-
});
|
|
61
|
-
this.isRunning = false;
|
|
62
|
-
return stopPromise;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
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 { createTracer } from '../utils/tracer';
|
|
7
|
-
import { ConsumerMetadata } from './consumer-metadata';
|
|
8
|
-
|
|
9
|
-
const trace = createTracer('OffsetManager');
|
|
10
|
-
|
|
11
|
-
type OffsetManagerOptions = {
|
|
12
|
-
cluster: Cluster;
|
|
13
|
-
metadata: ConsumerMetadata;
|
|
14
|
-
isolationLevel: IsolationLevel;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export class OffsetManager {
|
|
18
|
-
private currentOffsets: Record<string, Record<number, bigint>> = {};
|
|
19
|
-
public pendingOffsets: Record<string, Record<number, bigint>> = {};
|
|
20
|
-
|
|
21
|
-
constructor(private options: OffsetManagerOptions) {}
|
|
22
|
-
|
|
23
|
-
public getCurrentOffset(topic: string, partition: number) {
|
|
24
|
-
return this.currentOffsets[topic]?.[partition] ?? 0n;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public resolve(topic: string, partition: number, offset: bigint) {
|
|
28
|
-
this.pendingOffsets[topic] ??= {};
|
|
29
|
-
this.pendingOffsets[topic][partition] = offset;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public flush(topicPartitions: Record<string, Set<number>>) {
|
|
33
|
-
Object.entries(topicPartitions).forEach(([topic, partitions]) => {
|
|
34
|
-
this.currentOffsets[topic] ??= {};
|
|
35
|
-
partitions.forEach((partition) => {
|
|
36
|
-
if (this.pendingOffsets[topic]?.[partition]) {
|
|
37
|
-
this.currentOffsets[topic][partition] = this.pendingOffsets[topic][partition];
|
|
38
|
-
delete this.pendingOffsets[topic][partition];
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
public async fetchOffsets(options: { fromBeginning: boolean }) {
|
|
45
|
-
const { metadata } = this.options;
|
|
46
|
-
|
|
47
|
-
const topicPartitions = Object.entries(metadata.getAssignment()).flatMap(([topic, partitions]) =>
|
|
48
|
-
partitions.map((partition) => ({ topic, partition })),
|
|
49
|
-
);
|
|
50
|
-
const nodeTopicPartitions = distributeMessagesToTopicPartitionLeaders(
|
|
51
|
-
topicPartitions,
|
|
52
|
-
metadata.getTopicPartitionLeaderIds(),
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
await Promise.all(
|
|
56
|
-
Object.entries(nodeTopicPartitions).map(([nodeId, topicPartitions]) =>
|
|
57
|
-
this.listOffsets({
|
|
58
|
-
...options,
|
|
59
|
-
nodeId: parseInt(nodeId),
|
|
60
|
-
nodeAssignment: Object.fromEntries(
|
|
61
|
-
Object.entries(topicPartitions).map(
|
|
62
|
-
([topicName, partitions]) =>
|
|
63
|
-
[topicName, Object.keys(partitions).map(Number)] as [string, number[]],
|
|
64
|
-
),
|
|
65
|
-
),
|
|
66
|
-
}),
|
|
67
|
-
),
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private async listOffsets({
|
|
72
|
-
nodeId,
|
|
73
|
-
nodeAssignment,
|
|
74
|
-
fromBeginning,
|
|
75
|
-
}: {
|
|
76
|
-
nodeId: number;
|
|
77
|
-
nodeAssignment: Assignment;
|
|
78
|
-
fromBeginning: boolean;
|
|
79
|
-
}) {
|
|
80
|
-
const { cluster, isolationLevel } = this.options;
|
|
81
|
-
|
|
82
|
-
const offsets = await cluster.sendRequestToNode(nodeId)(API.LIST_OFFSETS, {
|
|
83
|
-
replicaId: -1,
|
|
84
|
-
isolationLevel,
|
|
85
|
-
topics: Object.entries(nodeAssignment)
|
|
86
|
-
.flatMap(([topic, partitions]) => partitions.map((partition) => ({ topic, partition })))
|
|
87
|
-
.map(({ topic, partition }) => ({
|
|
88
|
-
name: topic,
|
|
89
|
-
partitions: [{ partitionIndex: partition, currentLeaderEpoch: -1, timestamp: -1n }],
|
|
90
|
-
})),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const topicPartitions: Record<string, Set<number>> = {};
|
|
94
|
-
offsets.topics.forEach(({ name, partitions }) => {
|
|
95
|
-
topicPartitions[name] ??= new Set();
|
|
96
|
-
partitions.forEach(({ partitionIndex, offset }) => {
|
|
97
|
-
topicPartitions[name].add(partitionIndex);
|
|
98
|
-
this.resolve(name, partitionIndex, fromBeginning ? 0n : offset);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
this.flush(topicPartitions);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'stream';
|
|
2
|
-
import { Batch } from '../types';
|
|
3
|
-
import { createTracer } from '../utils/tracer';
|
|
4
|
-
|
|
5
|
-
const trace = createTracer('Processor');
|
|
6
|
-
|
|
7
|
-
type ProcessorOptions = {
|
|
8
|
-
poll: () => Promise<Batch>;
|
|
9
|
-
process: (batch: Batch) => Promise<void>;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export class Processor extends EventEmitter<{ stopped: [] }> {
|
|
13
|
-
private isRunning = false;
|
|
14
|
-
|
|
15
|
-
constructor(private options: ProcessorOptions) {
|
|
16
|
-
super();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
public async loop() {
|
|
20
|
-
this.isRunning = true;
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
while (this.isRunning) {
|
|
24
|
-
await this.step();
|
|
25
|
-
}
|
|
26
|
-
} finally {
|
|
27
|
-
this.isRunning = false;
|
|
28
|
-
this.emit('stopped');
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
@trace()
|
|
33
|
-
private async step() {
|
|
34
|
-
const { poll, process } = this.options;
|
|
35
|
-
|
|
36
|
-
const batch = await poll();
|
|
37
|
-
if (batch.length) {
|
|
38
|
-
await process(batch);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
public async stop() {
|
|
43
|
-
if (!this.isRunning) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const stopPromise = new Promise<void>((resolve) => {
|
|
48
|
-
this.once('stopped', resolve);
|
|
49
|
-
});
|
|
50
|
-
this.isRunning = false;
|
|
51
|
-
return stopPromise;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
type Assignment = { [topicName: string]: number[] };
|
|
2
|
-
type TopicPartitionReplicaIds = { [topicName: string]: { [partition: number]: number[] } };
|
|
3
|
-
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
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Metadata } from '../metadata';
|
|
2
|
-
import { Message } from '../types';
|
|
3
|
-
import { murmur2, toPositive } from '../utils/murmur2';
|
|
4
|
-
|
|
5
|
-
export type Partition = (message: Message) => number;
|
|
6
|
-
export type Partitioner = (context: { metadata: Metadata }) => Partition;
|
|
7
|
-
|
|
8
|
-
export const defaultPartitioner: Partitioner = ({ metadata }) => {
|
|
9
|
-
const topicCounterMap: Record<string, number> = {};
|
|
10
|
-
|
|
11
|
-
const getNextValue = (topic: string) => {
|
|
12
|
-
topicCounterMap[topic] ??= 0;
|
|
13
|
-
return topicCounterMap[topic]++;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
return ({ topic, partition, key }: Message) => {
|
|
17
|
-
if (partition !== null && partition !== undefined) {
|
|
18
|
-
return partition;
|
|
19
|
-
}
|
|
20
|
-
const partitions = metadata.getTopicPartitions()[topic];
|
|
21
|
-
const numPartitions = partitions.length;
|
|
22
|
-
if (key) {
|
|
23
|
-
return toPositive(murmur2(key)) % numPartitions;
|
|
24
|
-
}
|
|
25
|
-
return toPositive(getNextValue(topic)) % numPartitions;
|
|
26
|
-
};
|
|
27
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export * from './api';
|
|
2
|
-
export * from './auth';
|
|
3
|
-
export { SASLProvider } from './broker';
|
|
4
|
-
export * from './client';
|
|
5
|
-
export * from './distributors/partitioner';
|
|
6
|
-
export * from './types';
|
|
7
|
-
export * from './utils/error';
|
|
8
|
-
export * from './utils/logger';
|
|
9
|
-
export { Tracer, setTracer } from './utils/tracer';
|