xmtp_mls_core/
preflight.rs1use crate::context::{CoreContext, LocalContext};
4use std::sync::{Arc, Weak, atomic::Ordering};
5use xmtp_api::preflight::{ConfigurationFetch, PreflightError, RequestPreflight};
6use xmtp_configuration::ServerConfiguration;
7use xmtp_mls_server_config::server_configuration::{
8 BlockedConnection, ConfigurationFetchError, ServerConfigError, check_minimum_version,
9 commit_configuration,
10};
11
12struct ClientPreflight<A, Db, S, F> {
13 context: Weak<LocalContext<A, Db, S, F>>,
14}
15
16pub fn bind_preflight<A, Db, S, F>(context: &Arc<LocalContext<A, Db, S, F>>)
23where
24 A: xmtp_proto::api_client::XmtpApi + 'static,
25 Db: xmtp_db::XmtpDb + 'static,
26 S: xmtp_db::XmtpMlsStorageProvider + 'static,
27 F: xmtp_common::MaybeSend + xmtp_common::MaybeSync + 'static,
28{
29 let weak = Arc::downgrade(context);
30 assert!(
31 context.api().api_client.bind_preflight(|fetch| {
32 let _ = context.server_configuration().fetch.set(fetch);
33 Arc::new(ClientPreflight { context: weak })
34 }),
35 "client preflight is bound once"
36 );
37}
38
39#[xmtp_common::async_trait]
40impl<A, Db, S, F> RequestPreflight for ClientPreflight<A, Db, S, F>
41where
42 A: xmtp_proto::api_client::XmtpApi + 'static,
43 Db: xmtp_db::XmtpDb + 'static,
44 S: xmtp_db::XmtpMlsStorageProvider + 'static,
45 F: xmtp_common::MaybeSend + xmtp_common::MaybeSync + 'static,
46{
47 async fn check(&self, fetch: &dyn ConfigurationFetch) -> Result<(), PreflightError> {
48 let context = self
49 .context
50 .upgrade()
51 .ok_or_else(|| PreflightError::new(ServerConfigError::Closed))?;
52 run(&context, fetch, false)
53 .await
54 .map(|_| ())
55 .map_err(PreflightError::new)
56 }
57}
58
59fn check<C: CoreContext>(context: &C) -> Result<(), ServerConfigError> {
60 context.server_configuration().check()?;
61 if context.is_closed() {
62 return Err(ServerConfigError::Closed);
63 }
64 Ok(())
65}
66
67pub async fn refresh<C: CoreContext>(
69 context: &C,
70) -> Result<ServerConfiguration, ServerConfigError> {
71 let handle = context.server_configuration();
72 let fetched = if let Some(fetch) = handle.fetch.get() {
74 run(context, fetch.as_ref(), true).await?
75 } else {
76 run(context, &UnboundFetch(context), true).await?
77 };
78 Ok(fetched.expect("refresh always fetches configuration"))
79}
80
81struct UnboundFetch<'a, C>(&'a C);
82#[xmtp_common::async_trait]
83impl<C: CoreContext> ConfigurationFetch for UnboundFetch<'_, C> {
84 async fn fetch(&self) -> xmtp_api::Result<xmtp_proto::backend_v1::GetConfigurationResponse> {
85 self.0.api().get_configuration().await
86 }
87 fn backend_url(&self) -> Option<&str> {
88 self.0.api().backend_url()
89 }
90}
91
92async fn run<C: CoreContext>(
94 context: &C,
95 fetch: &dyn ConfigurationFetch,
96 refresh: bool,
97) -> Result<Option<ServerConfiguration>, ServerConfigError> {
98 let handle = context.server_configuration();
99 handle.check()?;
100 if !refresh && !handle.pending.load(Ordering::Acquire) {
103 return Ok(None);
104 }
105 check(context)?;
106 let _foreground = context
107 .foreground_calls()
108 .enter()
109 .ok_or(ServerConfigError::Closed)?;
110 let cancelled = context.cancellation_token();
111 let _admission = tokio::select! {
112 biased;
113 _ = cancelled.cancelled() => { check(context)?; return Err(ServerConfigError::Closed); },
114 pending = handle.admission.lock() => pending,
115 };
116 check(context)?;
117 if !handle.pending.load(Ordering::Acquire) && !refresh {
118 return Ok(None);
119 }
120 let response = tokio::select! {
121 biased;
122 _ = cancelled.cancelled() => { check(context)?; return Err(ServerConfigError::Closed); },
123 response = fetch.fetch() => response.map_err(ConfigurationFetchError::Api)?,
124 };
125 check(context)?;
126 let fetched = commit_configuration(response, fetch.backend_url(), &context.db(), handle);
129 let fetched = match fetched {
130 Ok(fetched) => fetched,
131 Err(error) => {
132 if handle.blocked_connection().is_some() {
133 cancelled.cancel();
134 }
135 return Err(error);
136 }
137 };
138 handle.record_deployment(&fetched.identifier).await?;
140 if let Err(ServerConfigError::Blocked(reason @ BlockedConnection::ClientVersionTooOld { .. })) =
141 check_minimum_version(&fetched, context.version_info().pkg_semver().semver())
142 {
143 let error = handle.block_connection(reason);
144 cancelled.cancel();
145 return Err(error.into());
146 }
147 handle.pending.store(false, Ordering::Release);
148 Ok(Some(fetched))
149}