risingwave_connector/sink/
nats.rs1use core::fmt::Debug;
16use core::future::IntoFuture;
17use std::collections::BTreeMap;
18use std::sync::Arc;
19use std::time::Duration;
20
21use anyhow::{Context as _, anyhow};
22use async_nats::jetstream::context::Context;
23use futures::FutureExt;
24use futures::prelude::TryFuture;
25use risingwave_common::array::StreamChunk;
26use risingwave_common::catalog::Schema;
27use risingwave_common::util::retry::exponential_backoff;
28use serde::Deserialize;
29use serde_with::serde_as;
30use tokio_retry::Retry;
31use tokio_retry::strategy::jitter;
32use with_options::WithOptions;
33
34use super::SinkWriterParam;
35use super::encoder::{
36 DateHandlingMode, JsonbHandlingMode, TimeHandlingMode, TimestamptzHandlingMode,
37};
38use super::utils::chunk_to_json;
39use crate::connector_common::NatsCommon;
40use crate::enforce_secret::EnforceSecret;
41use crate::sink::encoder::{JsonEncoder, TimestampHandlingMode};
42use crate::sink::log_store::DeliveryFutureManagerAddFuture;
43use crate::sink::writer::{
44 AsyncTruncateLogSinkerOf, AsyncTruncateSinkWriter, AsyncTruncateSinkWriterExt,
45};
46use crate::sink::{Result, SINK_TYPE_APPEND_ONLY, Sink, SinkError, SinkParam};
47
48pub const NATS_SINK: &str = "nats";
49const NATS_SEND_FUTURE_BUFFER_MAX_SIZE: usize = 65536;
50
51#[serde_as]
52#[derive(Clone, Debug, Deserialize, WithOptions)]
53pub struct NatsConfig {
54 #[serde(flatten)]
55 pub common: NatsCommon,
56 pub r#type: String,
58
59 #[serde(flatten)]
60 pub unknown_fields: std::collections::HashMap<String, String>,
61}
62
63crate::impl_sink_unknown_fields!(NatsConfig);
64
65#[derive(Clone, Debug)]
66pub struct NatsSink {
67 pub config: NatsConfig,
68 schema: Schema,
69 is_append_only: bool,
70}
71
72impl EnforceSecret for NatsSink {
73 fn enforce_secret<'a>(
74 prop_iter: impl Iterator<Item = &'a str>,
75 ) -> crate::error::ConnectorResult<()> {
76 for prop in prop_iter {
77 NatsCommon::enforce_one(prop)?;
78 }
79 Ok(())
80 }
81}
82
83pub struct NatsSinkWriter {
85 pub config: NatsConfig,
86 context: Context,
87 #[expect(dead_code)]
90 client: Arc<async_nats::Client>,
91 #[expect(dead_code)]
92 schema: Schema,
93 json_encoder: JsonEncoder,
94}
95
96pub type NatsSinkDeliveryFuture = impl TryFuture<Ok = (), Error = SinkError> + Unpin + 'static;
97
98impl NatsConfig {
100 pub fn from_btreemap(values: BTreeMap<String, String>) -> Result<Self> {
101 let config = serde_json::from_value::<NatsConfig>(serde_json::to_value(values).unwrap())
102 .map_err(|e| SinkError::Config(anyhow!(e)))?;
103 if config.r#type != SINK_TYPE_APPEND_ONLY {
104 Err(SinkError::Config(anyhow!(
105 "NATS sink only supports append-only mode"
106 )))
107 } else {
108 Ok(config)
109 }
110 }
111}
112
113impl TryFrom<SinkParam> for NatsSink {
114 type Error = SinkError;
115
116 fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
117 let schema = param.schema();
118 let config = NatsConfig::from_btreemap(param.properties)?;
119 Ok(Self {
120 config,
121 schema,
122 is_append_only: param.sink_type.is_append_only(),
123 })
124 }
125}
126
127impl Sink for NatsSink {
128 type LogSinker = AsyncTruncateLogSinkerOf<NatsSinkWriter>;
129
130 const SINK_NAME: &'static str = NATS_SINK;
131
132 crate::impl_validate_sink_unknown_fields!();
133
134 async fn validate(&self) -> Result<()> {
135 if !self.is_append_only {
136 return Err(SinkError::Nats(anyhow!(
137 "NATS sink only supports append-only mode"
138 )));
139 }
140 let _client = (self.config.common.build_client().await)
141 .context("validate nats sink error")
142 .map_err(SinkError::Nats)?;
143 Ok(())
144 }
145
146 async fn new_log_sinker(&self, _writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
147 Ok(
148 NatsSinkWriter::new(self.config.clone(), self.schema.clone())
149 .await?
150 .into_log_sinker(NATS_SEND_FUTURE_BUFFER_MAX_SIZE),
151 )
152 }
153}
154
155impl NatsSinkWriter {
156 pub async fn new(config: NatsConfig, schema: Schema) -> Result<Self> {
157 let client = config
158 .common
159 .build_client()
160 .await
161 .map_err(|e| SinkError::Nats(anyhow!(e)))?;
162 let context = NatsCommon::build_context_from_client(&client);
163 Ok::<_, SinkError>(Self {
164 config: config.clone(),
165 context,
166 client,
167 schema: schema.clone(),
168 json_encoder: JsonEncoder::new(
169 schema,
170 None,
171 DateHandlingMode::FromCe,
172 TimestampHandlingMode::Milli,
173 TimestamptzHandlingMode::UtcWithoutSuffix,
174 TimeHandlingMode::Milli,
175 JsonbHandlingMode::String,
176 ),
177 })
178 }
179}
180
181impl AsyncTruncateSinkWriter for NatsSinkWriter {
182 type DeliveryFuture = NatsSinkDeliveryFuture;
183
184 #[define_opaque(NatsSinkDeliveryFuture)]
185 async fn write_chunk<'a>(
186 &'a mut self,
187 chunk: StreamChunk,
188 mut add_future: DeliveryFutureManagerAddFuture<'a, Self::DeliveryFuture>,
189 ) -> Result<()> {
190 let mut data = chunk_to_json(chunk, &self.json_encoder)?;
191 for item in &mut data {
192 let publish_ack_future = Retry::spawn(
193 exponential_backoff(Duration::from_millis(100), 2, Duration::MAX)
194 .map(jitter)
195 .take(3),
196 || async {
197 self.context
198 .publish(self.config.common.subject.clone(), item.clone().into())
199 .await
200 .context("nats sink error")
201 .map_err(SinkError::Nats)
202 },
203 )
204 .await
205 .context("nats sink error")
206 .map_err(SinkError::Nats)?;
207 let future = publish_ack_future.into_future().map(|result| {
208 result
209 .context("Nats sink error")
210 .map_err(SinkError::Nats)
211 .map(|_| ())
212 });
213 add_future.add_future_may_await(future).await?;
214 }
215 Ok(())
216 }
217}