Skip to content

Commit 9032be8

Browse files
authored
Add ability to configure topic producer in node client (#567)
* fix(ci): use ubuntu 22 for compatibility * feat: make topic producer configurable in node client * feat: expose use_spu_local_address in node client
1 parent 1b3dbb4 commit 9032be8

4 files changed

Lines changed: 155 additions & 5 deletions

File tree

.github/workflows/publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ jobs:
1212
runs-on: ${{ matrix.os }}
1313
strategy:
1414
matrix:
15-
os: [ubuntu-latest]
16-
node: ["18.13"]
15+
os: [ubuntu-22.04]
16+
node: ["20"]
1717
target: [x86_64-unknown-linux-gnu]
1818
include:
1919
- os: macos-latest
2020
target: x86_64-apple-darwin
2121
- os: macos-latest
2222
target: aarch64-apple-darwin
23-
- os: ubuntu-latest
23+
- os: ubuntu-22.04
2424
target: x86_64-unknown-linux-gnu
2525
steps:
2626
- uses: actions/cache@v4

src/connect.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@ use crate::error::FluvioErrorJS;
55
use crate::fluvio::FluvioJS;
66

77
#[node_bindgen()]
8-
async fn connect(host_addr: Option<String>) -> Result<FluvioJS, FluvioErrorJS> {
8+
async fn connect(
9+
host_addr: Option<String>,
10+
use_spu_local_address: Option<bool>,
11+
) -> Result<FluvioJS, FluvioErrorJS> {
912
match host_addr {
1013
Some(host) => {
11-
let config = FluvioConfig::new(host);
14+
let mut config = FluvioConfig::new(host);
15+
16+
if let Some(use_spu_local_address) = use_spu_local_address {
17+
config.use_spu_local_address = use_spu_local_address;
18+
}
19+
1220
let socket = Fluvio::connect_with_config(&config).await?;
1321
Ok(FluvioJS::from(socket))
1422
}

src/fluvio.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
use std::time::Duration;
2+
13
use crate::CLIENT_NOT_FOUND_ERROR_MSG;
24
use crate::admin::FluvioAdminJS;
35
use crate::consumer::PartitionConsumerJS;
46
use crate::producer::TopicProducerJS;
57
use crate::error::FluvioErrorJS;
68

9+
use fluvio::TopicProducerConfig;
10+
use fluvio::Compression;
11+
use fluvio::TopicProducerConfigBuilder;
12+
use node_bindgen::core::JSValue;
713
use tracing::debug;
814

915
use fluvio::Fluvio;
@@ -14,6 +20,9 @@ use node_bindgen::core::NjError;
1420
use node_bindgen::core::val::JsEnv;
1521
use node_bindgen::sys::napi_value;
1622
use node_bindgen::core::JSClass;
23+
use node_bindgen::core::val::JsObject;
24+
25+
use crate::optional_property;
1726

1827
impl From<Fluvio> for FluvioJS {
1928
fn from(inner: Fluvio) -> Self {
@@ -82,4 +91,83 @@ impl FluvioJS {
8291
Err(FluvioErrorJS::new(CLIENT_NOT_FOUND_ERROR_MSG.to_owned()))
8392
}
8493
}
94+
95+
#[node_bindgen]
96+
async fn topic_producer_with_config(
97+
&mut self,
98+
topic: String,
99+
config: TopicProducerConfigWrapper,
100+
) -> Result<TopicProducerJS, FluvioErrorJS> {
101+
let config = config.inner;
102+
103+
if let Some(client) = &mut self.inner {
104+
Ok(TopicProducerJS::from(
105+
client.topic_producer_with_config(topic, config).await?,
106+
))
107+
} else {
108+
Err(FluvioErrorJS::new(CLIENT_NOT_FOUND_ERROR_MSG.to_owned()))
109+
}
110+
}
111+
}
112+
113+
pub struct TopicProducerConfigWrapper {
114+
pub inner: TopicProducerConfig,
115+
}
116+
117+
impl JSValue<'_> for TopicProducerConfigWrapper {
118+
fn convert_to_rust(env: &JsEnv, js_value: napi_value) -> Result<Self, NjError> {
119+
debug!("converting topic producer config from JS");
120+
if let Ok(js_obj) = env.convert_to_rust::<JsObject>(js_value) {
121+
let mut builder = TopicProducerConfigBuilder::default();
122+
123+
// Extract compression if provided
124+
if let Some(compression_str) = optional_property!("compression", String, js_obj) {
125+
let compression = match compression_str.as_str() {
126+
"none" => Compression::None,
127+
"gzip" => Compression::Gzip,
128+
"snappy" => Compression::Snappy,
129+
"lz4" => Compression::Lz4,
130+
"zstd" => Compression::Zstd,
131+
_ => {
132+
return Err(NjError::Other(format!(
133+
"Invalid compression type: {}",
134+
compression_str
135+
)))
136+
}
137+
};
138+
builder.compression(compression);
139+
}
140+
141+
// Extract max_request_size if provided
142+
if let Some(max_request_size) = optional_property!("maxRequestSize", i64, js_obj) {
143+
builder.max_request_size(max_request_size as usize);
144+
}
145+
146+
// Extract batch_size if provided
147+
if let Some(batch_size) = optional_property!("batchSize", i64, js_obj) {
148+
builder.batch_size(batch_size as usize);
149+
}
150+
151+
// Extract batch_queue_size if provided
152+
if let Some(batch_queue_size) = optional_property!("batchQueueSize", i64, js_obj) {
153+
builder.batch_queue_size(batch_queue_size as usize);
154+
}
155+
156+
// Extract linger if provided (in milliseconds)
157+
if let Some(linger_ms) = optional_property!("lingerMs", i64, js_obj) {
158+
builder.linger(Duration::from_millis(linger_ms as u64));
159+
}
160+
161+
// Build the config
162+
match builder.build() {
163+
Ok(config) => Ok(Self { inner: config }),
164+
Err(err) => Err(NjError::Other(format!(
165+
"Failed to build TopicProducerConfig: {}",
166+
err
167+
))),
168+
}
169+
} else {
170+
Err(NjError::Other("parameter must be a JSON object".to_owned()))
171+
}
172+
}
85173
}

src/index.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,38 @@ export interface Options {
7575
id?: number
7676
offsetIndex?: number
7777
offsetFrom?: string
78+
use_spu_local_address?: boolean
79+
}
80+
81+
/**
82+
* Configuration options for a topic producer
83+
*/
84+
export interface TopicProducerConfig {
85+
/**
86+
* Maximum size of a batch before it's sent automatically (in bytes)
87+
*/
88+
batchSize?: number
89+
/**
90+
* Maximum time in milliseconds to wait before sending a batch, even if not full
91+
*/
92+
timeoutMs?: number
93+
/**
94+
* Maximum number of records to include in a batch
95+
*/
96+
batchQueueSize?: number
97+
/**
98+
* Time in milliseconds to wait for additional records before sending a batch
99+
*/
100+
lingerMs?: number
101+
/**
102+
* Compression algorithm to use for batches
103+
* Supported values: "none", "gzip", "snappy", "lz4"
104+
*/
105+
compression?: 'none' | 'gzip' | 'snappy' | 'lz4'
106+
/**
107+
* Maximum size of a single request in bytes
108+
*/
109+
maxRequestSize?: number
78110
}
79111

80112
/**
@@ -525,6 +557,10 @@ export interface FluvioClient {
525557
connect(options?: Partial<Options>): Promise<FluvioClient>
526558
// connectWithConfig(config: any): Promise<this>
527559
topicProducer(topic: string): Promise<TopicProducer>
560+
topicProducerWithConfig(
561+
topic: string,
562+
config: TopicProducerConfig
563+
): Promise<TopicProducer>
528564
partitionConsumer(
529565
topic: string,
530566
partition: number
@@ -680,6 +716,24 @@ export default class Fluvio implements FluvioClient {
680716
return TopicProducer.create(inner)
681717
}
682718

719+
/**
720+
* Creates a new TopicProducer for the given topic name with configuration
721+
*
722+
* @param {string} topic Name of the topic to create a producer for
723+
* @param {TopicProducerConfig} config Configuration for the topic producer
724+
*/
725+
async topicProducerWithConfig(
726+
topic: string,
727+
config: TopicProducerConfig
728+
): Promise<TopicProducer> {
729+
this.checkConnection()
730+
const inner = await this.client?.topicProducerWithConfig(topic, config)
731+
if (!inner) {
732+
throw new Error('Failed to create topic producer with config')
733+
}
734+
return TopicProducer.create(inner)
735+
}
736+
683737
/**
684738
*
685739
* Creates a new PartitionConsumer for the given topic and partition

0 commit comments

Comments
 (0)