risingwave_object_store/object/opendal_engine/
s3.rs1use std::sync::Arc;
16use std::time::Duration;
17
18use opendal::layers::LoggingLayer;
19use opendal::services::S3;
20use opendal::{HttpTransporter, OperationContext, Operator};
21use opendal_http_transport_reqwest::ReqwestTransport;
22use risingwave_common::config::ObjectStoreConfig;
23use thiserror_ext::AsReport as _;
24
25use super::rotating_transport::RotatingTransport;
26use super::{MediaType, OpendalObjectStore, new_operator};
27use crate::object::object_metrics::ObjectStoreMetrics;
28use crate::object::{ObjectError, ObjectResult};
29
30impl OpendalObjectStore {
31 pub fn new_s3_engine(
33 bucket: String,
34 config: Arc<ObjectStoreConfig>,
35 metrics: Arc<ObjectStoreMetrics>,
36 ) -> ObjectResult<Self> {
37 let mut builder = S3::default().bucket(&bucket);
39 if let Ok(endpoint_url) = std::env::var("RW_S3_ENDPOINT") {
41 builder = builder.endpoint(&endpoint_url);
42 }
43
44 if std::env::var("RW_IS_FORCE_PATH_STYLE").is_err() {
45 builder = builder.enable_virtual_host_style();
46 }
47
48 let transport = Self::new_http_transport(config.clone())?;
49
50 let op = new_operator(
51 &config,
52 Operator::new(builder)?
53 .with_context(OperationContext::new().with_http_transport(transport))
54 .layer(LoggingLayer::default()),
55 );
56
57 Ok(Self {
58 op,
59 media_type: MediaType::S3,
60 config,
61 metrics,
62 })
63 }
64
65 pub fn new_minio_engine(
67 server: &str,
68 config: Arc<ObjectStoreConfig>,
69 metrics: Arc<ObjectStoreMetrics>,
70 ) -> ObjectResult<Self> {
71 let server = server.strip_prefix("minio://").unwrap();
72 let (access_key_id, rest) = server.split_once(':').unwrap();
73 let (secret_access_key, mut rest) = rest.split_once('@').unwrap();
74
75 let endpoint_prefix = if let Some(rest_stripped) = rest.strip_prefix("https://") {
76 rest = rest_stripped;
77 "https://"
78 } else if let Some(rest_stripped) = rest.strip_prefix("http://") {
79 rest = rest_stripped;
80 "http://"
81 } else {
82 "http://"
83 };
84 let (address, bucket) = rest.split_once('/').unwrap();
85 let builder = S3::default()
86 .bucket(bucket)
87 .region("custom")
88 .access_key_id(access_key_id)
89 .secret_access_key(secret_access_key)
90 .endpoint(&format!("{}{}", endpoint_prefix, address))
91 .disable_config_load();
92
93 let transport = Self::new_http_transport(config.clone())?;
94
95 let op = new_operator(
96 &config,
97 Operator::new(builder)?
98 .with_context(OperationContext::new().with_http_transport(transport))
99 .layer(LoggingLayer::default()),
100 );
101
102 Ok(Self {
103 op,
104 media_type: MediaType::Minio,
105 config,
106 metrics,
107 })
108 }
109
110 fn new_http_transport(config: Arc<ObjectStoreConfig>) -> ObjectResult<HttpTransporter> {
111 match config.s3.http_client_rotation_interval_ms {
112 None => Ok(HttpTransporter::new(ReqwestTransport::new(
113 Self::new_http_client(&config)?,
114 ))),
115 Some(interval_ms) => Ok(HttpTransporter::new(RotatingTransport::new(
116 move || {
117 Self::new_http_client(&config)
118 .map(ReqwestTransport::new)
119 .map_err(|err| {
120 opendal::Error::new(
121 opendal::ErrorKind::Unexpected,
122 "failed to build rotating HTTP client",
123 )
124 .set_source(err)
125 })
126 },
127 Duration::from_millis(interval_ms),
128 )?)),
129 }
130 }
131
132 pub fn new_http_client(config: &ObjectStoreConfig) -> ObjectResult<reqwest::Client> {
133 let mut client_builder = reqwest::ClientBuilder::new();
134
135 if let Some(keepalive_ms) = config.s3.keepalive_ms.as_ref() {
136 client_builder = client_builder.tcp_keepalive(Duration::from_millis(*keepalive_ms));
137 }
138
139 if let Some(nodelay) = config.s3.nodelay.as_ref() {
140 client_builder = client_builder.tcp_nodelay(*nodelay);
141 }
142 client_builder.build().map_err(|e| {
143 ObjectError::internal(format!("failed to build HTTP client: {}", e.as_report()))
144 })
145 }
146}