kafka-crab-js 0.0.0 → 0.0.3

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 CHANGED
@@ -16,9 +16,6 @@ anyhow = { version= "1.0", features = ["backtrace"]}
16
16
  rdkafka = { version = "0.32", features = ["libz-static", "cmake-build", "tokio", "ssl-vendored"] }
17
17
  tokio = { version = "1", features = ["full"] }
18
18
 
19
- # log = "0.4"
20
- # env_logger = "0.10.0"
21
-
22
19
  tracing = "0.1"
23
20
  tracing-subscriber = "0.3"
24
21
 
package/README.md CHANGED
@@ -1,5 +1,16 @@
1
1
  Kafka Crab Js
2
2
  -------------
3
3
 
4
- Blazingly Fast Kafka Client for NodeJs
4
+ **Blazingly Fast Kafka Client for NodeJs**
5
5
 
6
+ Kafka-Crab-JS is a powerful Node.js library that allows developers to interact with Apache Kafka using Rust programming language, seamlessly integrated with Node.js applications. This documentation provides an overview of the Kafka-Crab-JS library, including installation instructions, usage guidelines, and examples.
7
+
8
+
9
+ # Installation
10
+
11
+ To use Kafka-Crab-JS in your Node.js project, follow the steps below:
12
+
13
+ todo
14
+
15
+ # Examples
16
+ todo
@@ -1,50 +1,69 @@
1
- import { ConsumerResult, KafkaClient, KafkaCommitMode, PartitionPosition } from '../index.js'
2
- import timersPromises from 'node:timers/promises'
3
- import { Buffer } from 'node:buffer';
1
+ /* eslint-disable no-await-in-loop */
2
+ import timersPromises from 'node:timers/promises';
3
+ import {Buffer} from 'node:buffer';
4
+ import {ConsumerResult, KafkaClient, KafkaCommitMode, PartitionPosition} from '../index.js';
4
5
 
5
- const kafkaClient = new KafkaClient("localhost:29092", "my-id")
6
- const topic = "my-js-topic"
6
+ const kafkaClient = new KafkaClient('localhost:29092', 'my-id');
7
+ const topic = 'my-js-topic';
7
8
 
8
- let counter = 50;
9
+ let counter = 0;
9
10
 
10
11
  async function process(message) {
11
- await timersPromises.setTimeout(1)
12
- return "message received: " + message
12
+ await timersPromises.setTimeout(1);
13
+ return 'message received: ' + message;
13
14
  }
14
15
 
15
16
  const consumer = kafkaClient.createConsumer({
16
- topic,
17
- groupId: "my-js-group",
18
- commitMode: KafkaCommitMode.Sync,
19
- offset: { position: PartitionPosition.Stored },
20
- })
21
-
22
- console.time("consumer")
23
-
24
- consumer.startConsumer(async (err, value)=> {
25
- if (err) {
26
- console.error("Consumer error", err)
27
- return
28
- }
29
- const message = value.toString()
30
- const content = await process(message)
31
- counter++
32
- if (counter >= 0) {
33
- console.timeEnd("consumer")
34
- console.log("Counter:", counter, " Content:", content)
35
- console.time("consumer")
36
- }
37
- return ConsumerResult.Ok
38
- })
39
-
40
- await timersPromises.setTimeout(5_000)
41
-
42
- const result = await kafkaClient.createProducer().send(
43
- topic,
44
- {
45
- key: Buffer.from("abc"),
46
- value: Buffer.from(`{"_id":"${counter}","name":"Elizeu Drummond Sample js","phone":"555"}`),
47
- headers: {"key": Buffer.from("value1")}
48
- }
49
- )
50
- console.log(result)
17
+ topic,
18
+ groupId: 'my-js-group',
19
+ commitMode: KafkaCommitMode.Sync,
20
+ offset: {position: PartitionPosition.Stored},
21
+ retryStrategy: {
22
+ retries: 3,
23
+ },
24
+ });
25
+
26
+ console.time('consumer');
27
+
28
+ consumer.startConsumer(async (error, value) => {
29
+ if (error) {
30
+ console.error('Consumer error', error);
31
+ return;
32
+ }
33
+
34
+ const message = JSON.parse(value.toString());
35
+ const content = await process(message);
36
+ counter++;
37
+ if (counter >= 0) {
38
+ console.timeEnd('consumer');
39
+ console.log('Counter:', counter, 'Content:', JSON.stringify(content));
40
+ console.time('consumer');
41
+ }
42
+
43
+ if (message._id === '3') {
44
+ console.error('Error on process message, let\'s retry', message);
45
+ return ConsumerResult.Retry;
46
+ }
47
+
48
+ return ConsumerResult.Ok;
49
+ });
50
+
51
+ const producer = kafkaClient.createProducer({topic, configuration: {'message.timeout.ms': '2'}});
52
+ console.log('Sending message');
53
+
54
+ for (let i = 0; i < 5; i++) {
55
+ try {
56
+ const result = await producer.send(
57
+ {
58
+ topic,
59
+ key: Buffer.from('abc'),
60
+ value: Buffer.from(`{"_id":"${i}","name":"Elizeu Drummond Sample js","phone":"555"}`),
61
+ headers: {key: Buffer.from('value1')},
62
+ },
63
+ );
64
+ console.log(result);
65
+ await timersPromises.setTimeout(1200);
66
+ } catch (error) {
67
+ console.error('Error on send', error);
68
+ }
69
+ }
package/index.d.ts CHANGED
@@ -9,11 +9,22 @@ export const enum SecurityProtocol {
9
9
  SaslPlaintext = 'SaslPlaintext',
10
10
  SaslSsl = 'SaslSsl'
11
11
  }
12
- export interface OwnedDelivery {
13
- partition: number
14
- offset: number
12
+ export interface ConsumerConfiguration {
13
+ topic: string
14
+ groupId: string
15
+ retryStrategy?: RetryStrategy
16
+ offset?: OffsetModel
17
+ createTopic?: boolean
18
+ commitMode?: KafkaCommitMode
19
+ enableAutoCommit?: boolean
20
+ configuration?: Record<string, string>
21
+ }
22
+ export interface ProducerConfiguration {
23
+ queueTimeout?: number
24
+ configuration?: Record<string, string>
15
25
  }
16
26
  export interface MessageModel {
27
+ topic: string
17
28
  key: Buffer
18
29
  value: Buffer
19
30
  headers?: Record<string, Buffer>
@@ -36,13 +47,9 @@ export const enum KafkaCommitMode {
36
47
  Sync = 'Sync',
37
48
  Async = 'Async'
38
49
  }
39
- export interface ConsumerConfiguration {
40
- topic: string
41
- groupId: string
42
- retryStrategy?: RetryStrategy
43
- offset?: OffsetModel
44
- createTopic?: boolean
45
- commitMode?: KafkaCommitMode
50
+ export interface OwnedDelivery {
51
+ partition: number
52
+ offset: number
46
53
  }
47
54
  export const enum PartitionPosition {
48
55
  Beginning = 0,
@@ -55,7 +62,8 @@ export interface OffsetModel {
55
62
  }
56
63
  export interface RetryStrategy {
57
64
  retries: number
58
- nextTopicOnFail: string
65
+ retryTopic?: string
66
+ dqlTopic?: string
59
67
  pauseConsumerDuration?: number
60
68
  }
61
69
  export const enum ConsumerResult {
@@ -66,17 +74,17 @@ export class KafkaConfiguration {
66
74
  brokers: string
67
75
  clientId: string
68
76
  securityProtocol?: SecurityProtocol
69
- allowAutoCreateTopic?: boolean
77
+ configuration?: Record<string, string>
70
78
  }
71
79
  export class KafkaClient {
72
80
  readonly kafkaConfiguration: KafkaConfiguration
73
- constructor(brokers: string, clientId: string)
74
- createProducer(): KafkaProducer
81
+ constructor(brokers: string, clientId: string, securityProtocol?: SecurityProtocol | undefined | null, configuration?: Record<string, string> | undefined | null)
82
+ createProducer(producerConfiguration: ProducerConfiguration): KafkaProducer
75
83
  createConsumer(consumerConfiguration: ConsumerConfiguration): KafkaConsumer
76
84
  }
77
85
  export class KafkaConsumer {
78
86
  startConsumer(callback: (err: Error | null, result: Buffer) => Promise<ConsumerResult>): Promise<void>
79
87
  }
80
88
  export class KafkaProducer {
81
- send(topic: string, message: MessageModel): Promise<OwnedDelivery>
89
+ send(message: MessageModel): Promise<OwnedDelivery>
82
90
  }
package/package.json CHANGED
@@ -1,22 +1,25 @@
1
1
  {
2
2
  "name": "kafka-crab-js",
3
- "version": "0.0.0",
3
+ "version": "0.0.3",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "napi": {
7
7
  "name": "kafka-crab-js",
8
8
  "triples": {
9
+ "defaults": false,
9
10
  "additional": [
10
11
  "aarch64-apple-darwin",
11
- "x86_64-unknown-linux-musl"
12
+ "x86_64-apple-darwin",
13
+ "x86_64-unknown-linux-gnu"
12
14
  ]
13
15
  }
14
16
  },
15
17
  "license": "MIT",
16
18
  "devDependencies": {
17
19
  "@napi-rs/cli": "^2.16.1",
18
- "ava": "^5.1.1",
19
- "nanoid": "^4.0.2"
20
+ "ava": "^5.3.1",
21
+ "nanoid": "^4.0.2",
22
+ "xo": "^0.54.2"
20
23
  },
21
24
  "ava": {
22
25
  "timeout": "3m"
@@ -24,6 +27,9 @@
24
27
  "engines": {
25
28
  "node": ">= 10"
26
29
  },
30
+ "xo": {
31
+ "space": true
32
+ },
27
33
  "scripts": {
28
34
  "artifacts": "napi artifacts",
29
35
  "build": "napi build --platform --release",
@@ -35,10 +41,8 @@
35
41
  },
36
42
  "packageManager": "yarn@3.6.0",
37
43
  "optionalDependencies": {
38
- "kafka-crab-js-win32-x64-msvc": "0.0.0",
39
- "kafka-crab-js-darwin-x64": "0.0.0",
40
- "kafka-crab-js-linux-x64-gnu": "0.0.0",
41
- "kafka-crab-js-darwin-arm64": "0.0.0",
42
- "kafka-crab-js-linux-x64-musl": "0.0.0"
44
+ "kafka-crab-js-darwin-arm64": "0.0.3",
45
+ "kafka-crab-js-darwin-x64": "0.0.3",
46
+ "kafka-crab-js-linux-x64-gnu": "0.0.3"
43
47
  }
44
48
  }
@@ -65,7 +65,7 @@ impl<'a> KafkaAdmin<'a> {
65
65
  pub async fn create_topic(&self, topic_name: &str) -> anyhow::Result<()> {
66
66
  let broker_properties = self.fetch_config_resource().await?.clone();
67
67
 
68
- debug!("Broker properties {:?}", broker_properties);
68
+ // If you want print all properties => dbg!(broker_properties.clone());
69
69
 
70
70
  self
71
71
  .admin_client
@@ -1,6 +1,6 @@
1
- use std::fmt;
1
+ use std::{collections::HashMap, fmt};
2
2
 
3
- use napi::bindgen_prelude::*;
3
+ use napi::{bindgen_prelude::*, Result};
4
4
  use rdkafka::{
5
5
  client::ClientContext,
6
6
  config::{ClientConfig, RDKafkaLogLevel},
@@ -8,10 +8,12 @@ use rdkafka::{
8
8
  error::KafkaResult,
9
9
  topic_partition_list::TopicPartitionList,
10
10
  };
11
+
11
12
  use tracing::info;
12
13
 
13
14
  use super::{
14
- kafka_consumer::KafkaConsumer, kafka_producer::KafkaProducer, model::ConsumerConfiguration,
15
+ kafka_consumer::{ConsumerConfiguration, KafkaConsumer},
16
+ kafka_producer::{KafkaProducer, ProducerConfiguration},
15
17
  };
16
18
 
17
19
  struct CustomContext;
@@ -58,7 +60,7 @@ pub struct KafkaConfiguration {
58
60
  pub brokers: String,
59
61
  pub client_id: String,
60
62
  pub security_protocol: Option<SecurityProtocol>,
61
- pub allow_auto_create_topic: Option<bool>,
63
+ pub configuration: Option<HashMap<String, String>>,
62
64
  }
63
65
 
64
66
  #[derive(Clone, Debug)]
@@ -72,7 +74,12 @@ pub struct KafkaClient {
72
74
  #[napi]
73
75
  impl KafkaClient {
74
76
  #[napi(constructor)]
75
- pub fn new(brokers: String, client_id: String) -> Self {
77
+ pub fn new(
78
+ brokers: String,
79
+ client_id: String,
80
+ security_protocol: Option<SecurityProtocol>,
81
+ configuration: Option<HashMap<String, String>>,
82
+ ) -> Self {
76
83
  match tracing_subscriber::fmt::try_init() {
77
84
  Ok(_) => {}
78
85
  Err(e) => println!("Failed to initialize tracing: {:?}", e),
@@ -81,8 +88,8 @@ impl KafkaClient {
81
88
  KafkaClient::with_kafka_configuration(KafkaConfiguration {
82
89
  brokers,
83
90
  client_id,
84
- security_protocol: None,
85
- allow_auto_create_topic: None,
91
+ security_protocol,
92
+ configuration,
86
93
  })
87
94
  }
88
95
 
@@ -95,7 +102,7 @@ impl KafkaClient {
95
102
  brokers,
96
103
  security_protocol,
97
104
  client_id,
98
- allow_auto_create_topic,
105
+ configuration,
99
106
  } = kafka_configuration.clone();
100
107
 
101
108
  let mut rdkafka_client_config = ClientConfig::new();
@@ -106,11 +113,10 @@ impl KafkaClient {
106
113
  if let Some(security_protocol) = security_protocol {
107
114
  rdkafka_client_config.set("security.protocol", security_protocol.to_string());
108
115
  }
109
- if let Some(allow_auto_create_topic) = allow_auto_create_topic {
110
- rdkafka_client_config.set(
111
- "allow.auto.create.topics",
112
- allow_auto_create_topic.to_string(),
113
- );
116
+ if let Some(config) = configuration {
117
+ for (key, value) in config {
118
+ rdkafka_client_config.set(key, value);
119
+ }
114
120
  }
115
121
 
116
122
  KafkaClient {
@@ -120,12 +126,21 @@ impl KafkaClient {
120
126
  }
121
127
 
122
128
  #[napi]
123
- pub fn create_producer(&self) -> KafkaProducer {
124
- KafkaProducer::new(self.rdkafka_client_config.clone())
129
+ pub fn create_producer(
130
+ &self,
131
+ producer_configuration: ProducerConfiguration,
132
+ ) -> Result<KafkaProducer> {
133
+ match KafkaProducer::new(self.rdkafka_client_config.clone(), producer_configuration) {
134
+ Ok(producer) => Ok(producer),
135
+ Err(e) => Err(Error::new(Status::GenericFailure, e.to_string())),
136
+ }
125
137
  }
126
138
 
127
139
  #[napi]
128
- pub fn create_consumer(&self, consumer_configuration: ConsumerConfiguration) -> KafkaConsumer {
140
+ pub fn create_consumer(
141
+ &self,
142
+ consumer_configuration: ConsumerConfiguration,
143
+ ) -> Result<KafkaConsumer> {
129
144
  KafkaConsumer::new(self.clone(), consumer_configuration)
130
145
  }
131
146
  }
@@ -8,20 +8,28 @@ use rdkafka::{
8
8
  config::RDKafkaLogLevel,
9
9
  consumer::{stream_consumer::StreamConsumer, CommitMode, Consumer, ConsumerContext, Rebalance},
10
10
  error::KafkaResult,
11
- message::BorrowedMessage,
11
+ message::{BorrowedMessage, Header, OwnedHeaders},
12
+ producer::FutureRecord,
12
13
  topic_partition_list::TopicPartitionList,
13
- Message, Offset,
14
+ ClientConfig, Message,
14
15
  };
15
16
 
16
- use std::{time::Duration, vec};
17
+ use std::{collections::HashMap, time::Duration, vec};
17
18
  use tracing::{debug, error, info};
18
19
 
20
+ use crate::kafka::kafka_util::{
21
+ convert_to_rdkafka_offset, kakfa_headers_to_hashmap, ExtractValueOnKafkaHashMap,
22
+ };
23
+
19
24
  use super::{
20
25
  kafka_admin::KafkaAdmin,
21
26
  kafka_client::KafkaClient,
22
- model::{ConsumerConfiguration, ConsumerResult, KafkaCommitMode, OffsetModel, PartitionPosition},
27
+ kafka_producer::{KafkaProducer, ProducerConfiguration},
28
+ model::{ConsumerResult, KafkaCommitMode, OffsetModel, RetryStrategy},
23
29
  };
24
30
 
31
+ const RETRY_COUNTER_NAME: &str = "kafka-crab-js-retry-counter";
32
+
25
33
  type LoggingConsumer = StreamConsumer<CustomContext>;
26
34
 
27
35
  pub struct CustomContext;
@@ -42,25 +50,56 @@ impl ConsumerContext for CustomContext {
42
50
  }
43
51
  }
44
52
 
53
+ #[napi(object)]
45
54
  #[derive(Clone, Debug)]
55
+ pub struct ConsumerConfiguration {
56
+ pub topic: String,
57
+ pub group_id: String,
58
+ pub retry_strategy: Option<RetryStrategy>,
59
+ pub offset: Option<OffsetModel>,
60
+ pub create_topic: Option<bool>,
61
+ pub commit_mode: Option<KafkaCommitMode>,
62
+ pub enable_auto_commit: Option<bool>,
63
+ pub configuration: Option<HashMap<String, String>>,
64
+ }
65
+
66
+ #[derive(Clone)]
46
67
  #[napi]
47
68
  pub struct KafkaConsumer {
48
69
  kafka_client: KafkaClient,
49
70
  consumer_configuration: ConsumerConfiguration,
71
+ producer: KafkaProducer,
50
72
  }
51
73
 
52
74
  #[napi]
53
75
  impl KafkaConsumer {
54
- pub fn new(kafka_client: KafkaClient, consumer_configuration: ConsumerConfiguration) -> Self {
55
- KafkaConsumer {
76
+ pub fn new(
77
+ kafka_client: KafkaClient,
78
+ consumer_configuration: ConsumerConfiguration,
79
+ ) -> Result<Self> {
80
+ let producer = KafkaProducer::new(
81
+ kafka_client.get_client_config().clone(),
82
+ ProducerConfiguration {
83
+ configuration: None,
84
+ queue_timeout: None,
85
+ },
86
+ )?;
87
+
88
+ Ok(KafkaConsumer {
56
89
  kafka_client,
57
90
  consumer_configuration,
58
- }
91
+ producer,
92
+ })
59
93
  }
60
94
 
61
95
  #[napi(ts_args_type = "callback: (err: Error | null, result: Buffer) => Promise<ConsumerResult>")]
62
96
  pub async fn start_consumer(&self, func: ThreadsafeFunction<Buffer>) -> Result<()> {
63
- let ConsumerConfiguration { commit_mode, .. } = self.consumer_configuration.clone();
97
+ let ConsumerConfiguration {
98
+ commit_mode,
99
+ retry_strategy,
100
+ topic,
101
+ ..
102
+ } = self.consumer_configuration.clone();
64
103
 
65
104
  let commit_mode = match commit_mode {
66
105
  None | Some(KafkaCommitMode::AutoCommit) => None, //Default AutoCommit
@@ -68,40 +107,85 @@ impl KafkaConsumer {
68
107
  Some(KafkaCommitMode::Async) => Some(CommitMode::Async),
69
108
  };
70
109
 
110
+ let producer = self.producer.clone();
111
+
112
+ if let Some(strategy) = retry_strategy.clone() {
113
+ let retry_topic = strategy
114
+ .retry_topic
115
+ .unwrap_or(format!("{}-retry", topic.clone()));
116
+ let stream_consumer_retry: StreamConsumer<CustomContext> = self
117
+ .setup_consumer(retry_topic.clone())
118
+ .await
119
+ .expect("Consumer creation failed");
120
+ debug!(
121
+ "Stream consumer configured for retry topic: {}",
122
+ retry_topic
123
+ );
124
+ let (retry_topic, retry_func, retry_producer) =
125
+ (topic.clone(), func.clone(), producer.clone());
126
+ tokio::spawn(async move {
127
+ start_consumer_loop(
128
+ stream_consumer_retry,
129
+ retry_func,
130
+ commit_mode,
131
+ strategy.retries,
132
+ &strategy.dql_topic.unwrap_or(format!("{}-dql", retry_topic)),
133
+ retry_producer,
134
+ )
135
+ .await
136
+ });
137
+ };
138
+
71
139
  let stream_consumer: StreamConsumer<CustomContext> = self
72
- .setup_consumer()
140
+ .setup_consumer(topic.clone())
73
141
  .await
74
142
  .expect("Consumer creation failed");
143
+ debug!("Stream consumer configured for topic: {}", topic);
144
+ let next_topic_on_fail = setup_next_topic(
145
+ self.consumer_configuration.topic.clone(),
146
+ retry_strategy.clone(),
147
+ );
75
148
 
76
149
  tokio::spawn(async move {
77
- loop {
78
- match stream_consumer.recv().await {
79
- Err(e) => error!("Error while receiving from stream consumer: {:?}", e),
80
- Ok(message) => process_message(commit_mode, &stream_consumer, &func, &message).await,
81
- };
82
- }
150
+ start_consumer_loop(
151
+ stream_consumer,
152
+ func,
153
+ commit_mode,
154
+ 0,
155
+ &next_topic_on_fail,
156
+ producer,
157
+ )
158
+ .await
83
159
  });
84
160
 
85
161
  Ok(())
86
162
  }
87
163
 
88
- async fn setup_consumer(&self) -> anyhow::Result<StreamConsumer<CustomContext>> {
164
+ async fn setup_consumer(&self, topic: String) -> anyhow::Result<StreamConsumer<CustomContext>> {
89
165
  let context = CustomContext;
90
166
 
91
167
  let ConsumerConfiguration {
92
- topic,
93
168
  offset,
94
169
  create_topic,
95
170
  group_id,
171
+ configuration,
172
+ enable_auto_commit,
96
173
  ..
97
174
  } = self.consumer_configuration.clone();
98
175
 
99
- let consumer: LoggingConsumer = self
100
- .kafka_client
101
- .get_client_config()
176
+ let mut consumer_config: ClientConfig = self.kafka_client.get_client_config().clone();
177
+
178
+ if let Some(config) = configuration {
179
+ consumer_config.extend(config);
180
+ }
181
+
182
+ let consumer: LoggingConsumer = consumer_config
102
183
  .clone()
103
184
  .set("group.id", group_id.clone())
104
- .set("enable.auto.commit", "false")
185
+ .set(
186
+ "enable.auto.commit",
187
+ enable_auto_commit.unwrap_or(true).to_string(),
188
+ )
105
189
  .set_log_level(RDKafkaLogLevel::Debug)
106
190
  .create_with_context(context)
107
191
  .expect("Consumer creation failed");
@@ -140,49 +224,172 @@ impl KafkaConsumer {
140
224
  }
141
225
  }
142
226
 
143
- fn convert_to_rdkafka_offset(offset_model: Option<OffsetModel>) -> Option<Offset> {
144
- match offset_model {
145
- Some(model) => Some(match model.position {
146
- Some(PartitionPosition::Beginning) => Offset::Beginning,
147
- Some(PartitionPosition::End) => Offset::End,
148
- Some(PartitionPosition::Stored) => Offset::Stored,
149
- None => match model.offset {
150
- Some(value) => Offset::Offset(value),
151
- None => Offset::Stored, // Default to stored
227
+ async fn start_consumer_loop(
228
+ stream_consumer: StreamConsumer<CustomContext>,
229
+ func: ThreadsafeFunction<Buffer>,
230
+ commit_mode: Option<CommitMode>,
231
+ retries: i32,
232
+ next_topic_on_fail: &str,
233
+ producer: KafkaProducer,
234
+ ) {
235
+ loop {
236
+ match stream_consumer.recv().await {
237
+ Err(e) => error!("Error while receiving from stream consumer: {:?}", e),
238
+ Ok(message) => {
239
+ let message_result = process_message(&commit_mode, &stream_consumer, &func, &message).await;
240
+ match handle_message_result(
241
+ retries,
242
+ next_topic_on_fail,
243
+ &producer,
244
+ &message,
245
+ message_result,
246
+ )
247
+ .await
248
+ {
249
+ Ok(_) => {
250
+ debug!(
251
+ "Message consumed successfully. Partition: {}, Offset: {}",
252
+ message.partition(),
253
+ message.offset()
254
+ );
255
+ }
256
+ Err(_) => {
257
+ error!("Message processing failed")
258
+ }
259
+ }
260
+ }
261
+ };
262
+ }
263
+ }
264
+
265
+ async fn handle_message_result(
266
+ retries: i32,
267
+ next_topic_on_fail: &str,
268
+ producer: &KafkaProducer,
269
+ message: &BorrowedMessage<'_>,
270
+ message_result: anyhow::Result<Option<ConsumerResult>>,
271
+ ) -> anyhow::Result<()> {
272
+ match message_result {
273
+ Ok(consumer_result) => match consumer_result {
274
+ Some(result) => match result {
275
+ ConsumerResult::Ok => Ok(()),
276
+ ConsumerResult::Retry => {
277
+ match send_to_retry_strategy(producer, message, retries, next_topic_on_fail).await {
278
+ Ok(_) => {
279
+ debug!("Message sent to retry strategy");
280
+ Ok(())
281
+ }
282
+ Err(e) => {
283
+ error!("Error while sending message to retry strategy: {:?}", e);
284
+ Err(e)
285
+ }
286
+ }
287
+ }
152
288
  },
153
- }),
154
- None => None,
289
+ None => Ok(()),
290
+ },
291
+ Err(err) => {
292
+ error!("Error while processing message: {:?}", err);
293
+ Err(anyhow::Error::msg("Producer not found"))
294
+ }
155
295
  }
156
296
  }
157
297
 
158
298
  async fn process_message(
159
- commit_mode: Option<CommitMode>,
299
+ commit_mode: &Option<CommitMode>,
160
300
  stream_consumer: &StreamConsumer<CustomContext>,
161
301
  func: &ThreadsafeFunction<Buffer>,
162
302
  message: &BorrowedMessage<'_>,
163
- ) {
303
+ ) -> anyhow::Result<Option<ConsumerResult>> {
164
304
  match message.payload_view::<[u8]>() {
165
- None => {}
305
+ None => Ok(None),
166
306
  Some(Ok(payload)) => match func
167
307
  .call_async::<Promise<ConsumerResult>>(Ok(payload.into()))
168
308
  .await
169
309
  {
170
310
  Ok(js_result) => match js_result.await {
171
- Ok(value) => {
172
- debug!("Message processed: {:?}", value);
173
- if let Some(commit_mode) = commit_mode {
174
- match stream_consumer.commit_message(&message, commit_mode) {
175
- Ok(_) => debug!("Message committed"),
176
- Err(e) => error!("Error on commit: {:?}", e),
311
+ Ok(value) => match value {
312
+ ConsumerResult::Ok => {
313
+ if let Some(commit_mode) = commit_mode {
314
+ match stream_consumer.commit_message(message, *commit_mode) {
315
+ Ok(_) => Ok(Some(value)),
316
+ Err(e) => Err(anyhow::Error::new(e)),
317
+ }
318
+ } else {
319
+ Ok(Some(value))
177
320
  }
178
321
  }
179
- }
180
- Err(e) => error!("Error on promise: {:?}", e),
322
+ ConsumerResult::Retry => Ok(Some(value)),
323
+ },
324
+ Err(e) => Err(anyhow::Error::new(e)),
181
325
  },
182
- Err(js_error) => error!("Error on js call: {:?}", js_error),
326
+ Err(js_error) => Err(anyhow::Error::new(js_error)),
183
327
  },
184
- Some(Err(e)) => {
185
- error!("Error while deserializing message payload: {:?}", e);
328
+ Some(_) => Err(anyhow::Error::msg(
329
+ "Error while deserializing message payload",
330
+ )),
331
+ }
332
+ }
333
+
334
+ async fn send_to_retry_strategy(
335
+ producer: &KafkaProducer,
336
+ message: &BorrowedMessage<'_>,
337
+ retries: i32,
338
+ next_topic_on_fail: &str,
339
+ ) -> anyhow::Result<()> {
340
+ let retry_counter: usize = {
341
+ let headers_map = kakfa_headers_to_hashmap(message.headers());
342
+ let mut counter = headers_map.get_value(RETRY_COUNTER_NAME).unwrap_or(1);
343
+ if retries > 0 {
344
+ counter += 1;
345
+ }
346
+ counter
347
+ };
348
+
349
+ let next_topic = if retries > 0 && retry_counter <= retries.try_into().unwrap() {
350
+ debug!(
351
+ "Send message to same topic. Retry counter: {}",
352
+ retry_counter
353
+ );
354
+ message.topic()
355
+ } else {
356
+ debug!(
357
+ "Send message to next topic: {}. Retry counter {}",
358
+ next_topic_on_fail, retry_counter
359
+ );
360
+ next_topic_on_fail
361
+ };
362
+ let new_headers = message
363
+ .headers()
364
+ .unwrap_or(OwnedHeaders::new().as_borrowed())
365
+ .detach()
366
+ .insert(Header {
367
+ key: RETRY_COUNTER_NAME,
368
+ value: Some(retry_counter.to_string().as_str()),
369
+ });
370
+ let key = message.key().unwrap_or(&[]);
371
+
372
+ let record = FutureRecord::to(next_topic)
373
+ .key(key)
374
+ .headers(new_headers)
375
+ .payload(message.payload().unwrap_or(&[]));
376
+
377
+ let _result = producer.raw_send(record).await;
378
+ info!("Message sent to topic: {:?}", next_topic);
379
+ Ok(())
380
+ }
381
+
382
+ fn setup_next_topic(main_topic: String, retry_strategy: Option<RetryStrategy>) -> String {
383
+ match retry_strategy {
384
+ Some(strategy) => {
385
+ if (strategy.retries) > 0 {
386
+ strategy
387
+ .retry_topic
388
+ .unwrap_or(format!("{}-retry", main_topic))
389
+ } else {
390
+ strategy.dql_topic.unwrap_or(format!("{}-dlq", main_topic))
391
+ }
186
392
  }
393
+ None => format!("{}-dlq", main_topic),
187
394
  }
188
395
  }
@@ -1,57 +1,107 @@
1
- use std::time::Duration;
1
+ use std::{collections::HashMap, time::Duration};
2
2
 
3
3
  use napi::{Error, Result, Status};
4
4
  use rdkafka::{
5
- message::ToBytes,
5
+ message::{OwnedHeaders, ToBytes},
6
6
  producer::{FutureProducer, FutureRecord},
7
7
  ClientConfig,
8
8
  };
9
+ use tracing::{debug, warn};
9
10
 
10
- use super::model::MessageModel;
11
+ use super::{
12
+ kafka_util::hashmap_to_kafka_headers,
13
+ model::{MessageModel, OwnedDelivery},
14
+ };
15
+
16
+ const DEFAULT_QUEUE_TIMEOUT: i64 = 5000;
17
+
18
+ #[napi(object)]
19
+ #[derive(Clone, Debug)]
20
+ pub struct ProducerConfiguration {
21
+ pub queue_timeout: Option<i64>,
22
+ pub configuration: Option<HashMap<String, String>>,
23
+ }
11
24
 
12
25
  #[derive(Clone)]
13
26
  #[napi]
14
27
  pub struct KafkaProducer {
15
28
  future_producer: FutureProducer,
16
- }
17
-
18
- #[napi(object)]
19
- pub struct OwnedDelivery {
20
- pub partition: i32,
21
- pub offset: i64,
29
+ queue_timeout: Duration,
22
30
  }
23
31
 
24
32
  #[napi]
25
33
  impl KafkaProducer {
26
- pub fn new(client_config: ClientConfig) -> Self {
27
- let future_producer = client_config
28
- .clone()
29
- .set("message.timeout.ms", "5000")
34
+ pub fn new(
35
+ client_config: ClientConfig,
36
+ producer_configuration: ProducerConfiguration,
37
+ ) -> Result<Self> {
38
+ let mut future_producer_config = client_config;
39
+
40
+ if let Some(config) = producer_configuration.configuration {
41
+ future_producer_config.extend(config);
42
+ }
43
+
44
+ let future_producer = future_producer_config
30
45
  .create::<FutureProducer>()
31
- .expect("Producer creation error");
46
+ .map_err(|e| {
47
+ warn!("Failed to create Kafka producer. Error: {:?}", e);
48
+ Error::new(Status::GenericFailure, e)
49
+ })?;
50
+
51
+ let queue_timeout = Duration::from_millis(
52
+ producer_configuration
53
+ .queue_timeout
54
+ .unwrap_or(DEFAULT_QUEUE_TIMEOUT)
55
+ .try_into()
56
+ .map_err(|e| Error::new(Status::GenericFailure, e))?,
57
+ );
32
58
 
33
- KafkaProducer { future_producer }
59
+ Ok(KafkaProducer {
60
+ future_producer,
61
+ queue_timeout,
62
+ })
34
63
  }
35
64
 
36
65
  #[napi]
37
- pub async fn send(&self, topic: String, message: MessageModel) -> Result<OwnedDelivery> {
66
+ pub async fn send(&self, message: MessageModel) -> Result<OwnedDelivery> {
38
67
  let MessageModel {
68
+ topic,
39
69
  key,
40
70
  value,
41
- headers: _,
71
+ headers,
42
72
  } = message;
43
73
 
74
+ let kafka_headers = match headers {
75
+ Some(v) => hashmap_to_kafka_headers(&v),
76
+ None => OwnedHeaders::new(),
77
+ };
78
+
44
79
  let record = FutureRecord::to(&topic)
45
80
  .key(key.to_bytes())
81
+ .headers(kafka_headers)
46
82
  .payload(value.to_bytes());
47
83
 
48
- match self
49
- .future_producer
50
- .send(record, Duration::from_secs(5000))
51
- .await
52
- {
84
+ match self.raw_send(record).await {
53
85
  Ok((partition, offset)) => Ok(OwnedDelivery { partition, offset }),
54
- Err((kafka_error, _)) => Err(Error::new(Status::GenericFailure, kafka_error)),
86
+ Err(e) => Err(Error::new(Status::GenericFailure, e)),
55
87
  }
56
88
  }
89
+
90
+ pub async fn raw_send<'a, K: ToBytes + ?Sized, P: ToBytes + ?Sized>(
91
+ &self,
92
+ record: FutureRecord<'a, K, P>,
93
+ ) -> anyhow::Result<(i32, i64)> {
94
+ debug!("Trying to produce message. Topic {}", record.topic);
95
+ let producer: &FutureProducer = &self.future_producer;
96
+
97
+ producer
98
+ .send(record, self.queue_timeout)
99
+ .await
100
+ .map_err(|(k, m)| {
101
+ anyhow::Error::msg(format!(
102
+ "Failed to send message to Kafka. Error: {:?}. Message: {:?}",
103
+ k, m
104
+ ))
105
+ })
106
+ }
57
107
  }
@@ -1,8 +1,36 @@
1
1
  use std::collections::HashMap;
2
2
 
3
- use rdkafka::message::{BorrowedHeaders, Headers};
3
+ use napi::bindgen_prelude::Buffer;
4
+ use rdkafka::{
5
+ message::{BorrowedHeaders, Header, Headers, OwnedHeaders},
6
+ Offset,
7
+ };
4
8
 
5
- pub fn convert_kakfa_headers_to_hashmap(headers: Option<&BorrowedHeaders>) -> HashMap<&str, &[u8]> {
9
+ use super::model::{OffsetModel, PartitionPosition};
10
+
11
+ pub fn convert_to_rdkafka_offset(offset_model: Option<OffsetModel>) -> Option<Offset> {
12
+ offset_model.map(|model| match model.position {
13
+ Some(PartitionPosition::Beginning) => Offset::Beginning,
14
+ Some(PartitionPosition::End) => Offset::End,
15
+ Some(PartitionPosition::Stored) => Offset::Stored,
16
+ None => match model.offset {
17
+ Some(value) => Offset::Offset(value),
18
+ None => Offset::Stored, // Default to stored
19
+ },
20
+ })
21
+ }
22
+
23
+ pub fn hashmap_to_kafka_headers(map: &HashMap<String, Buffer>) -> OwnedHeaders {
24
+ map.iter().fold(OwnedHeaders::new(), |acc, (key, value)| {
25
+ let value: &[u8] = value.as_ref();
26
+ acc.insert(Header {
27
+ key,
28
+ value: Some(value),
29
+ })
30
+ })
31
+ }
32
+
33
+ pub fn kakfa_headers_to_hashmap(headers: Option<&BorrowedHeaders>) -> HashMap<&str, &[u8]> {
6
34
  match headers {
7
35
  Some(value) => value
8
36
  .iter()
@@ -39,29 +67,27 @@ impl ExtractValueOnKafkaHashMap<usize> for HashMap<&str, &[u8]> {
39
67
  mod tests {
40
68
  use std::collections::HashMap;
41
69
 
42
- use rdkafka::message::{Header, OwnedHeaders};
70
+ use napi::bindgen_prelude::Buffer;
71
+ use rdkafka::message::Headers;
43
72
 
44
- use crate::kafka::kafka_util::{convert_kakfa_headers_to_hashmap, ExtractValueOnKafkaHashMap};
45
-
46
- fn create_header<'a>(key: &'a str, value: Option<&'a str>) -> Header<'a, &'a str> {
47
- Header { key, value }
48
- }
73
+ use crate::kafka::kafka_util::{
74
+ hashmap_to_kafka_headers, kakfa_headers_to_hashmap, ExtractValueOnKafkaHashMap,
75
+ };
49
76
 
50
77
  #[test]
51
- fn filter_out_nones() {
52
- let _x = Header {
53
- key: "header_key",
54
- value: Some("header_value"),
55
- };
56
- let headers = OwnedHeaders::new()
57
- .insert(create_header("key1", Some("1")))
58
- .insert(create_header("key2", None))
59
- .insert(create_header("key4", None))
60
- .insert(create_header("key5", Some("5")));
61
-
62
- let hash_map = convert_kakfa_headers_to_hashmap(Some(headers.as_borrowed()));
63
-
64
- assert_eq!(hash_map.len(), 2);
78
+ fn headers_test() {
79
+ let hash_map: HashMap<String, Buffer> = HashMap::from([
80
+ ("key_a".to_owned(), "A".as_bytes().into()),
81
+ ("key_b".to_owned(), "B".as_bytes().into()),
82
+ ]);
83
+
84
+ let rd_headers = hashmap_to_kafka_headers(&hash_map);
85
+
86
+ dbg!(rd_headers.get(0));
87
+ dbg!(rd_headers.get(1));
88
+
89
+ let result = kakfa_headers_to_hashmap(Some(rd_headers.as_borrowed()));
90
+ assert_eq!(result.len(), 2);
65
91
  }
66
92
 
67
93
  #[test]
@@ -7,6 +7,7 @@ use napi::bindgen_prelude::Buffer;
7
7
  #[napi(object)]
8
8
  #[derive(Clone)]
9
9
  pub struct MessageModel {
10
+ pub topic: String,
10
11
  pub key: Buffer,
11
12
  pub value: Buffer,
12
13
  pub headers: Option<HashMap<String, Buffer>>,
@@ -38,7 +39,7 @@ impl fmt::Display for AutoOffsetReset {
38
39
  }
39
40
 
40
41
  #[napi(string_enum)]
41
- #[derive(Debug)]
42
+ #[derive(Debug, PartialEq)]
42
43
  pub enum KafkaCommitMode {
43
44
  AutoCommit,
44
45
  Sync,
@@ -46,14 +47,9 @@ pub enum KafkaCommitMode {
46
47
  }
47
48
 
48
49
  #[napi(object)]
49
- #[derive(Clone, Debug)]
50
- pub struct ConsumerConfiguration {
51
- pub topic: String,
52
- pub group_id: String,
53
- pub retry_strategy: Option<RetryStrategy>,
54
- pub offset: Option<OffsetModel>,
55
- pub create_topic: Option<bool>,
56
- pub commit_mode: Option<KafkaCommitMode>,
50
+ pub struct OwnedDelivery {
51
+ pub partition: i32,
52
+ pub offset: i64,
57
53
  }
58
54
 
59
55
  #[napi(object)]
@@ -74,7 +70,8 @@ pub struct OffsetModel {
74
70
  #[derive(Clone, Debug)]
75
71
  pub struct RetryStrategy {
76
72
  pub retries: i32,
77
- pub next_topic_on_fail: String,
73
+ pub retry_topic: Option<String>,
74
+ pub dql_topic: Option<String>,
78
75
  pub pause_consumer_duration: Option<i64>,
79
76
  }
80
77