risingwave_connector/source/google_pubsub/
mod.rs1use std::collections::HashMap;
16
17use anyhow::Context;
18use google_cloud_pubsub::client::{Client, ClientConfig};
19use google_cloud_pubsub::subscriber::SubscriberConfig;
20use google_cloud_pubsub::subscription::Subscription;
21use risingwave_common::bail;
22use serde::Deserialize;
23
24pub mod enumerator;
25pub mod source;
26pub mod split;
27
28pub use enumerator::*;
29use phf::{Set, phf_set};
30use serde_with::{DisplayFromStr, serde_as};
31pub use source::*;
32pub use split::*;
33use with_options::WithOptions;
34
35use crate::enforce_secret::EnforceSecret;
36use crate::error::ConnectorResult;
37use crate::source::SourceProperties;
38
39pub const GOOGLE_PUBSUB_CONNECTOR: &str = "google_pubsub";
40
41const DEFAULT_ACK_DEADLINE_SECONDS: i32 = 60;
42const DEFAULT_MAX_OUTSTANDING_MESSAGES: i64 = 1024;
45const DEFAULT_MAX_OUTSTANDING_BYTES: i64 = 1_000_000_000;
46
47#[serde_as]
52#[derive(Clone, Debug, Deserialize, WithOptions)]
53pub struct PubsubProperties {
54 #[serde(rename = "pubsub.subscription")]
62 pub subscription: String,
63
64 #[serde(rename = "pubsub.emulator_host")]
67 pub emulator_host: Option<String>,
68
69 #[serde(rename = "pubsub.credentials")]
73 pub credentials: Option<String>,
74
75 #[serde(rename = "pubsub.start_offset.nanos")]
82 pub start_offset: Option<String>,
83
84 #[serde(rename = "pubsub.start_snapshot")]
93 pub start_snapshot: Option<String>,
94
95 #[serde_as(as = "Option<DisplayFromStr>")]
99 #[serde(rename = "pubsub.parallelism")]
100 pub parallelism: Option<u32>,
101
102 #[serde_as(as = "Option<DisplayFromStr>")]
106 #[serde(rename = "pubsub.ack_deadline_seconds")]
107 #[with_option(allow_alter_on_fly)]
108 pub ack_deadline_seconds: Option<i32>,
109
110 #[serde_as(as = "Option<DisplayFromStr>")]
114 #[serde(rename = "pubsub.max_outstanding_messages")]
115 #[with_option(allow_alter_on_fly)]
116 pub max_outstanding_messages: Option<i64>,
117
118 #[serde_as(as = "Option<DisplayFromStr>")]
122 #[serde(rename = "pubsub.max_outstanding_bytes")]
123 #[with_option(allow_alter_on_fly)]
124 pub max_outstanding_bytes: Option<i64>,
125
126 #[serde(flatten)]
127 pub unknown_fields: HashMap<String, String>,
128}
129
130impl EnforceSecret for PubsubProperties {
131 const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
132 "pubsub.credentials",
133 };
134}
135
136impl SourceProperties for PubsubProperties {
137 type Split = PubsubSplit;
138 type SplitEnumerator = PubsubSplitEnumerator;
139 type SplitReader = PubsubSplitReader;
140
141 const SOURCE_NAME: &'static str = GOOGLE_PUBSUB_CONNECTOR;
142}
143
144impl crate::source::UnknownFields for PubsubProperties {
145 fn unknown_fields(&self) -> HashMap<String, String> {
146 self.unknown_fields.clone()
147 }
148}
149
150impl PubsubProperties {
151 pub(crate) fn subscriber_config(&self) -> ConnectorResult<SubscriberConfig> {
152 let stream_ack_deadline_seconds = self
153 .ack_deadline_seconds
154 .unwrap_or(DEFAULT_ACK_DEADLINE_SECONDS);
155 if !(10..=600).contains(&stream_ack_deadline_seconds) {
156 bail!("pubsub.ack_deadline_seconds must be between 10 and 600");
157 }
158
159 let max_outstanding_messages = self
160 .max_outstanding_messages
161 .unwrap_or(DEFAULT_MAX_OUTSTANDING_MESSAGES);
162 if max_outstanding_messages <= 0 {
163 bail!("pubsub.max_outstanding_messages must be greater than 0");
164 }
165
166 let max_outstanding_bytes = self
167 .max_outstanding_bytes
168 .unwrap_or(DEFAULT_MAX_OUTSTANDING_BYTES);
169 if max_outstanding_bytes <= 0 {
170 bail!("pubsub.max_outstanding_bytes must be greater than 0");
171 }
172
173 Ok(SubscriberConfig {
174 stream_ack_deadline_seconds,
175 max_outstanding_messages,
176 max_outstanding_bytes,
177 ..Default::default()
178 })
179 }
180
181 pub(crate) async fn subscription_client(&self) -> ConnectorResult<Subscription> {
182 {
184 tracing::debug!("setting pubsub environment variables");
185 if let Some(emulator_host) = &self.emulator_host {
186 unsafe { std::env::set_var("PUBSUB_EMULATOR_HOST", emulator_host) };
188 }
189 if let Some(credentials) = &self.credentials {
190 unsafe { std::env::set_var("GOOGLE_APPLICATION_CREDENTIALS_JSON", credentials) };
192 }
193 };
194
195 let config = ClientConfig::default().with_auth().await?;
197 let client = Client::new(config)
198 .await
199 .context("error initializing pubsub client")?;
200
201 Ok(client.subscription(&self.subscription))
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use serde_json::json;
208
209 use super::*;
210
211 fn parse_pubsub_properties(extra: serde_json::Value) -> PubsubProperties {
212 let mut value = json!({
213 "pubsub.subscription": "projects/test/subscriptions/test",
214 "pubsub.emulator_host": "localhost:8900",
215 });
216 value
217 .as_object_mut()
218 .unwrap()
219 .extend(extra.as_object().unwrap().clone());
220 serde_json::from_value(value).unwrap()
221 }
222
223 #[test]
224 fn test_subscriber_config_defaults() {
225 let config = parse_pubsub_properties(json!({}))
226 .subscriber_config()
227 .unwrap();
228
229 assert_eq!(config.stream_ack_deadline_seconds, 60);
230 assert_eq!(config.max_outstanding_messages, 1024);
231 assert_eq!(config.max_outstanding_bytes, 1_000_000_000);
232 }
233
234 #[test]
235 fn test_subscriber_config_overrides() {
236 let config = parse_pubsub_properties(json!({
237 "pubsub.ack_deadline_seconds": "120",
238 "pubsub.max_outstanding_messages": "2048",
239 "pubsub.max_outstanding_bytes": "1048576",
240 }))
241 .subscriber_config()
242 .unwrap();
243
244 assert_eq!(config.stream_ack_deadline_seconds, 120);
245 assert_eq!(config.max_outstanding_messages, 2048);
246 assert_eq!(config.max_outstanding_bytes, 1_048_576);
247 }
248
249 #[test]
250 fn test_subscriber_config_validation() {
251 let invalid_values = [
252 (
253 json!({"pubsub.ack_deadline_seconds": "9"}),
254 "pubsub.ack_deadline_seconds must be between 10 and 600",
255 ),
256 (
257 json!({"pubsub.max_outstanding_messages": "0"}),
258 "pubsub.max_outstanding_messages must be greater than 0",
259 ),
260 (
261 json!({"pubsub.max_outstanding_bytes": "0"}),
262 "pubsub.max_outstanding_bytes must be greater than 0",
263 ),
264 ];
265
266 for (value, expected_error) in invalid_values {
267 let error = parse_pubsub_properties(value)
268 .subscriber_config()
269 .unwrap_err();
270 assert!(error.to_string().contains(expected_error));
271 }
272 }
273}