kafka-crab-js 0.3.1 → 0.5.4
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/.yarnrc.yml +5 -1
- package/Cargo.toml +4 -4
- package/README.md +0 -1
- package/example/sample.mjs +5 -2
- package/index.d.ts +20 -20
- package/index.js +46 -4
- package/package.json +13 -12
- package/src/kafka/consumer/consumer_context.rs +29 -0
- package/src/kafka/consumer/consumer_thread.rs +196 -0
- package/src/kafka/{kafka_consumer.rs → consumer/kafka_consumer.rs} +49 -226
- package/src/kafka/consumer/mod.rs +3 -0
- package/src/kafka/kafka_admin.rs +4 -9
- package/src/kafka/kafka_client.rs +7 -1
- package/src/kafka/mod.rs +1 -1
- package/src/kafka/model.rs +1 -3
package/.yarnrc.yml
CHANGED
package/Cargo.toml
CHANGED
|
@@ -8,20 +8,19 @@ 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
|
-
"ssl",
|
|
25
24
|
"ssl-vendored",
|
|
26
25
|
] }
|
|
27
26
|
tokio = { version = "1", features = ["full"] }
|
|
@@ -34,3 +33,4 @@ napi-build = "2.0.1"
|
|
|
34
33
|
|
|
35
34
|
[profile.release]
|
|
36
35
|
lto = true
|
|
36
|
+
strip = "symbols"
|
package/README.md
CHANGED
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
|
|
package/index.d.ts
CHANGED
|
@@ -3,19 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
/* auto-generated by NAPI-RS */
|
|
5
5
|
|
|
6
|
-
export const enum SecurityProtocol {
|
|
7
|
-
Plaintext = 'Plaintext',
|
|
8
|
-
Ssl = 'Ssl',
|
|
9
|
-
SaslPlaintext = 'SaslPlaintext',
|
|
10
|
-
SaslSsl = 'SaslSsl'
|
|
11
|
-
}
|
|
12
|
-
export interface KafkaConfiguration {
|
|
13
|
-
brokers: string
|
|
14
|
-
clientId: string
|
|
15
|
-
securityProtocol?: SecurityProtocol
|
|
16
|
-
configuration?: Record<string, string>
|
|
17
|
-
logLevel?: string
|
|
18
|
-
}
|
|
19
6
|
export const enum CommitMode {
|
|
20
7
|
AutoCommit = 'AutoCommit',
|
|
21
8
|
Sync = 'Sync',
|
|
@@ -43,6 +30,20 @@ export interface ConsumerConfiguration {
|
|
|
43
30
|
enableAutoCommit?: boolean
|
|
44
31
|
configuration?: Record<string, string>
|
|
45
32
|
}
|
|
33
|
+
export const enum SecurityProtocol {
|
|
34
|
+
Plaintext = 'Plaintext',
|
|
35
|
+
Ssl = 'Ssl',
|
|
36
|
+
SaslPlaintext = 'SaslPlaintext',
|
|
37
|
+
SaslSsl = 'SaslSsl'
|
|
38
|
+
}
|
|
39
|
+
export interface KafkaConfiguration {
|
|
40
|
+
brokers: string
|
|
41
|
+
clientId: string
|
|
42
|
+
securityProtocol?: SecurityProtocol
|
|
43
|
+
configuration?: Record<string, string>
|
|
44
|
+
logLevel?: string
|
|
45
|
+
brokerAddressFamily?: string
|
|
46
|
+
}
|
|
46
47
|
export interface ProducerConfiguration {
|
|
47
48
|
queueTimeout?: number
|
|
48
49
|
thrownOnError?: boolean
|
|
@@ -70,9 +71,9 @@ export interface Payload {
|
|
|
70
71
|
offset: number
|
|
71
72
|
}
|
|
72
73
|
export const enum PartitionPosition {
|
|
73
|
-
Beginning =
|
|
74
|
-
End =
|
|
75
|
-
Stored =
|
|
74
|
+
Beginning = 'Beginning',
|
|
75
|
+
End = 'End',
|
|
76
|
+
Stored = 'Stored'
|
|
76
77
|
}
|
|
77
78
|
export interface OffsetModel {
|
|
78
79
|
offset?: number
|
|
@@ -93,16 +94,15 @@ export interface MessageModel {
|
|
|
93
94
|
key?: Buffer
|
|
94
95
|
headers?: Record<string, Buffer>
|
|
95
96
|
}
|
|
97
|
+
export class KafkaConsumer {
|
|
98
|
+
startConsumer(callback: (error: Error | undefined, result: Payload) => Promise<ConsumerResult | undefined>): Promise<void>
|
|
99
|
+
}
|
|
96
100
|
export class KafkaClient {
|
|
97
101
|
readonly kafkaConfiguration: KafkaConfiguration
|
|
98
102
|
constructor(kafkaConfiguration: KafkaConfiguration)
|
|
99
103
|
createProducer(producerConfiguration: ProducerConfiguration): KafkaProducer
|
|
100
104
|
createConsumer(consumerConfiguration: ConsumerConfiguration): KafkaConsumer
|
|
101
105
|
}
|
|
102
|
-
export class ProducerHelper { }
|
|
103
|
-
export class KafkaConsumer {
|
|
104
|
-
startConsumer(callback: (error: Error | undefined, result: Payload) => Promise<ConsumerResult | undefined>): Promise<void>
|
|
105
|
-
}
|
|
106
106
|
export class KafkaProducer {
|
|
107
107
|
send(producerRecord: ProducerRecord): Promise<Array<RecordMetadata>>
|
|
108
108
|
}
|
package/index.js
CHANGED
|
@@ -237,6 +237,49 @@ switch (platform) {
|
|
|
237
237
|
loadError = e
|
|
238
238
|
}
|
|
239
239
|
break
|
|
240
|
+
case 'riscv64':
|
|
241
|
+
if (isMusl()) {
|
|
242
|
+
localFileExisted = existsSync(
|
|
243
|
+
join(__dirname, 'kafka-crab-js.linux-riscv64-musl.node')
|
|
244
|
+
)
|
|
245
|
+
try {
|
|
246
|
+
if (localFileExisted) {
|
|
247
|
+
nativeBinding = require('./kafka-crab-js.linux-riscv64-musl.node')
|
|
248
|
+
} else {
|
|
249
|
+
nativeBinding = require('kafka-crab-js-linux-riscv64-musl')
|
|
250
|
+
}
|
|
251
|
+
} catch (e) {
|
|
252
|
+
loadError = e
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
localFileExisted = existsSync(
|
|
256
|
+
join(__dirname, 'kafka-crab-js.linux-riscv64-gnu.node')
|
|
257
|
+
)
|
|
258
|
+
try {
|
|
259
|
+
if (localFileExisted) {
|
|
260
|
+
nativeBinding = require('./kafka-crab-js.linux-riscv64-gnu.node')
|
|
261
|
+
} else {
|
|
262
|
+
nativeBinding = require('kafka-crab-js-linux-riscv64-gnu')
|
|
263
|
+
}
|
|
264
|
+
} catch (e) {
|
|
265
|
+
loadError = e
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
break
|
|
269
|
+
case 's390x':
|
|
270
|
+
localFileExisted = existsSync(
|
|
271
|
+
join(__dirname, 'kafka-crab-js.linux-s390x-gnu.node')
|
|
272
|
+
)
|
|
273
|
+
try {
|
|
274
|
+
if (localFileExisted) {
|
|
275
|
+
nativeBinding = require('./kafka-crab-js.linux-s390x-gnu.node')
|
|
276
|
+
} else {
|
|
277
|
+
nativeBinding = require('kafka-crab-js-linux-s390x-gnu')
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
loadError = e
|
|
281
|
+
}
|
|
282
|
+
break
|
|
240
283
|
default:
|
|
241
284
|
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
|
242
285
|
}
|
|
@@ -252,14 +295,13 @@ if (!nativeBinding) {
|
|
|
252
295
|
throw new Error(`Failed to load native binding`)
|
|
253
296
|
}
|
|
254
297
|
|
|
255
|
-
const {
|
|
298
|
+
const { CommitMode, ConsumerResult, KafkaConsumer, SecurityProtocol, KafkaClient, KafkaProducer, AutoOffsetReset, PartitionPosition } = nativeBinding
|
|
256
299
|
|
|
257
|
-
module.exports.SecurityProtocol = SecurityProtocol
|
|
258
|
-
module.exports.KafkaClient = KafkaClient
|
|
259
300
|
module.exports.CommitMode = CommitMode
|
|
260
301
|
module.exports.ConsumerResult = ConsumerResult
|
|
261
|
-
module.exports.ProducerHelper = ProducerHelper
|
|
262
302
|
module.exports.KafkaConsumer = KafkaConsumer
|
|
303
|
+
module.exports.SecurityProtocol = SecurityProtocol
|
|
304
|
+
module.exports.KafkaClient = KafkaClient
|
|
263
305
|
module.exports.KafkaProducer = KafkaProducer
|
|
264
306
|
module.exports.AutoOffsetReset = AutoOffsetReset
|
|
265
307
|
module.exports.PartitionPosition = PartitionPosition
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kafka-crab-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"types": "index.d.ts",
|
|
6
6
|
"napi": {
|
|
@@ -24,19 +24,20 @@
|
|
|
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
|
-
"@napi-rs/cli": "^2.
|
|
29
|
-
"ava": "^
|
|
30
|
-
"
|
|
31
|
-
"kafkajs": "^2.2.4",
|
|
32
|
-
"nanoid": "^4.0.2",
|
|
33
|
-
"xo": "^0.54.2"
|
|
32
|
+
"@napi-rs/cli": "^2.18.0",
|
|
33
|
+
"ava": "^6.1.2",
|
|
34
|
+
"xo": "^0.58.0"
|
|
34
35
|
},
|
|
35
36
|
"ava": {
|
|
36
37
|
"timeout": "3m"
|
|
37
38
|
},
|
|
38
39
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
40
|
+
"node": ">= 18"
|
|
40
41
|
},
|
|
41
42
|
"xo": {
|
|
42
43
|
"space": true,
|
|
@@ -54,10 +55,10 @@
|
|
|
54
55
|
"universal": "napi universal",
|
|
55
56
|
"version": "napi version"
|
|
56
57
|
},
|
|
57
|
-
"packageManager": "yarn@
|
|
58
|
+
"packageManager": "yarn@4.1.1",
|
|
58
59
|
"optionalDependencies": {
|
|
59
|
-
"kafka-crab-js-darwin-arm64": "0.
|
|
60
|
-
"kafka-crab-js-darwin-x64": "0.
|
|
61
|
-
"kafka-crab-js-linux-x64-gnu": "0.
|
|
60
|
+
"kafka-crab-js-darwin-arm64": "0.5.4",
|
|
61
|
+
"kafka-crab-js-darwin-x64": "0.5.4",
|
|
62
|
+
"kafka-crab-js-linux-x64-gnu": "0.5.4"
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
use rdkafka::consumer::StreamConsumer;
|
|
2
|
+
use tracing::info;
|
|
3
|
+
|
|
4
|
+
use rdkafka::{
|
|
5
|
+
client::ClientContext,
|
|
6
|
+
consumer::{ConsumerContext, Rebalance},
|
|
7
|
+
error::KafkaResult,
|
|
8
|
+
topic_partition_list::TopicPartitionList,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
pub type LoggingConsumer = StreamConsumer<CustomContext>;
|
|
12
|
+
|
|
13
|
+
pub struct CustomContext;
|
|
14
|
+
|
|
15
|
+
impl ClientContext for CustomContext {}
|
|
16
|
+
|
|
17
|
+
impl ConsumerContext for CustomContext {
|
|
18
|
+
fn pre_rebalance(&self, rebalance: &Rebalance) {
|
|
19
|
+
info!("Pre rebalance {:?}", rebalance);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fn post_rebalance(&self, rebalance: &Rebalance) {
|
|
23
|
+
info!("Post rebalance {:?}", rebalance);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn commit_callback(&self, result: KafkaResult<()>, _offsets: &TopicPartitionList) {
|
|
27
|
+
info!("Committing offsets: {:?}. Offset: {:?}", result, _offsets);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
use napi::{bindgen_prelude::*, threadsafe_function::ThreadsafeFunction};
|
|
2
|
+
|
|
3
|
+
use rdkafka::{
|
|
4
|
+
consumer::{stream_consumer::StreamConsumer, CommitMode as RdKfafkaCommitMode, Consumer},
|
|
5
|
+
message::{BorrowedMessage, Header, OwnedHeaders},
|
|
6
|
+
producer::FutureRecord,
|
|
7
|
+
Message,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
use std::time::Duration;
|
|
11
|
+
use tracing::{debug, error, info, warn};
|
|
12
|
+
|
|
13
|
+
use crate::kafka::{
|
|
14
|
+
consumer::kafka_consumer::RETRY_COUNTER_NAME,
|
|
15
|
+
kafka_util::{
|
|
16
|
+
kakfa_headers_to_hashmap, kakfa_headers_to_hashmap_buffer, ExtractValueOnKafkaHashMap,
|
|
17
|
+
},
|
|
18
|
+
model::Payload,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
use super::kafka_consumer::{ConsumerResult, ProducerHelper};
|
|
22
|
+
|
|
23
|
+
use super::consumer_context::CustomContext;
|
|
24
|
+
|
|
25
|
+
pub struct ConsumerThread {
|
|
26
|
+
pub stream_consumer: StreamConsumer<CustomContext>,
|
|
27
|
+
pub func: ThreadsafeFunction<Payload>,
|
|
28
|
+
pub commit_mode: Option<RdKfafkaCommitMode>,
|
|
29
|
+
pub retries: i32,
|
|
30
|
+
pub next_topic_on_fail: Option<String>,
|
|
31
|
+
pub producer: ProducerHelper,
|
|
32
|
+
pub pause_consumer_duration: Option<Duration>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl ConsumerThread {
|
|
36
|
+
pub async fn start(&self) {
|
|
37
|
+
loop {
|
|
38
|
+
if let Some(duration) = &self.pause_consumer_duration {
|
|
39
|
+
tracing::debug!("Pause consumer for {}", duration.as_millis());
|
|
40
|
+
tokio::time::sleep(*duration).await;
|
|
41
|
+
};
|
|
42
|
+
match self.stream_consumer.recv().await {
|
|
43
|
+
Err(e) => error!("Error while receiving from stream consumer: {:?}", e),
|
|
44
|
+
Ok(message) => {
|
|
45
|
+
let message_result = self.process_message(&message).await;
|
|
46
|
+
match self.handle_message_result(&message, message_result).await {
|
|
47
|
+
Ok(_) => {
|
|
48
|
+
debug!(
|
|
49
|
+
"Message consumed successfully. Topic: {}, Partition: {}, Offset: {}",
|
|
50
|
+
message.topic(),
|
|
51
|
+
message.partition(),
|
|
52
|
+
message.offset()
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
Err(err) => {
|
|
56
|
+
error!("Message processing failed. Error: {:?}", err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async fn process_message(
|
|
65
|
+
&self,
|
|
66
|
+
message: &BorrowedMessage<'_>,
|
|
67
|
+
) -> anyhow::Result<Option<ConsumerResult>> {
|
|
68
|
+
match message.payload_view::<[u8]>() {
|
|
69
|
+
None => Ok(None),
|
|
70
|
+
Some(Ok(payload)) => {
|
|
71
|
+
let payload_js = create_payload(message, payload);
|
|
72
|
+
match self
|
|
73
|
+
.func
|
|
74
|
+
.call_async::<Promise<Option<ConsumerResult>>>(Ok(payload_js))
|
|
75
|
+
.await
|
|
76
|
+
{
|
|
77
|
+
Ok(js_result) => match js_result.await {
|
|
78
|
+
Ok(value) => match value {
|
|
79
|
+
None | Some(ConsumerResult::Ok) => {
|
|
80
|
+
if let Some(commit_mode) = self.commit_mode {
|
|
81
|
+
match self.stream_consumer.commit_message(message, commit_mode) {
|
|
82
|
+
Ok(_) => Ok(Some(ConsumerResult::Ok)),
|
|
83
|
+
Err(e) => Err(anyhow::Error::new(e)),
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
Ok(Some(ConsumerResult::Ok))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
Some(ConsumerResult::Retry) => Ok(Some(ConsumerResult::Retry)),
|
|
90
|
+
},
|
|
91
|
+
Err(_) => {
|
|
92
|
+
warn!("Error while calling return function");
|
|
93
|
+
Ok(Some(ConsumerResult::Ok))
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
Err(err) => {
|
|
97
|
+
warn!("Error while calling function: {:?}", err.to_string());
|
|
98
|
+
Ok(Some(ConsumerResult::Retry))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
Some(_) => Err(anyhow::Error::msg(
|
|
103
|
+
"Error while deserializing message payload",
|
|
104
|
+
)),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async fn handle_message_result(
|
|
109
|
+
&self,
|
|
110
|
+
message: &BorrowedMessage<'_>,
|
|
111
|
+
message_result: anyhow::Result<Option<ConsumerResult>>,
|
|
112
|
+
) -> anyhow::Result<()> {
|
|
113
|
+
match message_result {
|
|
114
|
+
Ok(consumer_result) => match consumer_result {
|
|
115
|
+
Some(result) => match result {
|
|
116
|
+
ConsumerResult::Ok => Ok(()),
|
|
117
|
+
ConsumerResult::Retry => match self.send_to_retry_strategy(message).await {
|
|
118
|
+
Ok(_) => {
|
|
119
|
+
debug!("Message sent to retry strategy");
|
|
120
|
+
Ok(())
|
|
121
|
+
}
|
|
122
|
+
Err(e) => Err(anyhow::Error::msg(format!(
|
|
123
|
+
"Error while sending message to retry strategy: {:?}",
|
|
124
|
+
e
|
|
125
|
+
))),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
None => Ok(()),
|
|
129
|
+
},
|
|
130
|
+
Err(err) => Err(err),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async fn send_to_retry_strategy(&self, message: &BorrowedMessage<'_>) -> anyhow::Result<()> {
|
|
135
|
+
let retry_counter: usize = {
|
|
136
|
+
let headers_map = kakfa_headers_to_hashmap(message.headers());
|
|
137
|
+
let mut counter = headers_map.get_value(RETRY_COUNTER_NAME).unwrap_or(1);
|
|
138
|
+
if self.retries > 0 {
|
|
139
|
+
counter += 1;
|
|
140
|
+
}
|
|
141
|
+
counter
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
let next_topic = if self.retries > 0 && retry_counter <= self.retries.try_into().unwrap() {
|
|
145
|
+
Some(message.topic().to_string())
|
|
146
|
+
} else {
|
|
147
|
+
self.next_topic_on_fail.clone()
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
match next_topic {
|
|
151
|
+
Some(next_topic) => {
|
|
152
|
+
info!("Message sent to next topic: {}", next_topic);
|
|
153
|
+
let new_headers = message
|
|
154
|
+
.headers()
|
|
155
|
+
.unwrap_or(OwnedHeaders::new().as_borrowed())
|
|
156
|
+
.detach()
|
|
157
|
+
.insert(Header {
|
|
158
|
+
key: RETRY_COUNTER_NAME,
|
|
159
|
+
value: Some(retry_counter.to_string().as_str()),
|
|
160
|
+
});
|
|
161
|
+
let key = message.key().unwrap_or(&[]);
|
|
162
|
+
|
|
163
|
+
let record = FutureRecord::to(&next_topic)
|
|
164
|
+
.key(key)
|
|
165
|
+
.headers(new_headers)
|
|
166
|
+
.payload(message.payload().unwrap_or(&[]));
|
|
167
|
+
|
|
168
|
+
let _result = self
|
|
169
|
+
.producer
|
|
170
|
+
.future_producer
|
|
171
|
+
.send(record, self.producer.queue_timeout)
|
|
172
|
+
.await
|
|
173
|
+
.map_err(|e| anyhow::Error::new(e.0))?;
|
|
174
|
+
Ok(())
|
|
175
|
+
}
|
|
176
|
+
None => {
|
|
177
|
+
debug!("There is no next topic. Commit message");
|
|
178
|
+
Ok(())
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
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
|
+
}
|
|
@@ -1,58 +1,33 @@
|
|
|
1
|
-
use napi::{
|
|
1
|
+
use napi::{threadsafe_function::ThreadsafeFunction, Result};
|
|
2
2
|
|
|
3
3
|
use rdkafka::{
|
|
4
|
-
client::ClientContext,
|
|
5
4
|
config::RDKafkaLogLevel,
|
|
6
|
-
consumer::{
|
|
7
|
-
|
|
8
|
-
Rebalance,
|
|
9
|
-
},
|
|
10
|
-
error::KafkaResult,
|
|
11
|
-
message::{BorrowedMessage, Header, OwnedHeaders},
|
|
12
|
-
producer::{FutureProducer, FutureRecord},
|
|
5
|
+
consumer::{stream_consumer::StreamConsumer, CommitMode as RdKfafkaCommitMode, Consumer},
|
|
6
|
+
producer::FutureProducer,
|
|
13
7
|
topic_partition_list::TopicPartitionList,
|
|
14
|
-
ClientConfig,
|
|
8
|
+
ClientConfig,
|
|
15
9
|
};
|
|
16
10
|
|
|
17
11
|
use std::{collections::HashMap, time::Duration, vec};
|
|
18
12
|
use tracing::{debug, error, info, warn};
|
|
19
13
|
|
|
20
|
-
use crate::kafka::
|
|
21
|
-
convert_to_rdkafka_offset, kakfa_headers_to_hashmap, ExtractValueOnKafkaHashMap,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
use super::{
|
|
14
|
+
use crate::kafka::{
|
|
25
15
|
kafka_admin::KafkaAdmin,
|
|
26
16
|
kafka_client::KafkaClient,
|
|
27
|
-
kafka_util::{
|
|
17
|
+
kafka_util::{convert_to_rdkafka_offset, AnyhowToNapiError},
|
|
28
18
|
model::{OffsetModel, PartitionPosition, Payload},
|
|
29
19
|
};
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const DEFAULT_DLQ_TOPIC_SUFFIX: &str = "-dlq";
|
|
36
|
-
|
|
37
|
-
type LoggingConsumer = StreamConsumer<CustomContext>;
|
|
38
|
-
|
|
39
|
-
pub struct CustomContext;
|
|
40
|
-
|
|
41
|
-
impl ClientContext for CustomContext {}
|
|
42
|
-
|
|
43
|
-
impl ConsumerContext for CustomContext {
|
|
44
|
-
fn pre_rebalance(&self, rebalance: &Rebalance) {
|
|
45
|
-
info!("Pre rebalance {:?}", rebalance);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
fn post_rebalance(&self, rebalance: &Rebalance) {
|
|
49
|
-
info!("Post rebalance {:?}", rebalance);
|
|
50
|
-
}
|
|
21
|
+
use super::{
|
|
22
|
+
consumer_context::{CustomContext, LoggingConsumer},
|
|
23
|
+
consumer_thread::ConsumerThread,
|
|
24
|
+
};
|
|
51
25
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
26
|
+
pub const RETRY_COUNTER_NAME: &str = "kafka-crab-js-retry-counter";
|
|
27
|
+
pub const DEFAULT_QUEUE_TIMEOUT: u64 = 5000;
|
|
28
|
+
pub const DEFAULT_PAUSE_DURATION: i64 = 1000;
|
|
29
|
+
pub const DEFAULT_RETRY_TOPIC_SUFFIX: &str = "-retry";
|
|
30
|
+
pub const DEFAULT_DLQ_TOPIC_SUFFIX: &str = "-dlq";
|
|
56
31
|
|
|
57
32
|
#[napi(string_enum)]
|
|
58
33
|
#[derive(Debug, PartialEq)]
|
|
@@ -81,10 +56,9 @@ pub enum ConsumerResult {
|
|
|
81
56
|
}
|
|
82
57
|
|
|
83
58
|
#[derive(Clone)]
|
|
84
|
-
#[napi]
|
|
85
59
|
pub struct ProducerHelper {
|
|
86
|
-
future_producer: FutureProducer,
|
|
87
|
-
queue_timeout: Duration,
|
|
60
|
+
pub future_producer: FutureProducer,
|
|
61
|
+
pub queue_timeout: Duration,
|
|
88
62
|
}
|
|
89
63
|
|
|
90
64
|
fn setup_future_producer(
|
|
@@ -149,6 +123,11 @@ impl KafkaConsumer {
|
|
|
149
123
|
})
|
|
150
124
|
}
|
|
151
125
|
|
|
126
|
+
// #[napi(ts_args_type = "callback: (error: Error) => Promise<ConsumerResult | undefined>")]
|
|
127
|
+
// pub async fn create_consumer(&self) -> Result<()> {
|
|
128
|
+
// Ok(())
|
|
129
|
+
// }
|
|
130
|
+
|
|
152
131
|
#[napi(
|
|
153
132
|
ts_args_type = "callback: (error: Error | undefined, result: Payload) => Promise<ConsumerResult | undefined>"
|
|
154
133
|
)]
|
|
@@ -286,29 +265,46 @@ impl KafkaConsumer {
|
|
|
286
265
|
enable_auto_commit.unwrap_or(true).to_string(),
|
|
287
266
|
)
|
|
288
267
|
.set_log_level(RDKafkaLogLevel::Debug)
|
|
289
|
-
.create_with_context(context)
|
|
290
|
-
.expect("Consumer creation failed");
|
|
268
|
+
.create_with_context(context)?;
|
|
291
269
|
|
|
292
270
|
if create_topic.unwrap_or(true) {
|
|
271
|
+
info!("Creating topic: {:?}", topic);
|
|
293
272
|
let admin = KafkaAdmin::new(&self.client_config);
|
|
294
|
-
admin.create_topic(topic).await
|
|
273
|
+
let result = admin.create_topic(topic).await;
|
|
274
|
+
if let Err(e) = result {
|
|
275
|
+
warn!("Fail to create topic {:?}", e);
|
|
276
|
+
return Err(anyhow::Error::msg(format!("Fail to create topic: {:?}", e)));
|
|
277
|
+
}
|
|
278
|
+
info!("Topic created: {:?}", topic)
|
|
295
279
|
}
|
|
296
280
|
|
|
297
281
|
if let Some(offset) = convert_to_rdkafka_offset(offset) {
|
|
298
282
|
debug!("Setting offset to: {:?}", offset);
|
|
299
|
-
let metadata = consumer
|
|
300
|
-
.fetch_metadata(Some(topic), Duration::from_millis(1500))
|
|
301
|
-
.expect("Fail to retrive metadata from consumer");
|
|
283
|
+
let metadata = consumer.fetch_metadata(Some(topic), Duration::from_millis(1500))?;
|
|
302
284
|
|
|
303
285
|
metadata.topics().iter().for_each(|meta_topic| {
|
|
304
286
|
let mut tpl = TopicPartitionList::new();
|
|
305
287
|
meta_topic.partitions().iter().for_each(|meta_partition| {
|
|
306
288
|
tpl.add_partition(topic, meta_partition.id());
|
|
307
289
|
});
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
290
|
+
//TODO: Handle error
|
|
291
|
+
match tpl.set_all_offsets(offset) {
|
|
292
|
+
Ok(_) => {
|
|
293
|
+
debug!("Offset set to: {:?}", offset);
|
|
294
|
+
}
|
|
295
|
+
Err(e) => {
|
|
296
|
+
error!("Fail to set offset: {:?}", e)
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
//TODO: Handle error
|
|
300
|
+
match consumer.assign(&tpl) {
|
|
301
|
+
Ok(_) => {
|
|
302
|
+
debug!("Assigning topic: {:?}", topic);
|
|
303
|
+
}
|
|
304
|
+
Err(e) => {
|
|
305
|
+
error!("Fail to assign topic: {:?}", e);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
312
308
|
});
|
|
313
309
|
}
|
|
314
310
|
|
|
@@ -328,176 +324,3 @@ impl KafkaConsumer {
|
|
|
328
324
|
Ok(consumer)
|
|
329
325
|
}
|
|
330
326
|
}
|
|
331
|
-
|
|
332
|
-
pub struct ConsumerThread {
|
|
333
|
-
stream_consumer: StreamConsumer<CustomContext>,
|
|
334
|
-
func: ThreadsafeFunction<Payload>,
|
|
335
|
-
commit_mode: Option<RdKfafkaCommitMode>,
|
|
336
|
-
retries: i32,
|
|
337
|
-
next_topic_on_fail: Option<String>,
|
|
338
|
-
producer: ProducerHelper,
|
|
339
|
-
pause_consumer_duration: Option<Duration>,
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
impl ConsumerThread {
|
|
343
|
-
pub async fn start(&self) {
|
|
344
|
-
loop {
|
|
345
|
-
if let Some(duration) = &self.pause_consumer_duration {
|
|
346
|
-
tracing::debug!("Pause consumer for {}", duration.as_millis());
|
|
347
|
-
tokio::time::sleep(*duration).await;
|
|
348
|
-
};
|
|
349
|
-
match self.stream_consumer.recv().await {
|
|
350
|
-
Err(e) => error!("Error while receiving from stream consumer: {:?}", e),
|
|
351
|
-
Ok(message) => {
|
|
352
|
-
let message_result = self.process_message(&message).await;
|
|
353
|
-
match self.handle_message_result(&message, message_result).await {
|
|
354
|
-
Ok(_) => {
|
|
355
|
-
debug!(
|
|
356
|
-
"Message consumed successfully. Topic: {}, Partition: {}, Offset: {}",
|
|
357
|
-
message.topic(),
|
|
358
|
-
message.partition(),
|
|
359
|
-
message.offset()
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
Err(err) => {
|
|
363
|
-
error!("Message processing failed. Error: {:?}", err);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async fn process_message(
|
|
372
|
-
&self,
|
|
373
|
-
message: &BorrowedMessage<'_>,
|
|
374
|
-
) -> anyhow::Result<Option<ConsumerResult>> {
|
|
375
|
-
match message.payload_view::<[u8]>() {
|
|
376
|
-
None => Ok(None),
|
|
377
|
-
Some(Ok(payload)) => {
|
|
378
|
-
let payload_js = create_payload(message, payload);
|
|
379
|
-
match self
|
|
380
|
-
.func
|
|
381
|
-
.call_async::<Promise<Option<ConsumerResult>>>(Ok(payload_js))
|
|
382
|
-
.await
|
|
383
|
-
{
|
|
384
|
-
Ok(js_result) => match js_result.await {
|
|
385
|
-
Ok(value) => match value {
|
|
386
|
-
None | Some(ConsumerResult::Ok) => {
|
|
387
|
-
if let Some(commit_mode) = self.commit_mode {
|
|
388
|
-
match self.stream_consumer.commit_message(message, commit_mode) {
|
|
389
|
-
Ok(_) => Ok(Some(ConsumerResult::Ok)),
|
|
390
|
-
Err(e) => Err(anyhow::Error::new(e)),
|
|
391
|
-
}
|
|
392
|
-
} else {
|
|
393
|
-
Ok(Some(ConsumerResult::Ok))
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
Some(ConsumerResult::Retry) => Ok(Some(ConsumerResult::Retry)),
|
|
397
|
-
},
|
|
398
|
-
Err(_) => {
|
|
399
|
-
warn!("Error while calling return function");
|
|
400
|
-
Ok(Some(ConsumerResult::Ok))
|
|
401
|
-
}
|
|
402
|
-
},
|
|
403
|
-
Err(err) => {
|
|
404
|
-
warn!("Error while calling function: {:?}", err.to_string());
|
|
405
|
-
Ok(Some(ConsumerResult::Retry))
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
Some(_) => Err(anyhow::Error::msg(
|
|
410
|
-
"Error while deserializing message payload",
|
|
411
|
-
)),
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
async fn handle_message_result(
|
|
416
|
-
&self,
|
|
417
|
-
message: &BorrowedMessage<'_>,
|
|
418
|
-
message_result: anyhow::Result<Option<ConsumerResult>>,
|
|
419
|
-
) -> anyhow::Result<()> {
|
|
420
|
-
match message_result {
|
|
421
|
-
Ok(consumer_result) => match consumer_result {
|
|
422
|
-
Some(result) => match result {
|
|
423
|
-
ConsumerResult::Ok => Ok(()),
|
|
424
|
-
ConsumerResult::Retry => match self.send_to_retry_strategy(message).await {
|
|
425
|
-
Ok(_) => {
|
|
426
|
-
debug!("Message sent to retry strategy");
|
|
427
|
-
Ok(())
|
|
428
|
-
}
|
|
429
|
-
Err(e) => Err(anyhow::Error::msg(format!(
|
|
430
|
-
"Error while sending message to retry strategy: {:?}",
|
|
431
|
-
e
|
|
432
|
-
))),
|
|
433
|
-
},
|
|
434
|
-
},
|
|
435
|
-
None => Ok(()),
|
|
436
|
-
},
|
|
437
|
-
Err(err) => Err(err),
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async fn send_to_retry_strategy(&self, message: &BorrowedMessage<'_>) -> anyhow::Result<()> {
|
|
442
|
-
let retry_counter: usize = {
|
|
443
|
-
let headers_map = kakfa_headers_to_hashmap(message.headers());
|
|
444
|
-
let mut counter = headers_map.get_value(RETRY_COUNTER_NAME).unwrap_or(1);
|
|
445
|
-
if self.retries > 0 {
|
|
446
|
-
counter += 1;
|
|
447
|
-
}
|
|
448
|
-
counter
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
let next_topic = if self.retries > 0 && retry_counter <= self.retries.try_into().unwrap() {
|
|
452
|
-
Some(message.topic().to_string())
|
|
453
|
-
} else {
|
|
454
|
-
self.next_topic_on_fail.clone()
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
match next_topic {
|
|
458
|
-
Some(next_topic) => {
|
|
459
|
-
info!("Message sent to next topic: {}", next_topic);
|
|
460
|
-
let new_headers = message
|
|
461
|
-
.headers()
|
|
462
|
-
.unwrap_or(OwnedHeaders::new().as_borrowed())
|
|
463
|
-
.detach()
|
|
464
|
-
.insert(Header {
|
|
465
|
-
key: RETRY_COUNTER_NAME,
|
|
466
|
-
value: Some(retry_counter.to_string().as_str()),
|
|
467
|
-
});
|
|
468
|
-
let key = message.key().unwrap_or(&[]);
|
|
469
|
-
|
|
470
|
-
let record = FutureRecord::to(&next_topic)
|
|
471
|
-
.key(key)
|
|
472
|
-
.headers(new_headers)
|
|
473
|
-
.payload(message.payload().unwrap_or(&[]));
|
|
474
|
-
|
|
475
|
-
let _result = self
|
|
476
|
-
.producer
|
|
477
|
-
.future_producer
|
|
478
|
-
.send(record, self.producer.queue_timeout)
|
|
479
|
-
.await
|
|
480
|
-
.map_err(|e| anyhow::Error::new(e.0))?;
|
|
481
|
-
Ok(())
|
|
482
|
-
}
|
|
483
|
-
None => {
|
|
484
|
-
debug!("There is no next topic. Commit message");
|
|
485
|
-
Ok(())
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
fn create_payload(message: &BorrowedMessage<'_>, payload: &[u8]) -> Payload {
|
|
492
|
-
let key: Option<Buffer> = message.key().map(|bytes| bytes.into());
|
|
493
|
-
let headers = Some(kakfa_headers_to_hashmap_buffer(message.headers()));
|
|
494
|
-
let payload_js = Payload::new(
|
|
495
|
-
payload.into(),
|
|
496
|
-
key,
|
|
497
|
-
headers,
|
|
498
|
-
message.topic().to_owned(),
|
|
499
|
-
message.partition(),
|
|
500
|
-
message.offset(),
|
|
501
|
-
);
|
|
502
|
-
payload_js
|
|
503
|
-
}
|
package/src/kafka/kafka_admin.rs
CHANGED
|
@@ -33,14 +33,9 @@ impl<'a> KafkaAdmin<'a> {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async fn fetch_config_resource(&self) -> Result<HashMap<String, String>, KafkaError> {
|
|
36
|
-
let consumer: BaseConsumer = self
|
|
37
|
-
.client_config
|
|
38
|
-
.create()
|
|
39
|
-
.expect("Consumer creation failed");
|
|
36
|
+
let consumer: BaseConsumer = self.client_config.create()?;
|
|
40
37
|
|
|
41
|
-
let metadata = consumer
|
|
42
|
-
.fetch_metadata(None, Duration::from_secs(5))
|
|
43
|
-
.expect("Failed to fetch metadata");
|
|
38
|
+
let metadata = consumer.fetch_metadata(None, Duration::from_secs(5))?;
|
|
44
39
|
|
|
45
40
|
for broker in metadata.brokers() {
|
|
46
41
|
info!(
|
|
@@ -63,9 +58,9 @@ impl<'a> KafkaAdmin<'a> {
|
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
pub async fn create_topic(&self, topic_name: &str) -> anyhow::Result<()> {
|
|
61
|
+
debug!("Fetching broker properties");
|
|
66
62
|
let broker_properties = self.fetch_config_resource().await?.clone();
|
|
67
|
-
|
|
68
|
-
// If you want print all properties => dbg!(broker_properties.clone());
|
|
63
|
+
debug!("Broker properties: {:?}", broker_properties);
|
|
69
64
|
|
|
70
65
|
self
|
|
71
66
|
.admin_client
|
|
@@ -12,7 +12,7 @@ use rdkafka::{
|
|
|
12
12
|
use tracing::{info, Level};
|
|
13
13
|
|
|
14
14
|
use super::{
|
|
15
|
-
kafka_consumer::{ConsumerConfiguration, KafkaConsumer},
|
|
15
|
+
consumer::kafka_consumer::{ConsumerConfiguration, KafkaConsumer},
|
|
16
16
|
kafka_producer::{KafkaProducer, ProducerConfiguration},
|
|
17
17
|
};
|
|
18
18
|
|
|
@@ -62,6 +62,7 @@ pub struct KafkaConfiguration {
|
|
|
62
62
|
pub security_protocol: Option<SecurityProtocol>,
|
|
63
63
|
pub configuration: Option<HashMap<String, String>>,
|
|
64
64
|
pub log_level: Option<String>,
|
|
65
|
+
pub broker_address_family: Option<String>,
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
#[derive(Clone, Debug)]
|
|
@@ -103,6 +104,7 @@ impl KafkaClient {
|
|
|
103
104
|
security_protocol,
|
|
104
105
|
client_id,
|
|
105
106
|
configuration,
|
|
107
|
+
broker_address_family,
|
|
106
108
|
..
|
|
107
109
|
} = kafka_configuration.clone();
|
|
108
110
|
|
|
@@ -111,6 +113,10 @@ impl KafkaClient {
|
|
|
111
113
|
rdkafka_client_config.set_log_level(RDKafkaLogLevel::Debug);
|
|
112
114
|
rdkafka_client_config.set("bootstrap.servers", brokers);
|
|
113
115
|
rdkafka_client_config.set("client.id", client_id);
|
|
116
|
+
rdkafka_client_config.set(
|
|
117
|
+
"broker.address.family",
|
|
118
|
+
broker_address_family.unwrap_or("v4".to_string()),
|
|
119
|
+
);
|
|
114
120
|
if let Some(security_protocol) = security_protocol {
|
|
115
121
|
rdkafka_client_config.set("security.protocol", security_protocol.to_string());
|
|
116
122
|
}
|
package/src/kafka/mod.rs
CHANGED
package/src/kafka/model.rs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
use napi::bindgen_prelude::*;
|
|
2
|
-
|
|
3
1
|
use std::{collections::HashMap, fmt};
|
|
4
2
|
|
|
5
3
|
use napi::bindgen_prelude::Buffer;
|
|
@@ -53,7 +51,7 @@ impl Payload {
|
|
|
53
51
|
}
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
#[napi(
|
|
54
|
+
#[napi(string_enum)]
|
|
57
55
|
#[derive(Debug)]
|
|
58
56
|
pub enum PartitionPosition {
|
|
59
57
|
Beginning,
|