1use std::collections::BTreeMap;
13use std::sync::Arc;
14use std::time::Duration;
15
16use anyhow::anyhow;
17use mz_dyncfg::ConfigSet;
18use mz_ore::error::ErrorExt;
19use mz_ore::url::SensitiveUrl;
20use tracing::warn;
21
22use mz_postgres_client::PostgresClientKnobs;
23use mz_postgres_client::metrics::PostgresClientMetrics;
24
25use crate::azure::{AzureBlob, AzureBlobConfig};
26use crate::file::{FileBlob, FileBlobConfig};
27use crate::gcs::{GcsBlob, GcsBlobConfig};
28use crate::hedge::HedgeSibling;
29use crate::location::{Blob, Consensus, Determinate, ExternalError};
30use crate::mem::{MemBlob, MemBlobConfig, MemConsensus};
31use crate::metrics::S3BlobMetrics;
32use crate::postgres::{PostgresConsensus, PostgresConsensusConfig};
33use crate::s3::{BlobHttpPool, S3Blob, S3BlobConfig};
34
35pub fn all_dyn_configs(configs: ConfigSet) -> ConfigSet {
37 configs
38 .add(&crate::postgres::PG_CONSENSUS_READ_COMMITTED)
39 .add(&crate::hedge::BLOB_HEDGED_GET_ENABLED)
40 .add(&crate::hedge::BLOB_HEDGED_GET_DELAY)
41 .add(&crate::hedge::BLOB_HEDGED_GET_MAX_CONCURRENT)
42 .add(&crate::hedge::BLOB_HEDGED_GET_BUDGET_RATIO)
43 .add(&crate::hedge::BLOB_HEDGED_GET_WARM_INTERVAL)
44 .add(&crate::s3::BLOB_HTTP_POOL)
45 .add(&crate::s3::BLOB_HTTP_POOL_MAX_IN_FLIGHT_PER_CLIENT)
46}
47
48pub async fn open_hedge_sibling(
67 url: &SensitiveUrl,
68 knobs: Box<dyn BlobKnobs>,
69 metrics: S3BlobMetrics,
70) -> HedgeSibling {
71 let metrics = metrics.without_http_pool_metrics();
73 let config = match BlobConfig::try_from(url, knobs, metrics).await {
74 Ok(config) => config,
75 Err(err) => {
76 warn!(
77 "hedged blob gets unavailable, sibling config failed: {}",
78 err.display_with_causes()
79 );
80 return HedgeSibling::Unavailable;
81 }
82 };
83 match config {
84 config @ (BlobConfig::S3(_) | BlobConfig::Azure(_) | BlobConfig::Gcs(_)) => {
87 match config.open().await {
88 Ok(blob) => HedgeSibling::Isolated(blob),
89 Err(err) => {
90 warn!(
91 "hedged blob gets unavailable, sibling open failed: {}",
92 err.display_with_causes()
93 );
94 HedgeSibling::Unavailable
95 }
96 }
97 }
98 BlobConfig::File(_) | BlobConfig::Mem(_) => HedgeSibling::SharedWithPrimary,
104 #[cfg(feature = "turmoil")]
105 BlobConfig::Turmoil(_) => HedgeSibling::SharedWithPrimary,
106 }
107}
108
109#[derive(Debug, Clone)]
111pub enum BlobConfig {
112 File(FileBlobConfig),
114 S3(S3BlobConfig),
116 Mem(bool),
119 Azure(AzureBlobConfig),
121 Gcs(GcsBlobConfig),
123 #[cfg(feature = "turmoil")]
124 Turmoil(crate::turmoil::BlobConfig),
126}
127
128pub trait BlobKnobs: std::fmt::Debug + Send + Sync {
130 fn operation_timeout(&self) -> Duration;
132 fn operation_attempt_timeout(&self) -> Duration;
134 fn connect_timeout(&self) -> Duration;
136 fn read_timeout(&self) -> Duration;
138 fn is_cc_active(&self) -> bool;
140 fn http_pool(&self) -> BlobHttpPool {
142 BlobHttpPool::default()
143 }
144 fn http_pool_max_in_flight_per_client(&self) -> usize {
146 *crate::s3::BLOB_HTTP_POOL_MAX_IN_FLIGHT_PER_CLIENT.default()
147 }
148}
149
150impl BlobConfig {
151 pub async fn open(self) -> Result<Arc<dyn Blob>, ExternalError> {
153 match self {
154 BlobConfig::File(config) => Ok(Arc::new(FileBlob::open(config).await?)),
155 BlobConfig::S3(config) => Ok(Arc::new(S3Blob::open(config).await?)),
156 BlobConfig::Azure(config) => Ok(Arc::new(AzureBlob::open(config).await?)),
157 BlobConfig::Gcs(config) => Ok(Arc::new(GcsBlob::open(config).await?)),
158 BlobConfig::Mem(tombstone) => {
159 Ok(Arc::new(MemBlob::open(MemBlobConfig::new(tombstone))))
160 }
161 #[cfg(feature = "turmoil")]
162 BlobConfig::Turmoil(config) => Ok(Arc::new(crate::turmoil::TurmoilBlob::open(config))),
163 }
164 }
165
166 pub async fn try_from(
168 url: &SensitiveUrl,
169 knobs: Box<dyn BlobKnobs>,
170 metrics: S3BlobMetrics,
171 ) -> Result<Self, ExternalError> {
172 let mut query_params = url.query_pairs().collect::<BTreeMap<_, _>>();
173
174 let config = match url.scheme() {
175 "file" => {
176 let mut config = FileBlobConfig::from(url.path());
177 if query_params.remove("tombstone").is_some() {
178 config.tombstone = true;
179 }
180 Ok(BlobConfig::File(config))
181 }
182 "s3" => {
183 let bucket = url
184 .host()
185 .ok_or_else(|| anyhow!("missing bucket: {}", url))?
186 .to_string();
187 let prefix = url
188 .path()
189 .strip_prefix('/')
190 .unwrap_or_else(|| url.path())
191 .to_string();
192 let role_arn = query_params.remove("role_arn").map(|x| x.into_owned());
193 let endpoint = query_params.remove("endpoint").map(|x| x.into_owned());
194 let region = query_params.remove("region").map(|x| x.into_owned());
195
196 let credentials = match url.password() {
197 None => None,
198 Some(password) => Some((
199 String::from_utf8_lossy(&urlencoding::decode_binary(
200 url.username().as_bytes(),
201 ))
202 .into_owned(),
203 String::from_utf8_lossy(&urlencoding::decode_binary(password.as_bytes()))
204 .into_owned(),
205 )),
206 };
207
208 let config = S3BlobConfig::new(
209 bucket,
210 prefix,
211 role_arn,
212 endpoint,
213 region,
214 credentials,
215 knobs,
216 metrics,
217 )
218 .await?;
219
220 Ok(BlobConfig::S3(config))
221 }
222 "gcs" => {
223 let bucket = url
224 .host()
225 .ok_or_else(|| anyhow!("missing bucket: {}", url.as_str()))?
226 .to_string();
227 let prefix = url
228 .path()
229 .strip_prefix('/')
230 .unwrap_or_else(|| url.path())
231 .to_string();
232
233 query_params.clear();
234 let config = GcsBlobConfig::new(bucket, prefix, knobs, metrics).await?;
235
236 Ok(BlobConfig::Gcs(config))
237 }
238 "mem" => {
239 if !cfg!(debug_assertions) {
240 warn!("persist unexpectedly using in-mem blob in a release binary");
241 }
242 let tombstone = match query_params.remove("tombstone").as_deref() {
243 None | Some("true") => true,
244 Some("false") => false,
245 Some(other) => Err(Determinate::new(anyhow!(
246 "invalid tombstone param value: {other}"
247 )))?,
248 };
249 query_params.clear();
250 Ok(BlobConfig::Mem(tombstone))
251 }
252 "http" | "https" => match url
253 .host()
254 .ok_or_else(|| anyhow!("missing protocol: {}", url))?
255 .to_string()
256 .split_once('.')
257 {
258 Some((account, root))
260 if account == "devstoreaccount1" || root == "blob.core.windows.net" =>
261 {
262 if let Some(container) = url
263 .path_segments()
264 .expect("azure blob storage container")
265 .next()
266 {
267 query_params.clear();
268 Ok(BlobConfig::Azure(AzureBlobConfig::new(
269 account.to_string(),
270 container.to_string(),
271 "".to_string(),
275 metrics,
276 url.clone().into_redacted(),
277 knobs,
278 )?))
279 } else {
280 Err(anyhow!("unknown persist blob scheme: {}", url))
281 }
282 }
283 _ => Err(anyhow!("unknown persist blob scheme: {}", url)),
284 },
285 #[cfg(feature = "turmoil")]
286 "turmoil" => {
287 let cfg = crate::turmoil::BlobConfig::new(url);
288 Ok(BlobConfig::Turmoil(cfg))
289 }
290 p => Err(anyhow!("unknown persist blob scheme {}: {}", p, url)),
291 }?;
292
293 if !query_params.is_empty() {
294 return Err(ExternalError::from(anyhow!(
295 "unknown blob location params {}: {}",
296 query_params
297 .keys()
298 .map(|x| x.as_ref())
299 .collect::<Vec<_>>()
300 .join(" "),
301 url,
302 )));
303 }
304
305 Ok(config)
306 }
307}
308
309#[derive(Debug, Clone)]
311pub enum ConsensusConfig {
312 Postgres(PostgresConsensusConfig),
314 Mem,
316 #[cfg(feature = "turmoil")]
317 Turmoil(crate::turmoil::ConsensusConfig),
319}
320
321impl ConsensusConfig {
322 pub async fn open(self) -> Result<Arc<dyn Consensus>, ExternalError> {
324 match self {
325 ConsensusConfig::Postgres(config) => {
326 Ok(Arc::new(PostgresConsensus::open(config).await?))
327 }
328 ConsensusConfig::Mem => Ok(Arc::new(MemConsensus::default())),
329 #[cfg(feature = "turmoil")]
330 ConsensusConfig::Turmoil(config) => {
331 Ok(Arc::new(crate::turmoil::TurmoilConsensus::open(config)))
332 }
333 }
334 }
335
336 pub fn try_from(
338 url: &SensitiveUrl,
339 knobs: Box<dyn PostgresClientKnobs>,
340 metrics: PostgresClientMetrics,
341 dyncfg: Arc<ConfigSet>,
342 ) -> Result<Self, ExternalError> {
343 let config = match url.scheme() {
344 "postgres" | "postgresql" => Ok(ConsensusConfig::Postgres(
345 PostgresConsensusConfig::new(url, knobs, metrics, dyncfg)?,
346 )),
347 "mem" => {
348 if !cfg!(debug_assertions) {
349 warn!("persist unexpectedly using in-mem consensus in a release binary");
350 }
351 Ok(ConsensusConfig::Mem)
352 }
353 #[cfg(feature = "turmoil")]
354 "turmoil" => {
355 let cfg = crate::turmoil::ConsensusConfig::new(url);
356 Ok(ConsensusConfig::Turmoil(cfg))
357 }
358 p => Err(anyhow!("unknown persist consensus scheme {}: {}", p, url)),
359 }?;
360 Ok(config)
361 }
362}