1use std::collections::BTreeMap;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, Ordering};
13
14use anyhow::Context;
15use chrono::{DateTime, Utc};
16use derivative::Derivative;
17use mz_expr::Eval;
18use mz_ore::cast::CastFrom;
19use mz_repr::{Datum, Diff, Row, RowArena};
20use mz_secrets::SecretsReader;
21use mz_secrets::cache::CachingSecretsReader;
22use mz_sql::plan::{WebhookBodyFormat, WebhookHeaders, WebhookValidation, WebhookValidationSecret};
23use mz_storage_client::controller::MonotonicAppender;
24use mz_storage_client::statistics::WebhookStatistics;
25use mz_storage_types::controller::StorageError;
26use tokio::sync::Semaphore;
27
28use crate::optimize::dataflows::{ExprPrep, ExprPrepWebhookValidation};
29
30#[derive(thiserror::Error, Debug)]
32pub enum AppendWebhookError {
33 #[error("could not read a required secret")]
35 MissingSecret,
36 #[error("the provided request body is not UTF-8: {msg}")]
37 InvalidUtf8Body { msg: String },
38 #[error("the provided request body is not valid JSON: {msg}")]
39 InvalidJsonBody { msg: String },
40 #[error("webhook source '{database}.{schema}.{name}' does not exist")]
41 UnknownWebhook {
42 database: String,
43 schema: String,
44 name: String,
45 },
46 #[error("failed to validate the request")]
47 ValidationFailed,
48 #[error("validation error")]
53 ValidationError,
54 #[error("internal channel closed")]
55 ChannelClosed,
56 #[error("internal error: {0:?}")]
57 InternalError(#[from] anyhow::Error),
58 #[error("internal storage failure! {0:?}")]
59 StorageError(#[from] StorageError),
60}
61
62#[derive(Clone)]
66pub struct AppendWebhookValidator {
67 validation: WebhookValidation,
68 secrets_reader: CachingSecretsReader,
69}
70
71impl AppendWebhookValidator {
72 pub fn new(validation: WebhookValidation, secrets_reader: CachingSecretsReader) -> Self {
73 AppendWebhookValidator {
74 validation,
75 secrets_reader,
76 }
77 }
78
79 pub async fn eval(
85 self,
86 body: bytes::Bytes,
87 headers: Arc<BTreeMap<String, String>>,
88 received_at: DateTime<Utc>,
89 memory_budget: usize,
90 ) -> Result<bool, AppendWebhookError> {
91 let AppendWebhookValidator {
92 validation,
93 secrets_reader,
94 } = self;
95
96 let WebhookValidation {
97 mut expression,
98 relation_desc: _,
99 secrets,
100 bodies: body_columns,
101 headers: header_columns,
102 } = validation;
103
104 let mut secret_contents = BTreeMap::new();
106 for WebhookValidationSecret {
107 id,
108 column_idx,
109 use_bytes,
110 } in secrets
111 {
112 let secret = secrets_reader
113 .read(id)
114 .await
115 .map_err(|_| AppendWebhookError::MissingSecret)?;
116 secret_contents.insert(column_idx, (secret, use_bytes));
117 }
118
119 ExprPrepWebhookValidation { now: received_at }
124 .prep_scalar_expr(&mut expression)
125 .map_err(|err| {
126 tracing::error!(?err, "failed to evaluate current time");
127 AppendWebhookError::ValidationError
128 })?;
129
130 let validate = move || {
133 let temp_storage = RowArena::with_budget(memory_budget);
135 let mut datums = Vec::with_capacity(
136 body_columns.len() + header_columns.len() + secret_contents.len(),
137 );
138
139 for (column_idx, use_bytes) in body_columns {
141 assert_eq!(column_idx, datums.len(), "body index and datums mismatch!");
142
143 let datum = if use_bytes {
144 Datum::Bytes(&body[..])
145 } else {
146 let s = std::str::from_utf8(&body[..])
147 .map_err(|m| AppendWebhookError::InvalidUtf8Body { msg: m.to_string() })?;
148 Datum::String(s)
149 };
150 datums.push(datum);
151 }
152
153 let headers_byte = std::cell::OnceCell::new();
156 let headers_text = std::cell::OnceCell::new();
157 for (column_idx, use_bytes) in header_columns {
158 assert_eq!(column_idx, datums.len(), "index and datums mismatch!");
159
160 let row = if use_bytes {
161 headers_byte.get_or_init(|| {
162 let mut row = Row::with_capacity(1);
163 let mut packer = row.packer();
164 packer.push_dict(
165 headers
166 .iter()
167 .map(|(name, val)| (name.as_str(), Datum::Bytes(val.as_bytes()))),
168 );
169 row
170 })
171 } else {
172 headers_text.get_or_init(|| {
173 let mut row = Row::with_capacity(1);
174 let mut packer = row.packer();
175 packer.push_dict(
176 headers
177 .iter()
178 .map(|(name, val)| (name.as_str(), Datum::String(val))),
179 );
180 row
181 })
182 };
183 datums.push(row.unpack_first());
184 }
185
186 for column_idx in datums.len()..datums.len() + secret_contents.len() {
188 let (secret, use_bytes) = secret_contents
190 .get(&column_idx)
191 .expect("more secrets to provide, but none for the next column");
192
193 if *use_bytes {
194 datums.push(Datum::Bytes(secret));
195 } else {
196 let secret_str = std::str::from_utf8(&secret[..]).expect("valid UTF-8");
197 datums.push(Datum::String(secret_str));
198 }
199 }
200
201 let valid = expression
203 .eval(&datums[..], &temp_storage)
204 .map_err(|_| AppendWebhookError::ValidationError)?;
205 match valid {
206 Datum::True => Ok::<_, AppendWebhookError>(true),
207 Datum::False | Datum::Null => Ok(false),
208 _ => unreachable!("Creating a webhook source asserts we return a boolean"),
209 }
210 };
211
212 let valid = mz_ore::task::spawn_blocking(
214 || "webhook-validator-expr",
215 move || {
216 mz_ore::panic::catch_unwind(validate).map_err(|_| {
219 tracing::error!("panic while validating webhook request!");
220 AppendWebhookError::ValidationError
221 })
222 },
223 )
224 .await
225 .context("joining on validation")
226 .map_err(|e| {
227 tracing::error!("Failed to run validation for webhook, {e}");
228 AppendWebhookError::ValidationError
229 })?;
230
231 valid
232 }
233}
234
235#[derive(Derivative, Clone)]
236#[derivative(Debug)]
237pub struct AppendWebhookResponse {
238 pub tx: WebhookAppender,
240 pub body_format: WebhookBodyFormat,
242 pub header_tys: WebhookHeaders,
244 #[derivative(Debug = "ignore")]
246 pub validator: Option<AppendWebhookValidator>,
247}
248
249#[derive(Clone, Debug)]
252pub struct WebhookAppender {
253 tx: MonotonicAppender,
254 guard: WebhookAppenderGuard,
255 stats: Arc<WebhookStatistics>,
257}
258
259impl WebhookAppender {
260 pub fn is_closed(&self) -> bool {
262 self.guard.is_closed()
263 }
264
265 pub async fn append(&self, updates: Vec<(Row, Diff)>) -> Result<(), AppendWebhookError> {
267 if self.is_closed() {
268 return Err(AppendWebhookError::ChannelClosed);
269 }
270
271 let count = u64::cast_from(updates.len());
272 self.stats
273 .updates_staged
274 .fetch_add(count, Ordering::Relaxed);
275 let updates = updates.into_iter().map(|update| update.into()).collect();
276 self.tx.append(updates).await?;
277 self.stats
278 .updates_committed
279 .fetch_add(count, Ordering::Relaxed);
280 Ok(())
281 }
282
283 pub fn increment_messages_received(&self, msgs: u64) {
286 self.stats
287 .messages_received
288 .fetch_add(msgs, Ordering::Relaxed);
289 }
290
291 pub fn increment_bytes_received(&self, bytes: u64) {
294 self.stats
295 .bytes_received
296 .fetch_add(bytes, Ordering::Relaxed);
297 }
298
299 pub(crate) fn new(
300 tx: MonotonicAppender,
301 guard: WebhookAppenderGuard,
302 stats: Arc<WebhookStatistics>,
303 ) -> Self {
304 WebhookAppender { tx, guard, stats }
305 }
306}
307
308#[derive(Clone, Debug)]
313pub struct WebhookAppenderGuard {
314 is_closed: Arc<AtomicBool>,
315}
316
317impl WebhookAppenderGuard {
318 pub fn is_closed(&self) -> bool {
319 self.is_closed.load(Ordering::SeqCst)
320 }
321}
322
323#[derive(Debug)]
329pub struct WebhookAppenderInvalidator {
330 is_closed: Arc<AtomicBool>,
331}
332static_assertions::assert_not_impl_all!(WebhookAppenderInvalidator: Clone);
334
335impl WebhookAppenderInvalidator {
336 pub(crate) fn new() -> WebhookAppenderInvalidator {
337 let is_closed = Arc::new(AtomicBool::new(false));
338 WebhookAppenderInvalidator { is_closed }
339 }
340
341 pub fn guard(&self) -> WebhookAppenderGuard {
342 WebhookAppenderGuard {
343 is_closed: Arc::clone(&self.is_closed),
344 }
345 }
346}
347
348impl Drop for WebhookAppenderInvalidator {
349 fn drop(&mut self) {
350 self.is_closed.store(true, Ordering::SeqCst);
351 }
352}
353
354pub type WebhookAppenderName = (String, String, String);
355
356#[derive(Debug, Clone)]
361pub struct WebhookAppenderCache {
362 pub entries: Arc<tokio::sync::Mutex<BTreeMap<WebhookAppenderName, AppendWebhookResponse>>>,
363}
364
365impl WebhookAppenderCache {
366 pub fn new() -> Self {
367 WebhookAppenderCache {
368 entries: Arc::new(tokio::sync::Mutex::new(BTreeMap::new())),
369 }
370 }
371}
372
373#[derive(Debug, Clone)]
375pub struct WebhookConcurrencyLimiter {
376 semaphore: Arc<Semaphore>,
377 prev_limit: usize,
378}
379
380impl WebhookConcurrencyLimiter {
381 pub fn new(limit: usize) -> Self {
382 let semaphore = Arc::new(Semaphore::new(limit));
383
384 WebhookConcurrencyLimiter {
385 semaphore,
386 prev_limit: limit,
387 }
388 }
389
390 pub fn semaphore(&self) -> Arc<Semaphore> {
392 Arc::clone(&self.semaphore)
393 }
394
395 pub fn set_limit(&mut self, new_limit: usize) {
397 if new_limit > self.prev_limit {
398 let diff = new_limit.saturating_sub(self.prev_limit);
400 tracing::debug!("Adding {diff} permits");
401
402 self.semaphore.add_permits(diff);
403 } else if new_limit < self.prev_limit {
404 let diff = self.prev_limit.saturating_sub(new_limit);
406 let diff = u32::try_from(diff).unwrap_or(u32::MAX);
407 tracing::debug!("Removing {diff} permits");
408
409 let semaphore = self.semaphore();
410
411 mz_ore::task::spawn(|| "webhook-concurrency-limiter-drop-permits", async move {
414 if let Ok(permit) = Semaphore::acquire_many_owned(semaphore, diff).await {
415 permit.forget()
416 }
417 });
418 }
419
420 self.prev_limit = new_limit;
422 tracing::debug!("New limit, {} permits", self.prev_limit);
423 }
424}
425
426impl Default for WebhookConcurrencyLimiter {
427 fn default() -> Self {
428 WebhookConcurrencyLimiter::new(mz_sql::WEBHOOK_CONCURRENCY_LIMIT)
429 }
430}
431
432#[cfg(test)]
433mod test {
434 use mz_ore::assert_err;
435
436 use super::WebhookConcurrencyLimiter;
437
438 #[mz_ore::test(tokio::test)]
439 #[cfg_attr(miri, ignore)] async fn smoke_test_concurrency_limiter() {
441 let mut limiter = WebhookConcurrencyLimiter::new(10);
442
443 let semaphore_a = limiter.semaphore();
444 let _permit_a = semaphore_a.try_acquire_many(10).expect("acquire");
445
446 let semaphore_b = limiter.semaphore();
447 assert_err!(semaphore_b.try_acquire());
448
449 limiter.set_limit(15);
451
452 let _permit_b = semaphore_b.try_acquire().expect("acquire");
454
455 limiter.set_limit(5);
457
458 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
459
460 assert_err!(semaphore_b.try_acquire());
462 }
463}