kafka-crab-js 0.4.1 → 0.6.0
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/Cargo.toml +4 -4
- package/example/sample.mjs +6 -3
- package/example/stream_consumer.mjs +99 -0
- package/index.d.ts +69 -36
- package/index.js +65 -6
- package/package.json +17 -12
- package/src/kafka/consumer/consumer_helper.rs +179 -0
- package/src/kafka/consumer/consumer_thread.rs +6 -21
- package/src/kafka/consumer/kafka_consumer.rs +54 -146
- package/src/kafka/consumer/kafka_stream_consumer.rs +225 -0
- package/src/kafka/consumer/mod.rs +3 -1
- package/src/kafka/consumer/model.rs +60 -0
- package/src/kafka/kafka_admin.rs +10 -5
- package/src/kafka/kafka_client.rs +26 -3
- package/src/kafka/kafka_util.rs +39 -32
- package/src/kafka/mod.rs +1 -1
- package/src/kafka/model.rs +10 -54
- package/src/kafka/{kafka_producer.rs → producer/kafka_producer.rs} +24 -6
- package/src/kafka/producer/mod.rs +2 -0
- package/src/kafka/producer/model.rs +73 -0
- package/.yarnrc.yml +0 -3
- package/src/kafka/consumer/consumer_context.rs +0 -29
package/Cargo.toml
CHANGED
|
@@ -8,21 +8,20 @@ crate-type = ["cdylib"]
|
|
|
8
8
|
|
|
9
9
|
[dependencies]
|
|
10
10
|
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
|
|
11
|
-
napi = { version = "2", default-features = false, features = [
|
|
11
|
+
napi = { version = "2.16", default-features = false, features = [
|
|
12
12
|
"napi8",
|
|
13
13
|
"tokio_rt",
|
|
14
14
|
"serde-json",
|
|
15
15
|
] }
|
|
16
|
-
napi-derive = "2"
|
|
16
|
+
napi-derive = "2.16"
|
|
17
17
|
|
|
18
18
|
anyhow = { version = "1.0", features = ["backtrace"] }
|
|
19
19
|
|
|
20
|
-
rdkafka = { version = "0.
|
|
20
|
+
rdkafka = { version = "0.36", features = [
|
|
21
21
|
"libz-static",
|
|
22
22
|
"cmake-build",
|
|
23
23
|
"tokio",
|
|
24
24
|
"ssl-vendored",
|
|
25
|
-
"ssl",
|
|
26
25
|
] }
|
|
27
26
|
tokio = { version = "1", features = ["full"] }
|
|
28
27
|
|
|
@@ -34,3 +33,4 @@ napi-build = "2.0.1"
|
|
|
34
33
|
|
|
35
34
|
[profile.release]
|
|
36
35
|
lto = true
|
|
36
|
+
strip = "symbols"
|
package/example/sample.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/* eslint-disable no-await-in-loop */
|
|
2
2
|
|
|
3
|
-
import timersPromises from 'node:timers/promises';
|
|
3
|
+
// import timersPromises from 'node:timers/promises';
|
|
4
4
|
import { Buffer } from 'node:buffer';
|
|
5
5
|
import { nanoid } from 'nanoid';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
KafkaClient, CommitMode, PartitionPosition, ConsumerResult,
|
|
8
|
+
} from '../index.js';
|
|
7
9
|
|
|
8
10
|
const kafkaClient = new KafkaClient({
|
|
9
11
|
brokers: 'localhost:29092',
|
|
10
12
|
clientId: 'my-js-group',
|
|
11
13
|
logLevel: 'info',
|
|
14
|
+
brokerAddressFamily: 'v4',
|
|
12
15
|
});
|
|
13
16
|
const topic = `topic-${nanoid()}`;
|
|
14
17
|
|
|
@@ -31,7 +34,7 @@ async function produce() {
|
|
|
31
34
|
const result = await producer.send(
|
|
32
35
|
{
|
|
33
36
|
topic,
|
|
34
|
-
messages: [{
|
|
37
|
+
messages: [{ payload: Buffer.from(`{"_id":"${i}","name":"Elizeu Drummond Sample js","phone":"555"}`) }],
|
|
35
38
|
},
|
|
36
39
|
);
|
|
37
40
|
console.log('Js message sent. Offset:', result);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { CommitMode, KafkaClient, KafkaStreamConsumer, PartitionPosition } from "../index.js";
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { fakerPT_BR } from '@faker-js/faker';
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
|
|
6
|
+
const kafkaClient = new KafkaClient({
|
|
7
|
+
brokers: 'localhost:29092',
|
|
8
|
+
clientId: 'my-js-group-11',
|
|
9
|
+
logLevel: 'debug',
|
|
10
|
+
brokerAddressFamily: 'v4',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Produces some messages to the topic
|
|
15
|
+
* @param {string} topic
|
|
16
|
+
* @param {number} messages
|
|
17
|
+
*/
|
|
18
|
+
async function produce(topic, messages=1) {
|
|
19
|
+
const producer = kafkaClient.createProducer({ topic, configuration: { 'message.timeout.ms': '5000' } });
|
|
20
|
+
const index = Math.floor(Math.random() * 100_000)
|
|
21
|
+
for (let i = index ?? 0; i < index+messages; i++) {
|
|
22
|
+
try {
|
|
23
|
+
const result = await producer.send(
|
|
24
|
+
{
|
|
25
|
+
topic,
|
|
26
|
+
messages: [{ value: Buffer.from(`{"_id":"${i}","name":"${fakerPT_BR.person.fullName()}","phone":"${fakerPT_BR.phone.number()}"}`) }],
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
console.log('Js message sent. Offset:', result);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Js Error on send', error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class KafkaStreamReadable extends Readable {
|
|
37
|
+
/**
|
|
38
|
+
* Creates a new KafkaStream
|
|
39
|
+
* @param {KafkaStreamConsumer} kafkaStreamConsumer
|
|
40
|
+
*/
|
|
41
|
+
constructor(kafkaStreamConsumer) {
|
|
42
|
+
super({ objectMode: true });
|
|
43
|
+
this.kafkaStreamConsumer = kafkaStreamConsumer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async _read() {
|
|
47
|
+
let message = await this.kafkaStreamConsumer.recv();
|
|
48
|
+
this.push(message)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function startConsumer() {
|
|
53
|
+
const kafkaStreamConsumer = kafkaClient.createStreamConsumer({
|
|
54
|
+
groupId: `crab-test`,
|
|
55
|
+
enableAutoCommit: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// await streamerConsumer.subscribe(
|
|
59
|
+
// [{
|
|
60
|
+
// topic: 'foo',
|
|
61
|
+
// partitionOffset: [
|
|
62
|
+
// { partition:0, offset: { position: PartitionPosition.Beginning }}
|
|
63
|
+
// ]
|
|
64
|
+
// },
|
|
65
|
+
// {
|
|
66
|
+
// topic: 'bar',
|
|
67
|
+
// partitionOffset: [
|
|
68
|
+
// { partition:0, offset: { position: PartitionPosition.Stored }},
|
|
69
|
+
// { partition:1, offset: { position: PartitionPosition.Beginning }}
|
|
70
|
+
// ]
|
|
71
|
+
// }]
|
|
72
|
+
// );
|
|
73
|
+
// await streamerConsumer.subscribe([ { topic: 'foo', allOffsets: { position: PartitionPosition.Stored } } ]);
|
|
74
|
+
// await streamerConsumer.subscribe('foo');
|
|
75
|
+
await kafkaStreamConsumer.subscribe([ { topic: 'foo' }, { topic: 'bar' } ]);
|
|
76
|
+
|
|
77
|
+
const maxMessages = 20;
|
|
78
|
+
let counter = 0;
|
|
79
|
+
const kafkaStream = new KafkaStreamReadable(kafkaStreamConsumer);
|
|
80
|
+
console.log('Starting consumer')
|
|
81
|
+
kafkaStream.on('data', (message) => {
|
|
82
|
+
counter++;
|
|
83
|
+
console.log('>>> Message received:', { payload: message.payload.toString(), offset: message.offset, partition: message.partition, topic: message.topic })
|
|
84
|
+
if (counter === maxMessages) {
|
|
85
|
+
kafkaStreamConsumer.unsubscribe();
|
|
86
|
+
kafkaStream.destroy();
|
|
87
|
+
}
|
|
88
|
+
// streamerConsumer.commit(message.partition, message.offset+1, CommitMode.Sync)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (process.argv[2] === 'send') {
|
|
93
|
+
const topic = process.argv[3]
|
|
94
|
+
const messages = process.argv[4] ? Number(process.argv[4]) : 1
|
|
95
|
+
console.log('Sending', messages, 'messages to', topic)
|
|
96
|
+
await produce(topic, messages);
|
|
97
|
+
} else {
|
|
98
|
+
await startConsumer();
|
|
99
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -3,10 +3,20 @@
|
|
|
3
3
|
|
|
4
4
|
/* auto-generated by NAPI-RS */
|
|
5
5
|
|
|
6
|
-
export const enum
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
export const enum ConsumerResult {
|
|
7
|
+
Ok = 'Ok',
|
|
8
|
+
Retry = 'Retry'
|
|
9
|
+
}
|
|
10
|
+
export interface KafkaConsumerConfiguration {
|
|
11
|
+
topic: string
|
|
12
|
+
groupId: string
|
|
13
|
+
retryStrategy?: RetryStrategy
|
|
14
|
+
offset?: OffsetModel
|
|
15
|
+
createTopic?: boolean
|
|
16
|
+
commitMode?: CommitMode
|
|
17
|
+
enableAutoCommit?: boolean
|
|
18
|
+
configuration?: Record<string, string>
|
|
19
|
+
fecthMetadataTimeout?: number
|
|
10
20
|
}
|
|
11
21
|
export interface RetryStrategy {
|
|
12
22
|
retries: number
|
|
@@ -16,19 +26,16 @@ export interface RetryStrategy {
|
|
|
16
26
|
offset?: OffsetModel
|
|
17
27
|
configuration?: Record<string, string>
|
|
18
28
|
}
|
|
19
|
-
export const enum
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
export const enum CommitMode {
|
|
30
|
+
Sync = 'Sync',
|
|
31
|
+
Async = 'Async'
|
|
22
32
|
}
|
|
23
33
|
export interface ConsumerConfiguration {
|
|
24
|
-
topic: string
|
|
25
34
|
groupId: string
|
|
26
|
-
retryStrategy?: RetryStrategy
|
|
27
|
-
offset?: OffsetModel
|
|
28
35
|
createTopic?: boolean
|
|
29
|
-
commitMode?: CommitMode
|
|
30
36
|
enableAutoCommit?: boolean
|
|
31
37
|
configuration?: Record<string, string>
|
|
38
|
+
fecthMetadataTimeout?: number
|
|
32
39
|
}
|
|
33
40
|
export const enum SecurityProtocol {
|
|
34
41
|
Plaintext = 'Plaintext',
|
|
@@ -42,15 +49,7 @@ export interface KafkaConfiguration {
|
|
|
42
49
|
securityProtocol?: SecurityProtocol
|
|
43
50
|
configuration?: Record<string, string>
|
|
44
51
|
logLevel?: string
|
|
45
|
-
|
|
46
|
-
export interface ProducerConfiguration {
|
|
47
|
-
queueTimeout?: number
|
|
48
|
-
thrownOnError?: boolean
|
|
49
|
-
configuration?: Record<string, string>
|
|
50
|
-
}
|
|
51
|
-
export interface ProducerRecord {
|
|
52
|
-
topic: string
|
|
53
|
-
messages: Array<MessageModel>
|
|
52
|
+
brokerAddressFamily?: string
|
|
54
53
|
}
|
|
55
54
|
export const enum AutoOffsetReset {
|
|
56
55
|
Smallest = 'Smallest',
|
|
@@ -61,26 +60,23 @@ export const enum AutoOffsetReset {
|
|
|
61
60
|
End = 'End',
|
|
62
61
|
Error = 'Error'
|
|
63
62
|
}
|
|
64
|
-
export interface Payload {
|
|
65
|
-
value: Buffer
|
|
66
|
-
key?: Buffer
|
|
67
|
-
headers?: Record<string, Buffer>
|
|
68
|
-
topic: string
|
|
69
|
-
partition: number
|
|
70
|
-
offset: number
|
|
71
|
-
}
|
|
72
63
|
export const enum PartitionPosition {
|
|
73
|
-
Beginning =
|
|
74
|
-
End =
|
|
75
|
-
Stored =
|
|
64
|
+
Beginning = 'Beginning',
|
|
65
|
+
End = 'End',
|
|
66
|
+
Stored = 'Stored'
|
|
76
67
|
}
|
|
77
68
|
export interface OffsetModel {
|
|
78
69
|
offset?: number
|
|
79
70
|
position?: PartitionPosition
|
|
80
71
|
}
|
|
81
|
-
export interface
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
export interface PartitionOffset {
|
|
73
|
+
partition: number
|
|
74
|
+
offset: OffsetModel
|
|
75
|
+
}
|
|
76
|
+
export interface TopicPartitionConfig {
|
|
77
|
+
topic: string
|
|
78
|
+
allOffsets?: OffsetModel
|
|
79
|
+
partitionOffset?: Array<PartitionOffset>
|
|
84
80
|
}
|
|
85
81
|
export interface RecordMetadata {
|
|
86
82
|
topic: string
|
|
@@ -88,19 +84,56 @@ export interface RecordMetadata {
|
|
|
88
84
|
offset: number
|
|
89
85
|
error?: KafkaCrabError
|
|
90
86
|
}
|
|
91
|
-
export interface
|
|
87
|
+
export interface MessageProducer {
|
|
88
|
+
value: Buffer
|
|
89
|
+
key?: Buffer
|
|
90
|
+
headers?: Record<string, Buffer>
|
|
91
|
+
}
|
|
92
|
+
export interface ProducerConfiguration {
|
|
93
|
+
queueTimeout?: number
|
|
94
|
+
thrownOnError?: boolean
|
|
95
|
+
configuration?: Record<string, string>
|
|
96
|
+
}
|
|
97
|
+
export interface ProducerRecord {
|
|
98
|
+
topic: string
|
|
99
|
+
messages: Array<MessageProducer>
|
|
100
|
+
}
|
|
101
|
+
export interface Payload {
|
|
92
102
|
value: Buffer
|
|
93
103
|
key?: Buffer
|
|
94
104
|
headers?: Record<string, Buffer>
|
|
105
|
+
topic: string
|
|
106
|
+
partition: number
|
|
107
|
+
offset: number
|
|
108
|
+
}
|
|
109
|
+
export interface Message {
|
|
110
|
+
payload: Buffer
|
|
111
|
+
key?: Buffer
|
|
112
|
+
headers?: Record<string, Buffer>
|
|
113
|
+
topic: string
|
|
114
|
+
partition: number
|
|
115
|
+
offset: number
|
|
116
|
+
}
|
|
117
|
+
export interface KafkaCrabError {
|
|
118
|
+
code: number
|
|
119
|
+
message: string
|
|
95
120
|
}
|
|
96
121
|
export class KafkaConsumer {
|
|
97
122
|
startConsumer(callback: (error: Error | undefined, result: Payload) => Promise<ConsumerResult | undefined>): Promise<void>
|
|
98
123
|
}
|
|
124
|
+
export class KafkaStreamConsumer {
|
|
125
|
+
subscribe(topicConfigs: string | Array<TopicPartitionConfig>): Promise<void>
|
|
126
|
+
unsubscribe(): void
|
|
127
|
+
seek(topic: string, partition: number, offsetModel: OffsetModel, timeout?: number | undefined | null): void
|
|
128
|
+
recv(): Promise<Message>
|
|
129
|
+
commit(topic: string, partition: number, offset: number, commit: CommitMode): void
|
|
130
|
+
}
|
|
99
131
|
export class KafkaClient {
|
|
100
132
|
readonly kafkaConfiguration: KafkaConfiguration
|
|
101
133
|
constructor(kafkaConfiguration: KafkaConfiguration)
|
|
102
134
|
createProducer(producerConfiguration: ProducerConfiguration): KafkaProducer
|
|
103
|
-
createConsumer(consumerConfiguration:
|
|
135
|
+
createConsumer(consumerConfiguration: KafkaConsumerConfiguration): KafkaConsumer
|
|
136
|
+
createStreamConsumer(consumerConfiguration: ConsumerConfiguration): KafkaStreamConsumer
|
|
104
137
|
}
|
|
105
138
|
export class KafkaProducer {
|
|
106
139
|
send(producerRecord: ProducerRecord): Promise<Array<RecordMetadata>>
|
package/index.js
CHANGED
|
@@ -224,14 +224,72 @@ switch (platform) {
|
|
|
224
224
|
}
|
|
225
225
|
break
|
|
226
226
|
case 'arm':
|
|
227
|
+
if (isMusl()) {
|
|
228
|
+
localFileExisted = existsSync(
|
|
229
|
+
join(__dirname, 'kafka-crab-js.linux-arm-musleabihf.node')
|
|
230
|
+
)
|
|
231
|
+
try {
|
|
232
|
+
if (localFileExisted) {
|
|
233
|
+
nativeBinding = require('./kafka-crab-js.linux-arm-musleabihf.node')
|
|
234
|
+
} else {
|
|
235
|
+
nativeBinding = require('kafka-crab-js-linux-arm-musleabihf')
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
loadError = e
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
localFileExisted = existsSync(
|
|
242
|
+
join(__dirname, 'kafka-crab-js.linux-arm-gnueabihf.node')
|
|
243
|
+
)
|
|
244
|
+
try {
|
|
245
|
+
if (localFileExisted) {
|
|
246
|
+
nativeBinding = require('./kafka-crab-js.linux-arm-gnueabihf.node')
|
|
247
|
+
} else {
|
|
248
|
+
nativeBinding = require('kafka-crab-js-linux-arm-gnueabihf')
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
loadError = e
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
break
|
|
255
|
+
case 'riscv64':
|
|
256
|
+
if (isMusl()) {
|
|
257
|
+
localFileExisted = existsSync(
|
|
258
|
+
join(__dirname, 'kafka-crab-js.linux-riscv64-musl.node')
|
|
259
|
+
)
|
|
260
|
+
try {
|
|
261
|
+
if (localFileExisted) {
|
|
262
|
+
nativeBinding = require('./kafka-crab-js.linux-riscv64-musl.node')
|
|
263
|
+
} else {
|
|
264
|
+
nativeBinding = require('kafka-crab-js-linux-riscv64-musl')
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
loadError = e
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
localFileExisted = existsSync(
|
|
271
|
+
join(__dirname, 'kafka-crab-js.linux-riscv64-gnu.node')
|
|
272
|
+
)
|
|
273
|
+
try {
|
|
274
|
+
if (localFileExisted) {
|
|
275
|
+
nativeBinding = require('./kafka-crab-js.linux-riscv64-gnu.node')
|
|
276
|
+
} else {
|
|
277
|
+
nativeBinding = require('kafka-crab-js-linux-riscv64-gnu')
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
loadError = e
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break
|
|
284
|
+
case 's390x':
|
|
227
285
|
localFileExisted = existsSync(
|
|
228
|
-
join(__dirname, 'kafka-crab-js.linux-
|
|
286
|
+
join(__dirname, 'kafka-crab-js.linux-s390x-gnu.node')
|
|
229
287
|
)
|
|
230
288
|
try {
|
|
231
289
|
if (localFileExisted) {
|
|
232
|
-
nativeBinding = require('./kafka-crab-js.linux-
|
|
290
|
+
nativeBinding = require('./kafka-crab-js.linux-s390x-gnu.node')
|
|
233
291
|
} else {
|
|
234
|
-
nativeBinding = require('kafka-crab-js-linux-
|
|
292
|
+
nativeBinding = require('kafka-crab-js-linux-s390x-gnu')
|
|
235
293
|
}
|
|
236
294
|
} catch (e) {
|
|
237
295
|
loadError = e
|
|
@@ -252,13 +310,14 @@ if (!nativeBinding) {
|
|
|
252
310
|
throw new Error(`Failed to load native binding`)
|
|
253
311
|
}
|
|
254
312
|
|
|
255
|
-
const {
|
|
313
|
+
const { ConsumerResult, KafkaConsumer, KafkaStreamConsumer, CommitMode, SecurityProtocol, KafkaClient, AutoOffsetReset, PartitionPosition, KafkaProducer } = nativeBinding
|
|
256
314
|
|
|
257
|
-
module.exports.CommitMode = CommitMode
|
|
258
315
|
module.exports.ConsumerResult = ConsumerResult
|
|
259
316
|
module.exports.KafkaConsumer = KafkaConsumer
|
|
317
|
+
module.exports.KafkaStreamConsumer = KafkaStreamConsumer
|
|
318
|
+
module.exports.CommitMode = CommitMode
|
|
260
319
|
module.exports.SecurityProtocol = SecurityProtocol
|
|
261
320
|
module.exports.KafkaClient = KafkaClient
|
|
262
|
-
module.exports.KafkaProducer = KafkaProducer
|
|
263
321
|
module.exports.AutoOffsetReset = AutoOffsetReset
|
|
264
322
|
module.exports.PartitionPosition = PartitionPosition
|
|
323
|
+
module.exports.KafkaProducer = KafkaProducer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kafka-crab-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"types": "index.d.ts",
|
|
6
6
|
"napi": {
|
|
@@ -24,19 +24,24 @@
|
|
|
24
24
|
],
|
|
25
25
|
"author": "inaiat@gmail.com",
|
|
26
26
|
"homepage": "https://github.com/inaiat/kafka-crab-js.git",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/inaiat/kafka-crab-js.git"
|
|
30
|
+
},
|
|
27
31
|
"devDependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"nanoid": "^
|
|
33
|
-
"
|
|
32
|
+
"@faker-js/faker": "^8.4.1",
|
|
33
|
+
"@napi-rs/cli": "^2.18.3",
|
|
34
|
+
"@types/node": "^20.12.12",
|
|
35
|
+
"ava": "^6.1.2",
|
|
36
|
+
"nanoid": "^5.0.7",
|
|
37
|
+
"typescript": "^5.4.5",
|
|
38
|
+
"xo": "^0.58.0"
|
|
34
39
|
},
|
|
35
40
|
"ava": {
|
|
36
41
|
"timeout": "3m"
|
|
37
42
|
},
|
|
38
43
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
44
|
+
"node": ">= 18"
|
|
40
45
|
},
|
|
41
46
|
"xo": {
|
|
42
47
|
"space": true,
|
|
@@ -54,10 +59,10 @@
|
|
|
54
59
|
"universal": "napi universal",
|
|
55
60
|
"version": "napi version"
|
|
56
61
|
},
|
|
57
|
-
"packageManager": "
|
|
62
|
+
"packageManager": "pnpm@9.1.1",
|
|
58
63
|
"optionalDependencies": {
|
|
59
|
-
"kafka-crab-js-darwin-arm64": "0.
|
|
60
|
-
"kafka-crab-js-darwin-x64": "0.
|
|
61
|
-
"kafka-crab-js-linux-x64-gnu": "0.
|
|
64
|
+
"kafka-crab-js-darwin-arm64": "0.6.0",
|
|
65
|
+
"kafka-crab-js-darwin-x64": "0.6.0",
|
|
66
|
+
"kafka-crab-js-linux-x64-gnu": "0.6.0"
|
|
62
67
|
}
|
|
63
68
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
use std::{collections::HashMap, time::Duration};
|
|
2
|
+
|
|
3
|
+
use rdkafka::{
|
|
4
|
+
config::RDKafkaLogLevel,
|
|
5
|
+
consumer::{Consumer, StreamConsumer},
|
|
6
|
+
ClientConfig, TopicPartitionList,
|
|
7
|
+
};
|
|
8
|
+
use tracing::{debug, error, info, warn};
|
|
9
|
+
|
|
10
|
+
use crate::kafka::{
|
|
11
|
+
consumer::model::LoggingConsumer,
|
|
12
|
+
kafka_admin::KafkaAdmin,
|
|
13
|
+
kafka_util::convert_to_rdkafka_offset,
|
|
14
|
+
model::{OffsetModel, PartitionOffset},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
use super::{model::ConsumerConfiguration, model::CustomContext};
|
|
18
|
+
|
|
19
|
+
pub async fn create_stream_consumer_and_setup_everything(
|
|
20
|
+
client_config: &ClientConfig,
|
|
21
|
+
consumer_configuration: &ConsumerConfiguration,
|
|
22
|
+
topic: &str,
|
|
23
|
+
offset: &Option<OffsetModel>,
|
|
24
|
+
configuration: Option<HashMap<String, String>>,
|
|
25
|
+
timeout: Duration,
|
|
26
|
+
) -> anyhow::Result<StreamConsumer<CustomContext>> {
|
|
27
|
+
let consumer = create_stream_consumer(client_config, consumer_configuration, configuration)?;
|
|
28
|
+
|
|
29
|
+
if consumer_configuration.create_topic.unwrap_or(true) {
|
|
30
|
+
try_create_topic(topic, client_config).await?;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if let Some(offset_model) = offset {
|
|
34
|
+
set_offset_of_all_partitions(offset_model, &consumer, topic, timeout)?;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try_subscribe(&consumer, topic)?;
|
|
38
|
+
|
|
39
|
+
Ok(consumer)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn create_stream_consumer(
|
|
43
|
+
client_config: &ClientConfig,
|
|
44
|
+
consumer_configuration: &ConsumerConfiguration,
|
|
45
|
+
configuration: Option<HashMap<String, String>>,
|
|
46
|
+
) -> anyhow::Result<StreamConsumer<CustomContext>> {
|
|
47
|
+
let context = CustomContext;
|
|
48
|
+
|
|
49
|
+
let ConsumerConfiguration {
|
|
50
|
+
group_id,
|
|
51
|
+
enable_auto_commit,
|
|
52
|
+
..
|
|
53
|
+
} = consumer_configuration.clone();
|
|
54
|
+
|
|
55
|
+
let mut consumer_config: ClientConfig = client_config.clone();
|
|
56
|
+
|
|
57
|
+
if let Some(config) = configuration {
|
|
58
|
+
consumer_config.extend(config);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
debug!(
|
|
62
|
+
"Creating consumer with configuration: {:?}",
|
|
63
|
+
consumer_config
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
let consumer: LoggingConsumer = consumer_config
|
|
67
|
+
.clone()
|
|
68
|
+
.set("group.id", group_id.clone())
|
|
69
|
+
.set(
|
|
70
|
+
"enable.auto.commit",
|
|
71
|
+
enable_auto_commit.unwrap_or(true).to_string(),
|
|
72
|
+
)
|
|
73
|
+
.set_log_level(RDKafkaLogLevel::Debug)
|
|
74
|
+
.create_with_context(context)?;
|
|
75
|
+
|
|
76
|
+
info!("Consumer created. Group id: {:?}", group_id);
|
|
77
|
+
Ok(consumer)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub fn try_subscribe(consumer: &LoggingConsumer, topic: &str) -> anyhow::Result<()> {
|
|
81
|
+
consumer
|
|
82
|
+
.subscribe(vec![&*topic.to_string()].as_slice())
|
|
83
|
+
.map_err(|e| {
|
|
84
|
+
anyhow::Error::msg(format!(
|
|
85
|
+
"Can't subscribe to specified topics. Error: {:?}",
|
|
86
|
+
e
|
|
87
|
+
))
|
|
88
|
+
})?;
|
|
89
|
+
debug!("Subscribed to topic: {:?}", topic);
|
|
90
|
+
Ok(())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pub async fn try_create_topic(topic: &str, client_config: &ClientConfig) -> anyhow::Result<()> {
|
|
94
|
+
info!("Creating topic: {:?}", topic);
|
|
95
|
+
let admin = KafkaAdmin::new(client_config, None);
|
|
96
|
+
let result = admin.create_topic(topic).await;
|
|
97
|
+
if let Err(e) = result {
|
|
98
|
+
warn!("Fail to create topic {:?}", e);
|
|
99
|
+
return Err(anyhow::Error::msg(format!("Fail to create topic: {:?}", e)));
|
|
100
|
+
}
|
|
101
|
+
info!("Topic created: {:?}", topic);
|
|
102
|
+
Ok(())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pub fn set_offset_of_all_partitions(
|
|
106
|
+
offset_model: &OffsetModel,
|
|
107
|
+
consumer: &StreamConsumer<CustomContext>,
|
|
108
|
+
topic: &str,
|
|
109
|
+
timeout: Duration,
|
|
110
|
+
) -> anyhow::Result<()> {
|
|
111
|
+
let offset = convert_to_rdkafka_offset(offset_model);
|
|
112
|
+
debug!("Setting offset to: {:?}", offset);
|
|
113
|
+
let metadata = consumer.fetch_metadata(Some(topic), timeout)?;
|
|
114
|
+
|
|
115
|
+
metadata.topics().iter().for_each(|meta_topic| {
|
|
116
|
+
let mut tpl = TopicPartitionList::new();
|
|
117
|
+
meta_topic.partitions().iter().for_each(|meta_partition| {
|
|
118
|
+
info!("Adding partition: {:?}", meta_partition.id());
|
|
119
|
+
tpl.add_partition(topic, meta_partition.id());
|
|
120
|
+
});
|
|
121
|
+
match tpl.set_all_offsets(offset) {
|
|
122
|
+
Ok(_) => {
|
|
123
|
+
debug!("Offset set to: {:?}", offset);
|
|
124
|
+
}
|
|
125
|
+
Err(e) => {
|
|
126
|
+
error!("Fail to set offset: {:?}", e)
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
match consumer.assign(&tpl) {
|
|
130
|
+
Ok(_) => {
|
|
131
|
+
debug!("Assigning topic: {:?}", topic);
|
|
132
|
+
}
|
|
133
|
+
Err(e) => {
|
|
134
|
+
error!("Fail to assign topic: {:?}", e);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
Ok(())
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pub fn assign_offset_or_use_metadata(
|
|
143
|
+
topic: &str,
|
|
144
|
+
partition_offset: Option<Vec<PartitionOffset>>,
|
|
145
|
+
offset_model: Option<&OffsetModel>,
|
|
146
|
+
consumer: &StreamConsumer<CustomContext>,
|
|
147
|
+
timeout: Duration,
|
|
148
|
+
) -> anyhow::Result<()> {
|
|
149
|
+
let mut tpl = TopicPartitionList::new();
|
|
150
|
+
|
|
151
|
+
if let Some(value) = partition_offset {
|
|
152
|
+
for item in value {
|
|
153
|
+
let offset = convert_to_rdkafka_offset(&item.offset);
|
|
154
|
+
debug!(
|
|
155
|
+
"Adding partition: {:?} with offset: {:?} for topic: {:?}",
|
|
156
|
+
item.partition, offset, topic
|
|
157
|
+
);
|
|
158
|
+
tpl.add_partition_offset(topic, item.partition, offset)?;
|
|
159
|
+
}
|
|
160
|
+
} else if let Some(offset_model) = offset_model {
|
|
161
|
+
let offset = convert_to_rdkafka_offset(offset_model);
|
|
162
|
+
let metadata = consumer.fetch_metadata(Some(topic), timeout)?;
|
|
163
|
+
for meta_topic in metadata.topics() {
|
|
164
|
+
for meta_partition in meta_topic.partitions() {
|
|
165
|
+
info!(
|
|
166
|
+
"Adding partition: {:?} with offset: {:?} for topic: {:?}",
|
|
167
|
+
meta_partition.id(),
|
|
168
|
+
offset,
|
|
169
|
+
topic
|
|
170
|
+
);
|
|
171
|
+
tpl.add_partition_offset(topic, meta_partition.id(), offset)?;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
anyhow::bail!("At least one of partition_offset or offset_model should be provided");
|
|
176
|
+
}
|
|
177
|
+
consumer.assign(&tpl)?;
|
|
178
|
+
Ok(())
|
|
179
|
+
}
|
|
@@ -12,15 +12,14 @@ use tracing::{debug, error, info, warn};
|
|
|
12
12
|
|
|
13
13
|
use crate::kafka::{
|
|
14
14
|
consumer::kafka_consumer::RETRY_COUNTER_NAME,
|
|
15
|
-
kafka_util::{
|
|
16
|
-
|
|
17
|
-
},
|
|
18
|
-
model::Payload,
|
|
15
|
+
kafka_util::{create_payload, kakfa_headers_to_hashmap, ExtractValueOnKafkaHashMap},
|
|
16
|
+
producer::model::Payload,
|
|
19
17
|
};
|
|
20
18
|
|
|
21
|
-
use super::
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
use super::{
|
|
20
|
+
kafka_consumer::{ConsumerResult, ProducerHelper},
|
|
21
|
+
model::CustomContext,
|
|
22
|
+
};
|
|
24
23
|
|
|
25
24
|
pub struct ConsumerThread {
|
|
26
25
|
pub stream_consumer: StreamConsumer<CustomContext>,
|
|
@@ -180,17 +179,3 @@ impl ConsumerThread {
|
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
181
|
}
|
|
183
|
-
|
|
184
|
-
fn create_payload(message: &BorrowedMessage<'_>, payload: &[u8]) -> Payload {
|
|
185
|
-
let key: Option<Buffer> = message.key().map(|bytes| bytes.into());
|
|
186
|
-
let headers = Some(kakfa_headers_to_hashmap_buffer(message.headers()));
|
|
187
|
-
let payload_js = Payload::new(
|
|
188
|
-
payload.into(),
|
|
189
|
-
key,
|
|
190
|
-
headers,
|
|
191
|
-
message.topic().to_owned(),
|
|
192
|
-
message.partition(),
|
|
193
|
-
message.offset(),
|
|
194
|
-
);
|
|
195
|
-
payload_js
|
|
196
|
-
}
|