Skip to content

Commit 3851a40

Browse files
committed
turn event class globs into SIMILAR TO patterns
needs testing, but i'd like to do that after finishing the DB queries that use it...
1 parent 47116cb commit 3851a40

File tree

6 files changed

+164
-3
lines changed

6 files changed

+164
-3
lines changed

nexus/db-model/src/schema.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2139,6 +2139,7 @@ table! {
21392139
webhook_rx_subscription (rx_id, event_class) {
21402140
rx_id -> Uuid,
21412141
event_class -> Text,
2142+
similar_to -> Text,
21422143
time_created -> Timestamptz,
21432144
}
21442145
}

nexus/db-model/src/webhook_rx.rs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::schema::{webhook_rx, webhook_rx_secret, webhook_rx_subscription};
66
use crate::typed_uuid::DbTypedUuid;
77
use chrono::{DateTime, Utc};
88
use db_macros::Resource;
9-
use omicron_uuid_kinds::WebhookReceiverKind;
9+
use omicron_uuid_kinds::{WebhookReceiverKind, WebhookReceiverUuid};
1010
use serde::{Deserialize, Serialize};
1111

1212
/// A webhook receiver configuration.
@@ -47,5 +47,87 @@ pub struct WebhookRxSecret {
4747
pub struct WebhookRxSubscription {
4848
pub rx_id: DbTypedUuid<WebhookReceiverKind>,
4949
pub event_class: String,
50+
pub similar_to: String,
5051
pub time_created: DateTime<Utc>,
5152
}
53+
54+
impl WebhookRxSubscription {
55+
pub fn new(rx_id: WebhookReceiverUuid, event_class: String) -> Self {
56+
fn seg2regex(segment: &str, similar_to: &mut String) {
57+
match segment {
58+
// Match one segment (i.e. any number of segment characters)
59+
"*" => similar_to.push_str("[a-zA-Z0-9\\_\\-]+"),
60+
// Match any number of segments
61+
"**" => similar_to.push('%'),
62+
// Match the literal segment.
63+
// Because `_` his a metacharacter in Postgres' SIMILAR TO
64+
// regexes, we've gotta go through and escape them.
65+
s => {
66+
for s in s.split_inclusive('_') {
67+
// Handle the fact that there might not be a `_` in the
68+
// string at all
69+
if let Some(s) = s.strip_suffix('_') {
70+
similar_to.push_str(s);
71+
similar_to.push_str("\\_");
72+
} else {
73+
similar_to.push_str(s);
74+
}
75+
}
76+
}
77+
}
78+
}
79+
80+
// The subscription's regex will always be at least as long as the event class.
81+
let mut similar_to = String::with_capacity(event_class.len());
82+
let mut segments = event_class.split('.');
83+
if let Some(segment) = segments.next() {
84+
seg2regex(segment, &mut similar_to);
85+
for segment in segments {
86+
similar_to.push('.'); // segment separator
87+
seg2regex(segment, &mut similar_to);
88+
}
89+
} else {
90+
// TODO(eliza): we should probably validate that the event class has
91+
// at least one segment...
92+
};
93+
94+
// `_` is a metacharacter in Postgres' SIMILAR TO regexes, so escape
95+
// them.
96+
97+
Self {
98+
rx_id: DbTypedUuid(rx_id),
99+
event_class,
100+
similar_to,
101+
time_created: Utc::now(),
102+
}
103+
}
104+
}
105+
106+
#[cfg(test)]
107+
mod test {
108+
use super::*;
109+
110+
#[test]
111+
fn test_event_class_glob_to_regex() {
112+
const CASES: &[(&str, &str)] = &[
113+
("foo.bar", "foo.bar"),
114+
("foo.*.bar", "foo.[a-zA-Z0-9\\_\\-]+.bar"),
115+
("foo.*", "foo.[a-zA-Z0-9\\_\\-]+"),
116+
("*.foo", "[a-zA-Z0-9\\_\\-]+.foo"),
117+
("foo.**.bar", "foo.%.bar"),
118+
("foo.**", "foo.%"),
119+
("foo_bar.baz", "foo\\_bar.baz"),
120+
("foo_bar.*.baz", "foo\\_bar.[a-zA-Z0-9\\_\\-]+.baz"),
121+
];
122+
let rx_id = WebhookReceiverUuid::new_v4();
123+
for (class, regex) in CASES {
124+
let subscription =
125+
WebhookRxSubscription::new(rx_id, dbg!(class).to_string());
126+
assert_eq!(
127+
dbg!(regex),
128+
dbg!(&subscription.similar_to),
129+
"event class {class:?} should produce the regex {regex:?}"
130+
);
131+
}
132+
}
133+
}

nexus/db-queries/src/db/datastore/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ mod vmm;
106106
mod volume;
107107
mod volume_repair;
108108
mod vpc;
109+
mod webhook_event;
109110
mod zpool;
110111

111112
pub use address_lot::AddressLotCreateResult;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! [`DataStore`] methods for webhook events and event delivery dispatching.
6+
7+
use super::DataStore;
8+
use crate::db::pool::DbConnection;
9+
use async_bb8_diesel::AsyncRunQueryDsl;
10+
use diesel::prelude::*;
11+
use diesel::result::OptionalExtension;
12+
use omicron_uuid_kinds::{GenericUuid, WebhookEventUuid};
13+
14+
use crate::db::model::WebhookEvent;
15+
use crate::db::schema::webhook_event::dsl as event_dsl;
16+
17+
impl DataStore {
18+
/// Select the next webhook event in need of dispatching.
19+
///
20+
/// This performs a `SELECT ... FOR UPDATE SKIP LOCKED` on the
21+
/// `webhook_event` table, returning the oldest webhook event which has not
22+
/// yet been dispatched to receivers and which is not actively being
23+
/// dispatched in another transaction.
24+
// NOTE: it would be kinda nice if this query could also select the
25+
// webhook receivers subscribed to this event, but I am not totally sure
26+
// what the CRDB semantics of joining on another table in a `SELECT ... FOR
27+
// UPDATE SKIP LOCKED` query are. We don't want to inadvertantly also lock
28+
// the webhook receivers...
29+
pub async fn webhook_event_select_for_dispatch(
30+
&self,
31+
conn: &async_bb8_diesel::Connection<DbConnection>,
32+
) -> Result<Option<WebhookEvent>, diesel::result::Error> {
33+
event_dsl::webhook_event
34+
.filter(event_dsl::time_dispatched.is_null())
35+
.order_by(event_dsl::time_created.asc())
36+
.limit(1)
37+
.for_update()
38+
.skip_locked()
39+
.select(WebhookEvent::as_select())
40+
.get_result_async(conn)
41+
.await
42+
.optional()
43+
}
44+
45+
/// Mark the webhook event with the provided UUID as dispatched.
46+
pub async fn webhook_event_set_dispatched(
47+
&self,
48+
event_id: &WebhookEventUuid,
49+
conn: &async_bb8_diesel::Connection<DbConnection>,
50+
) -> Result<(), diesel::result::Error> {
51+
diesel::update(event_dsl::webhook_event)
52+
.filter(event_dsl::id.eq(event_id.into_untyped_uuid()))
53+
.filter(event_dsl::time_dispatched.is_null())
54+
.set(event_dsl::time_dispatched.eq(diesel::dsl::now))
55+
.execute_async(conn)
56+
.await
57+
.map(|_| ()) // this should always be 1...
58+
}
59+
}

schema/crdb/add-webhooks/up04.sql

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@ CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx_subscription (
22
-- UUID of the webhook receiver (foreign key into
33
-- `omicron.public.webhook_rx`)
44
rx_id UUID NOT NULL,
5-
-- An event class to which this receiver is subscribed.
5+
-- An event class (or event class glob) to which this receiver is subscribed.
66
event_class STRING(512) NOT NULL,
7+
-- The event class or event classs glob transformed into a patteern for use
8+
-- in SQL `SIMILAR TO` clauses.
9+
--
10+
-- This is a bit interesting: users specify event class globs as sequences
11+
-- of dot-separated segments which may be `*` to match any one segment or
12+
-- `**` to match any number of segments. In order to match webhook events to
13+
-- subscriptions within the database, we transform these into patterns that
14+
-- can be used with a `SIMILAR TO` clause.
15+
similar_to STRING(512) NOT NULL,
716
time_created TIMESTAMPTZ NOT NULL,
817

918
PRIMARY KEY (rx_id, event_class)

schema/crdb/dbinit.sql

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4731,8 +4731,17 @@ CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx_subscription (
47314731
-- UUID of the webhook receiver (foreign key into
47324732
-- `omicron.public.webhook_rx`)
47334733
rx_id UUID NOT NULL,
4734-
-- An event class to which this receiver is subscribed.
4734+
-- An event class (or event class glob) to which this receiver is subscribed.
47354735
event_class STRING(512) NOT NULL,
4736+
-- The event class or event classs glob transformed into a patteern for use
4737+
-- in SQL `SIMILAR TO` clauses.
4738+
--
4739+
-- This is a bit interesting: users specify event class globs as sequences
4740+
-- of dot-separated segments which may be `*` to match any one segment or
4741+
-- `**` to match any number of segments. In order to match webhook events to
4742+
-- subscriptions within the database, we transform these into patterns that
4743+
-- can be used with a `SIMILAR TO` clause.
4744+
similar_to STRING(512) NOT NULL,
47364745
time_created TIMESTAMPTZ NOT NULL,
47374746

47384747
PRIMARY KEY (rx_id, event_class)

0 commit comments

Comments
 (0)