Skip to content

Commit

Permalink
turn event class globs into SIMILAR TO patterns
Browse files Browse the repository at this point in the history
needs testing, but i'd like to do that after finishing the DB queries
that use it...
  • Loading branch information
hawkw committed Jan 4, 2025
1 parent 49736a6 commit 326b3f7
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 3 deletions.
1 change: 1 addition & 0 deletions nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2139,6 +2139,7 @@ table! {
webhook_rx_subscription (rx_id, event_class) {
rx_id -> Uuid,
event_class -> Text,
similar_to -> Text,
time_created -> Timestamptz,
}
}
Expand Down
84 changes: 83 additions & 1 deletion nexus/db-model/src/webhook_rx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::schema::{webhook_rx, webhook_rx_secret, webhook_rx_subscription};
use crate::typed_uuid::DbTypedUuid;
use chrono::{DateTime, Utc};
use db_macros::Resource;
use omicron_uuid_kinds::WebhookReceiverKind;
use omicron_uuid_kinds::{WebhookReceiverKind, WebhookReceiverUuid};
use serde::{Deserialize, Serialize};

/// A webhook receiver configuration.
Expand Down Expand Up @@ -47,5 +47,87 @@ pub struct WebhookRxSecret {
pub struct WebhookRxSubscription {
pub rx_id: DbTypedUuid<WebhookReceiverKind>,
pub event_class: String,
pub similar_to: String,
pub time_created: DateTime<Utc>,
}

impl WebhookRxSubscription {
pub fn new(rx_id: WebhookReceiverUuid, event_class: String) -> Self {
fn seg2regex(segment: &str, similar_to: &mut String) {
match segment {
// Match one segment (i.e. any number of segment characters)
"*" => similar_to.push_str("[a-zA-Z0-9\\_\\-]+"),
// Match any number of segments
"**" => similar_to.push('%'),
// Match the literal segment.
// Because `_` his a metacharacter in Postgres' SIMILAR TO
// regexes, we've gotta go through and escape them.
s => {
for s in s.split_inclusive('_') {
// Handle the fact that there might not be a `_` in the
// string at all
if let Some(s) = s.strip_suffix('_') {
similar_to.push_str(s);
similar_to.push_str("\\_");
} else {
similar_to.push_str(s);
}
}
}
}
}

// The subscription's regex will always be at least as long as the event class.
let mut similar_to = String::with_capacity(event_class.len());
let mut segments = event_class.split('.');
if let Some(segment) = segments.next() {
seg2regex(segment, &mut similar_to);
for segment in segments {
similar_to.push('.'); // segment separator
seg2regex(segment, &mut similar_to);
}
} else {
// TODO(eliza): we should probably validate that the event class has
// at least one segment...
};

// `_` is a metacharacter in Postgres' SIMILAR TO regexes, so escape
// them.

Self {
rx_id: DbTypedUuid(rx_id),
event_class,
similar_to,
time_created: Utc::now(),
}
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_event_class_glob_to_regex() {
const CASES: &[(&str, &str)] = &[
("foo.bar", "foo.bar"),
("foo.*.bar", "foo.[a-zA-Z0-9\\_\\-]+.bar"),
("foo.*", "foo.[a-zA-Z0-9\\_\\-]+"),
("*.foo", "[a-zA-Z0-9\\_\\-]+.foo"),
("foo.**.bar", "foo.%.bar"),
("foo.**", "foo.%"),
("foo_bar.baz", "foo\\_bar.baz"),
("foo_bar.*.baz", "foo\\_bar.[a-zA-Z0-9\\_\\-]+.baz"),
];
let rx_id = WebhookReceiverUuid::new_v4();
for (class, regex) in CASES {
let subscription =
WebhookRxSubscription::new(rx_id, dbg!(class).to_string());
assert_eq!(
dbg!(regex),
dbg!(&subscription.similar_to),
"event class {class:?} should produce the regex {regex:?}"
);
}
}
}
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/datastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ mod vmm;
mod volume;
mod volume_repair;
mod vpc;
mod webhook_event;
mod zpool;

pub use address_lot::AddressLotCreateResult;
Expand Down
59 changes: 59 additions & 0 deletions nexus/db-queries/src/db/datastore/webhook_event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! [`DataStore`] methods for webhook events and event delivery dispatching.
use super::DataStore;
use crate::db::pool::DbConnection;
use async_bb8_diesel::AsyncRunQueryDsl;
use diesel::prelude::*;
use diesel::result::OptionalExtension;
use omicron_uuid_kinds::{GenericUuid, WebhookEventUuid};

use crate::db::model::WebhookEvent;
use crate::db::schema::webhook_event::dsl as event_dsl;

impl DataStore {
/// Select the next webhook event in need of dispatching.
///
/// This performs a `SELECT ... FOR UPDATE SKIP LOCKED` on the
/// `webhook_event` table, returning the oldest webhook event which has not
/// yet been dispatched to receivers and which is not actively being
/// dispatched in another transaction.
// NOTE: it would be kinda nice if this query could also select the
// webhook receivers subscribed to this event, but I am not totally sure
// what the CRDB semantics of joining on another table in a `SELECT ... FOR
// UPDATE SKIP LOCKED` query are. We don't want to inadvertantly also lock
// the webhook receivers...
pub async fn webhook_event_select_for_dispatch(
&self,
conn: &async_bb8_diesel::Connection<DbConnection>,
) -> Result<Option<WebhookEvent>, diesel::result::Error> {
event_dsl::webhook_event
.filter(event_dsl::time_dispatched.is_null())
.order_by(event_dsl::time_created.asc())
.limit(1)
.for_update()
.skip_locked()
.select(WebhookEvent::as_select())
.get_result_async(conn)
.await
.optional()
}

/// Mark the webhook event with the provided UUID as dispatched.
pub async fn webhook_event_set_dispatched(
&self,
event_id: &WebhookEventUuid,
conn: &async_bb8_diesel::Connection<DbConnection>,
) -> Result<(), diesel::result::Error> {
diesel::update(event_dsl::webhook_event)
.filter(event_dsl::id.eq(event_id.into_untyped_uuid()))
.filter(event_dsl::time_dispatched.is_null())
.set(event_dsl::time_dispatched.eq(diesel::dsl::now))
.execute_async(conn)
.await
.map(|_| ()) // this should always be 1...
}
}
11 changes: 10 additions & 1 deletion schema/crdb/add-webhooks/up04.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@ CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx_subscription (
-- UUID of the webhook receiver (foreign key into
-- `omicron.public.webhook_rx`)
rx_id UUID NOT NULL,
-- An event class to which this receiver is subscribed.
-- An event class (or event class glob) to which this receiver is subscribed.
event_class STRING(512) NOT NULL,
-- The event class or event classs glob transformed into a patteern for use
-- in SQL `SIMILAR TO` clauses.
--
-- This is a bit interesting: users specify event class globs as sequences
-- of dot-separated segments which may be `*` to match any one segment or
-- `**` to match any number of segments. In order to match webhook events to
-- subscriptions within the database, we transform these into patterns that
-- can be used with a `SIMILAR TO` clause.
similar_to STRING(512) NOT NULL,
time_created TIMESTAMPTZ NOT NULL,

PRIMARY KEY (rx_id, event_class)
Expand Down
11 changes: 10 additions & 1 deletion schema/crdb/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4731,8 +4731,17 @@ CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx_subscription (
-- UUID of the webhook receiver (foreign key into
-- `omicron.public.webhook_rx`)
rx_id UUID NOT NULL,
-- An event class to which this receiver is subscribed.
-- An event class (or event class glob) to which this receiver is subscribed.
event_class STRING(512) NOT NULL,
-- The event class or event classs glob transformed into a patteern for use
-- in SQL `SIMILAR TO` clauses.
--
-- This is a bit interesting: users specify event class globs as sequences
-- of dot-separated segments which may be `*` to match any one segment or
-- `**` to match any number of segments. In order to match webhook events to
-- subscriptions within the database, we transform these into patterns that
-- can be used with a `SIMILAR TO` clause.
similar_to STRING(512) NOT NULL,
time_created TIMESTAMPTZ NOT NULL,

PRIMARY KEY (rx_id, event_class)
Expand Down

0 comments on commit 326b3f7

Please sign in to comment.