Skip to content

Commit f006c42

Browse files
authored
[omdb] remove full scans in webhook delivery list, refactoring (#10758)
Depends on #10753. It turns out the query in `omdb db alert webhook delivery list` is also very full-table-scan-y, so this branch basically gives it the same treatment as I gave `alert list` in #10753. I also made similar clap changes here, so we get nice help text for possible values.
1 parent 935c547 commit f006c42

14 files changed

Lines changed: 586 additions & 82 deletions

File tree

dev-tools/omdb/src/bin/omdb/db/alert.rs

Lines changed: 163 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,29 @@ use diesel::query_dsl::QueryDsl;
2525
use nexus_db_lookup::LookupPath;
2626
use nexus_db_model::Alert;
2727
use nexus_db_model::AlertClass;
28+
use nexus_db_model::AlertDeliveryState;
29+
use nexus_db_model::AlertDeliveryTrigger;
2830
use nexus_db_model::AlertReceiver;
2931
use nexus_db_model::WebhookDelivery;
3032
use nexus_db_model::fm::RendezvousAlertCreated;
33+
use nexus_db_queries::authz;
3134
use nexus_db_queries::context::OpContext;
3235
use nexus_db_queries::db;
3336
use nexus_db_queries::db::DataStore;
37+
use nexus_db_queries::db::datastore::WebhookDeliveryFilters;
3438
use nexus_db_schema::schema::webhook_delivery::dsl as delivery_dsl;
3539
use nexus_db_schema::schema::webhook_delivery_attempt::dsl as attempt_dsl;
40+
use nexus_types::external_api::alert as types;
3641
use nexus_types::identity::Resource;
3742
use omicron_common::api::external::DataPageParams;
3843
use omicron_common::api::external::Generation;
3944
use omicron_common::api::external::NameOrId;
4045
use omicron_common::api::external::http_pagination::PaginatedBy;
46+
use omicron_uuid_kinds::AlertReceiverUuid;
4147
use omicron_uuid_kinds::AlertUuid;
4248
use omicron_uuid_kinds::CaseUuid;
4349
use omicron_uuid_kinds::GenericUuid;
50+
use std::sync::LazyLock;
4451
use tabled::Tabled;
4552
use uuid::Uuid;
4653

@@ -126,8 +133,8 @@ struct ClapAlertClass(nexus_types::alert::AlertClass);
126133

127134
impl clap::ValueEnum for ClapAlertClass {
128135
fn value_variants<'a>() -> &'a [Self] {
129-
static VALUE_VARIANTS: std::sync::LazyLock<Vec<ClapAlertClass>> =
130-
std::sync::LazyLock::new(|| {
136+
static VALUE_VARIANTS: LazyLock<Vec<ClapAlertClass>> =
137+
LazyLock::new(|| {
131138
nexus_types::alert::AlertClass::ALL_CLASSES
132139
.iter()
133140
.copied()
@@ -224,17 +231,26 @@ struct WebhookDeliveryListArgs {
224231
#[clap(long, short, alias = "rx")]
225232
receiver: Option<NameOrId>,
226233

227-
/// If present, select only deliveries for the given event.
228-
#[clap(long, short)]
229-
event: Option<AlertUuid>,
234+
/// If present, select only deliveries for the given alert.
235+
#[clap(
236+
long, short = 'A',
237+
alias = "event", // backwards compatibility
238+
)]
239+
alert: Option<AlertUuid>,
230240

231241
/// If present, select only deliveries in the provided state(s)
232-
#[clap(long = "state", short)]
233-
states: Vec<db::model::AlertDeliveryState>,
242+
///
243+
/// If multiple values are provided, deliveries in any of those states will
244+
/// be included in the output.
245+
#[clap(long = "state", short, num_args(1..), value_enum)]
246+
states: Vec<ClapAlertDeliveryState>,
234247

235248
/// If present, select only deliveries with the provided trigger(s)
236-
#[clap(long = "trigger", short)]
237-
triggers: Vec<db::model::AlertDeliveryTrigger>,
249+
///
250+
/// If multiple values are provided, deliveries with any of those triggers
251+
/// will be included in the output.
252+
#[clap(long = "trigger", short, num_args(1..), value_enum)]
253+
triggers: Vec<ClapAlertDeliveryTrigger>,
238254

239255
/// Include only delivery entries created before this timestamp
240256
#[clap(long, short)]
@@ -245,6 +261,78 @@ struct WebhookDeliveryListArgs {
245261
after: Option<DateTime<Utc>>,
246262
}
247263

264+
// Wrapper for implementing `clap::ValueEnum` for `nexus_types`'s version of
265+
// `AlertDeliveryState`.
266+
#[derive(Debug, Copy, Clone)]
267+
struct ClapAlertDeliveryState(types::AlertDeliveryState);
268+
269+
impl clap::ValueEnum for ClapAlertDeliveryState {
270+
fn value_variants<'a>() -> &'a [Self] {
271+
static VALUE_VARIANTS: LazyLock<Vec<ClapAlertDeliveryState>> =
272+
LazyLock::new(|| {
273+
types::AlertDeliveryState::ALL
274+
.iter()
275+
.copied()
276+
.map(ClapAlertDeliveryState)
277+
.collect()
278+
});
279+
&VALUE_VARIANTS
280+
}
281+
282+
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
283+
Some(clap::builder::PossibleValue::new(self.0.as_str()))
284+
}
285+
286+
fn from_str(input: &str, _ignore_case: bool) -> Result<Self, String> {
287+
input
288+
.parse::<types::AlertDeliveryState>()
289+
.map(ClapAlertDeliveryState)
290+
.map_err(|e| e.to_string())
291+
}
292+
}
293+
294+
impl From<&'_ ClapAlertDeliveryState> for AlertDeliveryState {
295+
fn from(&ClapAlertDeliveryState(state): &ClapAlertDeliveryState) -> Self {
296+
state.into()
297+
}
298+
}
299+
300+
#[derive(Debug, Copy, Clone)]
301+
struct ClapAlertDeliveryTrigger(types::AlertDeliveryTrigger);
302+
303+
impl clap::ValueEnum for ClapAlertDeliveryTrigger {
304+
fn value_variants<'a>() -> &'a [Self] {
305+
static VALUE_VARIANTS: LazyLock<Vec<ClapAlertDeliveryTrigger>> =
306+
LazyLock::new(|| {
307+
types::AlertDeliveryTrigger::ALL
308+
.iter()
309+
.copied()
310+
.map(ClapAlertDeliveryTrigger)
311+
.collect()
312+
});
313+
&VALUE_VARIANTS
314+
}
315+
316+
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
317+
Some(clap::builder::PossibleValue::new(self.0.as_str()))
318+
}
319+
320+
fn from_str(input: &str, _ignore_case: bool) -> Result<Self, String> {
321+
input
322+
.parse::<types::AlertDeliveryTrigger>()
323+
.map(ClapAlertDeliveryTrigger)
324+
.map_err(|e| e.to_string())
325+
}
326+
}
327+
328+
impl From<&'_ ClapAlertDeliveryTrigger> for AlertDeliveryTrigger {
329+
fn from(
330+
&ClapAlertDeliveryTrigger(trigger): &ClapAlertDeliveryTrigger,
331+
) -> Self {
332+
trigger.into()
333+
}
334+
}
335+
248336
#[derive(Debug, Args, Clone)]
249337
struct WebhookDeliveryInfoArgs {
250338
/// The ID of the delivery to show.
@@ -285,7 +373,10 @@ async fn cmd_db_webhook(
285373
}) => cmd_db_webhook_rx_list(opctx, datastore, fetch_opts, args).await,
286374
WebhookCommands::Delivery(WebhookDeliveryArgs {
287375
command: WebhookDeliveryCommands::List(args),
288-
}) => cmd_db_webhook_delivery_list(datastore, fetch_opts, args).await,
376+
}) => {
377+
cmd_db_webhook_delivery_list(opctx, datastore, fetch_opts, args)
378+
.await
379+
}
289380
WebhookCommands::Delivery(WebhookDeliveryArgs {
290381
command: WebhookDeliveryCommands::Info(args),
291382
}) => cmd_db_webhook_delivery_info(datastore, fetch_opts, args).await,
@@ -558,6 +649,7 @@ async fn cmd_db_webhook_rx_info(
558649
}
559650

560651
async fn cmd_db_webhook_delivery_list(
652+
opctx: &OpContext,
561653
datastore: &DataStore,
562654
fetch_opts: &DbFetchOptions,
563655
args: &WebhookDeliveryListArgs,
@@ -568,56 +660,63 @@ async fn cmd_db_webhook_delivery_list(
568660
receiver,
569661
states,
570662
triggers,
571-
event,
663+
alert,
572664
} = args;
573-
let conn = datastore.pool_connection_for_tests().await?;
574-
let mut query = delivery_dsl::webhook_delivery
575-
.limit(fetch_opts.fetch_limit.get().into())
576-
.order_by(delivery_dsl::time_created.desc())
577-
.into_boxed();
578-
579-
if let (Some(before), Some(after)) = (before, after) {
580-
anyhow::ensure!(
581-
after < before,
582-
"if both `--after` and `--before` are included, after must be
583-
earlier than before"
584-
);
585-
}
586-
587-
if let Some(before) = *before {
588-
query = query.filter(delivery_dsl::time_created.lt(before));
589-
}
590-
591-
if let Some(after) = *after {
592-
query = query.filter(delivery_dsl::time_created.gt(after));
593-
}
594-
595-
if let Some(receiver) = receiver {
596-
let rx =
597-
lookup_webhook_rx(datastore, receiver).await?.ok_or_else(|| {
598-
anyhow::anyhow!("no webhook receiver {receiver} found")
599-
})?;
600-
query = query.filter(delivery_dsl::rx_id.eq(rx.identity.id));
601-
}
602-
603-
if !states.is_empty() {
604-
query = query.filter(delivery_dsl::state.eq_any(states.clone()));
605-
}
606-
607-
if !triggers.is_empty() {
608-
query =
609-
query.filter(delivery_dsl::triggered_by.eq_any(triggers.clone()));
610-
}
611-
612-
if let Some(id) = event {
613-
query = query.filter(delivery_dsl::alert_id.eq(id.into_untyped_uuid()));
614-
}
665+
let filters = {
666+
let mut filters = WebhookDeliveryFilters::new();
667+
if let &Some(before) = before {
668+
filters = filters.before(before)?;
669+
}
670+
if let &Some(after) = after {
671+
filters = filters.after(after)?;
672+
}
673+
if let Some(receiver) = receiver {
674+
// Resolve (and authorize access to) the receiver before filtering
675+
// deliveries by it; `for_receiver` requires an `authz` resource.
676+
let lookup = LookupPath::new(opctx, datastore);
677+
let rx_lookup = match receiver {
678+
NameOrId::Id(id) => lookup.alert_receiver_id(
679+
AlertReceiverUuid::from_untyped_uuid(*id),
680+
),
681+
NameOrId::Name(name) => {
682+
lookup.alert_receiver_name_owned(name.clone().into())
683+
}
684+
};
685+
let (authz_rx,) =
686+
rx_lookup.lookup_for(authz::Action::Read).await.with_context(
687+
|| format!("looking up alert receiver {receiver} failed"),
688+
)?;
689+
filters = filters.for_receiver(&authz_rx);
690+
}
691+
if let &Some(alert) = alert {
692+
let (authz_alert,) = LookupPath::new(opctx, datastore)
693+
.alert_id(alert)
694+
.lookup_for(authz::Action::Read)
695+
.await
696+
.with_context(|| format!("looking up alert {alert} failed"))?;
697+
filters = filters.for_alert(&authz_alert);
698+
}
699+
if !states.is_empty() {
700+
filters = filters
701+
.with_states(states.iter().map(AlertDeliveryState::from));
702+
}
703+
if !triggers.is_empty() {
704+
filters = filters
705+
.with_triggers(triggers.iter().map(AlertDeliveryTrigger::from));
706+
}
707+
filters
708+
};
615709

616710
let ctx = || "listing webhook deliveries";
617711

618-
let deliveries = query
619-
.select(WebhookDelivery::as_select())
620-
.load_async(&*conn)
712+
let pagparams = DataPageParams {
713+
// Descending order shows the most recent delivery first
714+
direction: dropshot::PaginationOrder::Descending,
715+
..first_page(fetch_opts.fetch_limit)
716+
};
717+
718+
let deliveries = datastore
719+
.webhook_delivery_list(opctx, &filters, &pagparams)
621720
.await
622721
.with_context(ctx)?;
623722

@@ -640,7 +739,7 @@ async fn cmd_db_webhook_delivery_list(
640739
}
641740
}
642741

643-
let mut table = match (args.receiver.as_ref(), args.event) {
742+
let mut table = match (args.receiver.as_ref(), args.alert) {
644743
// Filtered by both receiver and event, so don't display either.
645744
(Some(_), Some(_)) => {
646745
tabled::Table::new(deliveries.iter().map(DeliveryRow::from))
@@ -693,8 +792,8 @@ impl From<&'_ WebhookDelivery> for DeliveryRow {
693792
fn from(d: &WebhookDelivery) -> Self {
694793
let WebhookDelivery {
695794
id,
696-
// event and receiver UUIDs are toggled on and off based on
697-
// whether or not we are filtering by receiver and event, so
795+
// alert and receiver UUIDs are toggled on and off based on
796+
// whether or not we are filtering by receiver and alert, so
698797
// ignore them here.
699798
alert_id: _,
700799
rx_id: _,
@@ -1170,7 +1269,11 @@ async fn cmd_db_alert_info(
11701269
..first_page(fetch_opts.fetch_limit)
11711270
};
11721271
let deliveries = datastore
1173-
.alert_list_webhook_deliveries(opctx, &authz_alert, &pagparams)
1272+
.webhook_delivery_list(
1273+
opctx,
1274+
&WebhookDeliveryFilters::default().for_alert(&authz_alert),
1275+
&pagparams,
1276+
)
11741277
.await
11751278
.with_context(ctx)?;
11761279

dev-tools/omdb/tests/test_all_output.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ async fn test_omdb_usage_errors() {
8383
&["db", "alert", "list", "--help"],
8484
// Nonexistent alert class
8585
&["db", "alert", "list", "--classes", "test.foo.bar", "test.foo.box"],
86+
&["db", "alert", "webhook", "delivery", "list", "--help"],
87+
// Nonexistent webhook delivery state and trigger
88+
&["db", "alert", "webhook", "delivery", "list", "--state", "bogus"],
89+
&["db", "alert", "webhook", "delivery", "list", "--trigger", "bogus"],
8690
&["db", "disks"],
8791
&["db", "dns"],
8892
&["db", "dns", "diff"],

0 commit comments

Comments
 (0)