1use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex};
5
6use futures_util::StreamExt;
7use tokio::sync::watch;
8use tokio_util::sync::CancellationToken;
9use tracing::{debug, trace, warn};
10
11use crate::cache::{read_cache, write_cache};
12use crate::error::Error;
13use crate::http::fetch_config;
14use crate::nodes::RandomNodes;
15use crate::options::{ClientBuilder, ClientOptions};
16use crate::protocol::ConfigItem;
17use crate::source::Source;
18use crate::store::{ConfigSnapshot, empty_snapshot};
19use crate::websocket::{self, WsSink};
20
21#[derive(Clone, Debug)]
30pub struct Client {
31 inner: Arc<Inner>,
32}
33
34pub(crate) struct Inner {
35 pub(crate) options: ClientOptions,
36 http: reqwest::Client,
37 store: watch::Sender<Arc<ConfigSnapshot>>,
38 loaded: AtomicBool,
39 pub(crate) cancel: CancellationToken,
40 session: Mutex<CancellationToken>,
41 pub(crate) writer: tokio::sync::Mutex<Option<WsSink>>,
42 pub(crate) loops_started: AtomicBool,
43 pub(crate) reconnect_enabled: AtomicBool,
44}
45
46impl std::fmt::Debug for Inner {
47 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 formatter
49 .debug_struct("Inner")
50 .field("app_id", &self.options.app_id)
51 .field("loaded", &self.loaded)
52 .finish_non_exhaustive()
53 }
54}
55
56impl Drop for Inner {
57 fn drop(&mut self) {
58 self.reconnect_enabled.store(false, Ordering::SeqCst);
59 self.cancel.cancel();
60 }
61}
62
63impl Client {
64 pub fn new(options: ClientOptions) -> Result<Self, Error> {
71 let options = options.normalized()?;
72 let http = reqwest::Client::builder()
73 .timeout(options.http_timeout)
74 .build()?;
75 let (store, _) = watch::channel(empty_snapshot());
76 let cancel = CancellationToken::new();
77 let session = cancel.child_token();
78 session.cancel();
79 Ok(Self {
80 inner: Arc::new(Inner {
81 options,
82 http,
83 store,
84 loaded: AtomicBool::new(false),
85 cancel,
86 session: Mutex::new(session),
87 writer: tokio::sync::Mutex::new(None),
88 loops_started: AtomicBool::new(false),
89 reconnect_enabled: AtomicBool::new(false),
90 }),
91 })
92 }
93
94 pub fn builder() -> ClientBuilder {
96 ClientBuilder::default()
97 }
98
99 #[must_use]
101 pub fn options(&self) -> &ClientOptions {
102 &self.inner.options
103 }
104
105 #[must_use]
107 pub fn source(&self) -> Source {
108 Source::new(Arc::clone(&self.inner))
109 }
110
111 #[must_use]
113 pub fn snapshot(&self) -> Arc<ConfigSnapshot> {
114 self.inner.snapshot()
115 }
116
117 #[must_use]
123 pub fn subscribe(&self) -> watch::Receiver<Arc<ConfigSnapshot>> {
124 self.inner.store.subscribe()
125 }
126
127 pub async fn load(&self) -> Result<(), Error> {
133 self.inner.load().await
134 }
135
136 pub async fn connect(&self) -> Result<(), Error> {
147 self.inner.reconnect_enabled.store(true, Ordering::SeqCst);
148 websocket::spawn_background_loops(&self.inner);
149 if let Err(error) = self.inner.connect_websocket().await {
150 warn!(
151 ?error,
152 "websocket connect failed; continuing with HTTP load"
153 );
154 }
155 self.inner.load().await
156 }
157
158 pub async fn disconnect(&self) {
160 self.inner.disconnect().await;
161 }
162}
163
164impl Inner {
165 pub(crate) fn snapshot(&self) -> Arc<ConfigSnapshot> {
166 Arc::clone(&self.store.borrow())
167 }
168
169 pub(crate) async fn ensure_loaded(&self) -> Result<(), Error> {
170 if self.loaded.load(Ordering::SeqCst) {
171 return Ok(());
172 }
173 self.load().await
174 }
175
176 pub(crate) async fn load(&self) -> Result<(), Error> {
177 let mut last_error: Option<Error> = None;
178 for node in RandomNodes::new(&self.options.nodes) {
179 match fetch_config(&self.http, &node, &self.options).await {
180 Ok(payload) => match parse_items(&payload.json) {
181 Ok(items) => {
182 self.apply_snapshot(items, payload.publish_time_line_id, false);
183 if let Err(error) = write_cache(
184 &self.options.cache,
185 &self.options.app_id,
186 &self.options.secret,
187 &payload.json,
188 ) {
189 warn!(
190 ?error,
191 "client try to cache all configs to local but failed"
192 );
193 }
194 if let Err(error) = websocket::send_text(self, "loaded").await {
195 debug!(?error, "client try to send loaded msg to server but failed");
196 } else {
197 trace!("client send loaded to server by websocket");
198 }
199 return Ok(());
200 }
201 Err(error) => {
202 warn!(?error, node, "invalid configuration payload");
203 last_error = Some(error);
204 }
205 },
206 Err(error) => {
207 warn!(
208 ?error,
209 node, "client try to load all the configs but failed"
210 );
211 last_error = Some(error);
212 }
213 }
214 }
215
216 match self.load_from_cache() {
217 Ok(()) => {
218 trace!("client load all configs from local file");
219 Ok(())
220 }
221 Err(cache_error) => {
222 debug!(?cache_error, "local cache unavailable");
223 Err(last_error.unwrap_or(Error::LoadFailed))
224 }
225 }
226 }
227
228 fn load_from_cache(&self) -> Result<(), Error> {
229 let Some(json) = read_cache(
230 &self.options.cache,
231 &self.options.app_id,
232 &self.options.secret,
233 )?
234 else {
235 return Err(Error::LoadFailed);
236 };
237 let items = parse_items(&json)?;
238 self.apply_snapshot(items, None, true);
239 Ok(())
240 }
241
242 fn apply_snapshot(
243 &self,
244 items: Vec<ConfigItem>,
245 publish_time_line_id: Option<String>,
246 from_cache: bool,
247 ) {
248 let snapshot = Arc::new(ConfigSnapshot::from_items(
249 items,
250 publish_time_line_id,
251 from_cache,
252 ));
253 self.store.send_replace(snapshot);
254 self.loaded.store(true, Ordering::SeqCst);
255 }
256
257 pub(crate) async fn connect_websocket(self: &Arc<Self>) -> Result<(), Error> {
258 let mut last_error = Error::LoadFailed;
259 for node in RandomNodes::new(&self.options.nodes) {
260 match websocket::connect(&node, &self.options).await {
261 Ok(stream) => {
262 self.install_socket(stream).await;
263 trace!(node, "client connect websocket successful");
264 return Ok(());
265 }
266 Err(error) => {
267 warn!(?error, node, "client try to connect server occur error");
268 last_error = error;
269 }
270 }
271 }
272 Err(last_error)
273 }
274
275 async fn install_socket(self: &Arc<Self>, stream: websocket::WsStream) {
276 let session = self.cancel.child_token();
277 if let Ok(mut current) = self.session.lock() {
278 current.cancel();
279 *current = session.clone();
280 }
281 websocket::close_writer(self).await;
282 let (sink, reader) = stream.split();
283 *self.writer.lock().await = Some(sink);
284 websocket::spawn_reader(Arc::downgrade(self), session, reader);
285 }
286
287 pub(crate) async fn disconnect(&self) {
288 self.reconnect_enabled.store(false, Ordering::SeqCst);
289 if let Ok(session) = self.session.lock() {
290 session.cancel();
291 }
292 websocket::close_writer(self).await;
293 }
294
295 pub(crate) async fn handle_inbound(&self, text: &str) {
296 websocket::handle_inbound(self, text).await;
297 }
298}
299
300fn parse_items(json: &str) -> Result<Vec<ConfigItem>, Error> {
301 Ok(serde_json::from_str(json)?)
302}
303
304#[cfg(test)]
305mod tests {
306 use super::Client;
307 use crate::options::{CacheOptions, ClientOptions};
308
309 #[test]
310 fn new_rejects_empty_app_id() {
311 let error = Client::new(ClientOptions {
312 nodes: vec!["http://localhost:5000".into()],
313 ..ClientOptions::default()
314 })
315 .unwrap_err();
316 assert_eq!(error.to_string(), "app_id must not be empty");
317 }
318
319 #[test]
320 fn new_rejects_empty_nodes() {
321 let error = Client::new(ClientOptions {
322 app_id: "app".into(),
323 ..ClientOptions::default()
324 })
325 .unwrap_err();
326 assert_eq!(error.to_string(), "at least one server node is required");
327 }
328
329 #[test]
330 fn builder_and_struct_construction_are_equivalent() {
331 let from_struct = Client::new(ClientOptions {
332 app_id: "app".into(),
333 secret: "s".into(),
334 nodes: vec!["http://localhost:5000".into()],
335 env: "dev".into(),
336 cache: CacheOptions {
337 enabled: false,
338 ..CacheOptions::default()
339 },
340 ..ClientOptions::default()
341 })
342 .unwrap();
343 let from_builder = Client::builder()
344 .app_id("app")
345 .secret("s")
346 .nodes(["http://localhost:5000"])
347 .env("dev")
348 .cache(CacheOptions {
349 enabled: false,
350 ..CacheOptions::default()
351 })
352 .build()
353 .unwrap();
354 assert_eq!(from_struct.options().env, "DEV");
355 assert_eq!(from_builder.options().app_id, "app");
356 assert_eq!(from_builder.options().nodes, from_struct.options().nodes);
357 }
358}