1use async_trait::async_trait;
3use ecat_circuit_breaker::{Breaker, BreakerConfig};
4use ecat_data::{
5 BackendKind, DataPoint, FieldValue, TsdbClient, breaker_error_to_backend_error,
6 run_with_timeout,
7};
8use ecat_errors::{Error, ErrorCode};
9use ecat_tls::TlsClientConfig;
10use serde::Deserialize;
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::sync::{Semaphore, SemaphorePermit};
14
15#[cfg(feature = "metrics")]
16mod metrics;
17#[cfg(feature = "metrics")]
18pub use metrics::register_outbound_metrics;
19
20#[derive(Debug, Clone, Deserialize)]
21pub struct IotdbConfig {
22 pub base_url: String,
23 pub username: String,
24 pub password: String,
25 #[serde(default)]
26 pub tls: Option<TlsClientConfig>,
27 #[serde(default)]
32 pub query_timeout_secs: Option<u64>,
33 #[serde(default)]
40 pub breaker: Option<BreakerConfig>,
41 #[serde(default)]
47 pub max_concurrency: Option<usize>,
48}
49
50pub struct IotdbClient {
51 client: reqwest::Client,
52 base_url: String,
53 username: String,
54 password: String,
55 query_timeout: Option<Duration>,
56 breaker: Arc<Breaker>,
58 semaphore: Option<Arc<Semaphore>>,
60}
61
62impl IotdbClient {
63 pub fn new(
64 base_url: impl Into<String>,
65 username: impl Into<String>,
66 password: impl Into<String>,
67 ) -> Self {
68 Self {
69 client: reqwest::Client::new(),
70 base_url: base_url.into(),
71 username: username.into(),
72 password: password.into(),
73 query_timeout: query_timeout(None),
74 breaker: Arc::new(Breaker::new(BreakerConfig::default())),
75 semaphore: Some(Arc::new(Semaphore::new(32))),
76 }
77 }
78
79 pub fn from_config(cfg: IotdbConfig) -> Result<Self, Error> {
80 let client = ecat_tls::build_reqwest_client(&cfg.tls)
81 .map_err(|e| Error::new(ErrorCode::Internal, "iotdb", format!("TLS: {e}")))?;
82 let breaker = Arc::new(Breaker::new(cfg.breaker.unwrap_or_default()));
83 #[cfg(feature = "metrics")]
88 crate::register_outbound_metrics(Arc::clone(&breaker));
89 Ok(Self {
90 client,
91 base_url: cfg.base_url,
92 username: cfg.username,
93 password: cfg.password,
94 query_timeout: query_timeout(cfg.query_timeout_secs),
95 breaker,
96 semaphore: match cfg.max_concurrency {
97 Some(0) => None,
101 Some(n) => Some(Arc::new(Semaphore::new(n))),
102 None => Some(Arc::new(Semaphore::new(32))),
103 },
104 })
105 }
106
107 pub fn breaker(&self) -> Arc<Breaker> {
109 Arc::clone(&self.breaker)
110 }
111
112 async fn permit(&self) -> Option<SemaphorePermit<'_>> {
115 match &self.semaphore {
116 Some(sem) => Some(sem.acquire().await.expect("semaphore is never closed")),
117 None => None,
118 }
119 }
120
121 async fn guarded<F, T: 'static>(&self, fut: F) -> Result<T, Error>
139 where
140 F: std::future::Future<Output = Result<T, Error>> + Send,
141 {
142 let _permit = self.permit().await;
143 self.breaker
144 .call(|| run_with_timeout(BackendKind::Tsdb, self.query_timeout, fut))
145 .await
146 .map_err(|e| breaker_error_to_backend_error(e, "iotdb"))
147 }
148}
149
150fn query_timeout(secs: Option<u64>) -> Option<Duration> {
152 match secs {
153 None => Some(Duration::from_secs(30)),
154 Some(0) => None,
155 Some(s) => Some(Duration::from_secs(s)),
156 }
157}
158
159#[async_trait]
160impl TsdbClient for IotdbClient {
161 async fn write(&self, points: &[DataPoint]) -> Result<(), Error> {
162 self.guarded(async {
167 for p in points {
168 let mut measurements = Vec::with_capacity(p.fields.len());
173 let mut data_types = Vec::with_capacity(p.fields.len());
174 let mut values: Vec<serde_json::Value> = Vec::with_capacity(p.fields.len());
175 for (k, v) in &p.fields {
176 measurements.push(k.clone());
177 let (dt, val) = match v {
178 FieldValue::Float(f) => (
179 "DOUBLE",
180 serde_json::Value::Number(
181 serde_json::Number::from_f64(*f).unwrap_or(0.into()),
182 ),
183 ),
184 FieldValue::Int(i) => ("INT64", serde_json::Value::Number((*i).into())),
185 FieldValue::String(s) => ("TEXT", serde_json::Value::String(s.clone())),
186 FieldValue::Bool(b) => ("BOOLEAN", serde_json::Value::Bool(*b)),
187 };
188 data_types.push(dt);
189 values.push(val);
190 }
191 let body = serde_json::json!({
192 "device": p.measurement,
193 "is_aligned": false,
194 "timestamps": [p.timestamp.unwrap_or(0)],
195 "measurements": measurements,
196 "data_types": data_types,
197 "values": [values],
198 });
199 let resp = self
200 .client
201 .post(format!("{}/rest/v2/insertTablet", self.base_url))
202 .basic_auth(&self.username, Some(&self.password))
203 .header("Content-Type", "application/json")
204 .json(&body)
205 .send()
206 .await
207 .map_err(|e| {
208 Error::new(ErrorCode::Internal, "iotdb", format!("iotdb write: {e}"))
209 })?;
210 if !resp.status().is_success() {
211 return Err(Error::new(
212 ErrorCode::Internal,
213 "iotdb",
214 resp.text().await.unwrap_or_default(),
215 ));
216 }
217 if let Ok(v) = resp.json::<serde_json::Value>().await
220 && let Some(code) = v.get("code").and_then(|c| c.as_i64())
221 && code != 200
222 {
223 return Err(Error::new(
224 ErrorCode::Internal,
225 "iotdb",
226 format!(
227 "iotdb write failed: code {code}: {}",
228 v.get("message")
229 .and_then(|m| m.as_str())
230 .unwrap_or("no message")
231 ),
232 ));
233 }
234 }
235 Ok(())
236 })
237 .await
238 }
239
240 async fn query(&self, sql: &str) -> Result<serde_json::Value, Error> {
241 self.guarded(async {
242 let resp = self
243 .client
244 .post(format!("{}/rest/v2/query", self.base_url))
245 .basic_auth(&self.username, Some(&self.password))
246 .header("Content-Type", "text/plain; charset=utf-8")
247 .body(sql.to_string())
248 .send()
249 .await
250 .map_err(|e| {
251 Error::new(ErrorCode::Internal, "iotdb", format!("iotdb query: {e}"))
252 })?;
253 if !resp.status().is_success() {
254 return Err(Error::new(
255 ErrorCode::Internal,
256 "iotdb",
257 resp.text().await.unwrap_or_default(),
258 ));
259 }
260 resp.json()
261 .await
262 .map_err(|e| Error::new(ErrorCode::Internal, "iotdb", format!("iotdb parse: {e}")))
263 })
264 .await
265 }
266
267 }
272
273#[cfg(test)]
274mod tests;