xmtp_mls_core/
config_refresh.rs1use xmtp_common::{RetryableError, time::Duration};
10use xmtp_configuration::{
11 CONFIGURATION_REFRESH_ATTEMPTS, CONFIGURATION_REFRESH_BACKOFF, CONFIGURATION_REFRESH_INTERVAL,
12 CONFIGURATION_REFRESH_JITTER,
13};
14
15use crate::context::CoreContext;
16use crate::worker::{
17 BoxedWorker, NeedsDbReconnect, NewWorker, Worker, WorkerFactory, WorkerKind, WorkerResult,
18};
19use xmtp_mls_server_config::server_configuration::ServerConfigError;
20
21use super::preflight::refresh;
22
23#[derive(Clone)]
24pub struct Factory<Context> {
25 context: Context,
26}
27
28impl<Context> WorkerFactory for Factory<Context>
29where
30 Context: CoreContext + 'static,
31{
32 fn kind(&self) -> WorkerKind {
33 WorkerKind::ConfigurationRefresh
34 }
35
36 fn create(&self) -> BoxedWorker {
37 Box::new(ConfigurationWorker::new(self.context.clone()))
38 }
39}
40
41#[derive(Debug, thiserror::Error)]
45#[error("the configuration refresh worker stopped")]
46pub struct ConfigurationWorkerError;
47
48impl NeedsDbReconnect for ConfigurationWorkerError {
49 fn needs_db_reconnect(&self) -> bool {
50 false
51 }
52}
53
54pub struct ConfigurationWorker<Context> {
55 context: Context,
56}
57
58impl<Context> ConfigurationWorker<Context> {
59 pub fn new(context: Context) -> Self {
60 Self { context }
61 }
62}
63
64#[xmtp_common::async_trait]
65impl<Context> Worker for ConfigurationWorker<Context>
66where
67 Context: CoreContext + 'static,
68{
69 fn kind(&self) -> WorkerKind {
70 WorkerKind::ConfigurationRefresh
71 }
72
73 async fn run_tasks(&mut self) -> WorkerResult<()> {
74 self.run().await;
75 Ok(())
76 }
77}
78
79impl<Context> NewWorker<Context> for ConfigurationWorker<Context>
80where
81 Context: CoreContext + 'static,
82{
83 fn factory(context: Context) -> impl WorkerFactory + 'static {
84 Factory { context }
85 }
86}
87
88impl<Context> ConfigurationWorker<Context>
89where
90 Context: CoreContext + 'static,
91{
92 async fn run(&mut self) {
93 loop {
94 let (base, jitter) = self.schedule();
95 xmtp_common::time::sleep(base + xmtp_common::time::rand_offset(jitter)).await;
96 if self
98 .context
99 .server_configuration()
100 .blocked_connection()
101 .is_some()
102 {
103 return;
104 }
105 self.tick().await;
106 if self
109 .context
110 .server_configuration()
111 .blocked_connection()
112 .is_some()
113 {
114 self.context.cancellation_token().cancel();
115 return;
116 }
117 }
118 }
119
120 fn schedule(&self) -> (Duration, Duration) {
123 let kind = WorkerKind::ConfigurationRefresh;
124 let (base, jitter) = self
125 .context
126 .worker_interval(kind, CONFIGURATION_REFRESH_INTERVAL);
127 let jitter = if self
128 .context
129 .worker_config()
130 .jitter_overrides
131 .contains_key(&kind)
132 {
133 jitter
134 } else {
135 CONFIGURATION_REFRESH_JITTER
136 };
137 (base, jitter)
138 }
139
140 #[tracing::instrument(
142 skip_all,
143 fields(worker = "ConfigurationRefresh", operation = "worker_turn")
144 )]
145 pub(crate) async fn tick(&mut self) {
146 for attempt in 1..=CONFIGURATION_REFRESH_ATTEMPTS {
147 match self.attempt().await {
148 Ok(()) => return,
149 Err(error) => {
150 tracing::warn!(
153 attempt,
154 %error,
155 "server configuration refresh attempt failed; keeping the stored copy"
156 );
157 if !error.is_retryable() {
158 return;
159 }
160 match CONFIGURATION_REFRESH_BACKOFF.get(attempt - 1) {
161 Some(wait) => xmtp_common::time::sleep(*wait).await,
162 None => return,
163 }
164 }
165 }
166 }
167 }
168
169 async fn attempt(&mut self) -> Result<(), ServerConfigError> {
171 refresh(&self.context).await.map(|_| ())
172 }
173}
174
175#[cfg(any(test, feature = "test-utils"))]
176#[doc(hidden)]
177pub mod test_support {
178 use super::ConfigurationWorker;
179 use crate::context::CoreContext;
180
181 pub async fn tick<Context>(worker: &mut ConfigurationWorker<Context>)
183 where
184 Context: CoreContext + 'static,
185 {
186 worker.tick().await
187 }
188}
189
190#[cfg(test)]
191#[cfg(not(target_arch = "wasm32"))]
192mod tests;