use sqlx::{PgPool, postgres::PgPoolOptions};
use std::time::Duration;
use uuid::Uuid;
use crate::{
error::Result,
model::{DeliveryAttempt, Endpoint, EventStatus, NewEndpoint, QueueStats, UpdateEndpoint, WebhookEvent},
storage,
worker::{self, DeliveryWorker},
};
pub struct WebhookEngineBuilder {
database_url: Option<String>,
pool: Option<PgPool>,
allow_insecure_urls: bool,
batch_size: i64,
poll_interval: Duration,
max_connections: u32,
acquire_timeout: Duration,
http_timeout: Duration,
stuck_timeout: Duration,
}
impl Default for WebhookEngineBuilder {
fn default() -> Self {
Self {
database_url: None,
pool: None,
allow_insecure_urls: false,
batch_size: worker::DEFAULT_BATCH_SIZE,
poll_interval: worker::DEFAULT_POLL_INTERVAL,
max_connections: 20,
acquire_timeout: Duration::from_secs(10),
http_timeout: worker::DEFAULT_HTTP_TIMEOUT,
stuck_timeout: worker::DEFAULT_STUCK_TIMEOUT,
}
}
}
impl WebhookEngineBuilder {
pub fn database_url(mut self, url: impl Into<String>) -> Self {
self.database_url = Some(url.into());
self
}
pub fn pool(mut self, pool: PgPool) -> Self {
self.pool = Some(pool);
self
}
pub fn allow_insecure_urls(mut self) -> Self {
self.allow_insecure_urls = true;
self
}
pub fn batch_size(mut self, n: i64) -> Self {
assert!(n >= 1, "batch_size must be >= 1, got {n}");
self.batch_size = n;
self
}
pub fn poll_interval(mut self, d: Duration) -> Self {
self.poll_interval = d;
self
}
pub fn max_connections(mut self, n: u32) -> Self {
self.max_connections = n;
self
}
pub fn acquire_timeout(mut self, d: Duration) -> Self {
self.acquire_timeout = d;
self
}
pub fn http_timeout(mut self, d: Duration) -> Self {
self.http_timeout = d;
self
}
pub fn stuck_timeout(mut self, d: Duration) -> Self {
self.stuck_timeout = d;
self
}
pub async fn build(self) -> Result<WebhookEngine> {
Self::validate_timeouts(self.http_timeout, self.stuck_timeout);
let WebhookEngineBuilder {
database_url, pool, allow_insecure_urls,
batch_size, poll_interval, max_connections, acquire_timeout,
http_timeout, stuck_timeout,
} = self;
let pool = match pool {
Some(p) => p,
None => {
let url = database_url.expect("database_url or pool required");
PgPoolOptions::new()
.max_connections(max_connections)
.acquire_timeout(acquire_timeout)
.connect(&url)
.await?
}
};
Ok(make_engine(pool, allow_insecure_urls, batch_size, poll_interval, http_timeout, stuck_timeout))
}
fn validate_timeouts(http_timeout: Duration, stuck_timeout: Duration) {
assert!(
http_timeout < stuck_timeout,
"http_timeout ({:?}) must be less than stuck_timeout ({:?}). \
If http_timeout >= stuck_timeout the reaper resets events that \
are still waiting for an HTTP response.",
http_timeout,
stuck_timeout
);
}
pub fn build_sync(self) -> WebhookEngine {
Self::validate_timeouts(self.http_timeout, self.stuck_timeout);
let WebhookEngineBuilder {
pool, allow_insecure_urls, batch_size, poll_interval,
http_timeout, stuck_timeout, ..
} = self;
let pool = pool.expect("build_sync requires a pool, not a database_url");
make_engine(pool, allow_insecure_urls, batch_size, poll_interval, http_timeout, stuck_timeout)
}
}
fn event_status_to_str(status: &EventStatus) -> &'static str {
match status {
EventStatus::Pending => "pending",
EventStatus::Delivering => "delivering",
EventStatus::Delivered => "delivered",
EventStatus::Failed => "failed",
EventStatus::Dead => "dead",
}
}
fn make_engine(
pool: PgPool,
allow_insecure_urls: bool,
batch_size: i64,
poll_interval: Duration,
http_timeout: Duration,
stuck_timeout: Duration,
) -> WebhookEngine {
let worker = DeliveryWorker::new(pool.clone())
.with_batch_size(batch_size)
.with_poll_interval(poll_interval)
.with_http_timeout(http_timeout)
.with_stuck_timeout(stuck_timeout);
WebhookEngine { pool, worker, allow_insecure_urls }
}
pub struct WebhookEngine {
pool: PgPool,
worker: DeliveryWorker,
allow_insecure_urls: bool,
}
impl WebhookEngine {
pub fn builder() -> WebhookEngineBuilder {
WebhookEngineBuilder::default()
}
pub async fn new(database_url: &str) -> Result<Self> {
Self::builder().database_url(database_url).build().await
}
pub fn from_pool(pool: PgPool) -> Self {
let worker = DeliveryWorker::new(pool.clone());
Self { pool, worker, allow_insecure_urls: false }
}
pub async fn migrate(&self) -> Result<()> {
sqlx::migrate!()
.run(&self.pool)
.await
.map_err(|e| crate::error::HooksmithError::Database(e.into()))?;
Ok(())
}
pub async fn register(&self, url: &str, secret: &str) -> Result<Endpoint> {
let config = NewEndpoint {
url: url.to_owned(),
signing_secret: secret.to_owned(),
description: None,
max_attempts: None,
initial_delay_ms: None,
};
if self.allow_insecure_urls {
storage::create_endpoint_unchecked(&self.pool, config).await
} else {
storage::create_endpoint(&self.pool, config).await
}
}
pub async fn register_with(&self, config: NewEndpoint) -> Result<Endpoint> {
if self.allow_insecure_urls {
storage::create_endpoint_unchecked(&self.pool, config).await
} else {
storage::create_endpoint(&self.pool, config).await
}
}
pub async fn update_endpoint(&self, id: Uuid, update: UpdateEndpoint) -> Result<Endpoint> {
storage::update_endpoint(&self.pool, id, update, self.allow_insecure_urls).await
}
pub async fn disable_endpoint(&self, id: Uuid) -> Result<Endpoint> {
storage::update_endpoint(
&self.pool,
id,
UpdateEndpoint { enabled: Some(false), ..Default::default() },
self.allow_insecure_urls,
)
.await
}
pub async fn enable_endpoint(&self, id: Uuid) -> Result<Endpoint> {
storage::update_endpoint(
&self.pool,
id,
UpdateEndpoint { enabled: Some(true), ..Default::default() },
self.allow_insecure_urls,
)
.await
}
pub async fn send_idempotent(
&self,
event_type: &str,
payload: serde_json::Value,
endpoint_id: Uuid,
key: &str,
) -> Result<WebhookEvent> {
storage::enqueue_idempotent(&self.pool, endpoint_id, event_type, payload, key).await
}
pub async fn send_idempotent_in_tx(
&self,
event_type: &str,
payload: serde_json::Value,
endpoint_id: Uuid,
key: &str,
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<WebhookEvent> {
storage::enqueue_idempotent_in_tx(tx, endpoint_id, event_type, payload, key).await
}
pub async fn broadcast_idempotent(
&self,
event_type: &str,
payload: serde_json::Value,
key: &str,
) -> Result<Vec<WebhookEvent>> {
storage::broadcast_idempotent(&self.pool, event_type, payload, key).await
}
pub async fn broadcast(
&self,
event_type: &str,
payload: serde_json::Value,
) -> Result<Vec<WebhookEvent>> {
storage::broadcast(&self.pool, event_type, payload).await
}
pub async fn broadcast_in_tx(
&self,
event_type: &str,
payload: serde_json::Value,
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<WebhookEvent>> {
storage::broadcast_in_tx(tx, event_type, payload).await
}
pub async fn send(
&self,
event_type: &str,
payload: serde_json::Value,
endpoint_id: Uuid,
) -> Result<WebhookEvent> {
storage::enqueue(&self.pool, endpoint_id, event_type, payload).await
}
pub async fn send_in_tx(
&self,
event_type: &str,
payload: serde_json::Value,
endpoint_id: Uuid,
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<WebhookEvent> {
storage::enqueue_in_tx(tx, endpoint_id, event_type, payload).await
}
pub async fn endpoint(&self, id: Uuid) -> Result<Option<Endpoint>> {
storage::get_endpoint(&self.pool, id).await
}
pub async fn list_endpoints(&self) -> Result<Vec<Endpoint>> {
storage::list_endpoints(&self.pool).await
}
pub async fn list_endpoints_paged(&self, limit: i64, offset: i64) -> Result<Vec<Endpoint>> {
storage::list_endpoints_paged(&self.pool, limit, offset).await
}
pub async fn events_global(
&self,
status: EventStatus,
limit: i64,
offset: i64,
) -> Result<Vec<WebhookEvent>> {
let s = event_status_to_str(&status);
storage::events_global_by_status(&self.pool, s, limit, offset).await
}
pub async fn delete_endpoint(&self, id: Uuid) -> Result<()> {
storage::delete_endpoint(&self.pool, id).await
}
pub async fn queue_stats(&self) -> Result<QueueStats> {
storage::queue_stats(&self.pool).await
}
pub async fn events_by_status(
&self,
endpoint_id: Uuid,
status: EventStatus,
limit: i64,
offset: i64,
) -> Result<Vec<WebhookEvent>> {
let s = event_status_to_str(&status);
storage::events_by_status(&self.pool, endpoint_id, s, limit, offset).await
}
pub async fn delivery_log(&self, event_id: Uuid) -> Result<Vec<DeliveryAttempt>> {
storage::delivery_log(&self.pool, event_id).await
}
pub async fn dead_events(&self, endpoint_id: Uuid) -> Result<Vec<WebhookEvent>> {
storage::list_dead_events(&self.pool, endpoint_id).await
}
pub async fn dead_events_paged(
&self,
endpoint_id: Uuid,
limit: i64,
offset: i64,
) -> Result<Vec<WebhookEvent>> {
storage::dead_events_paged(&self.pool, endpoint_id, limit, offset).await
}
pub async fn retry_dead(&self, event_id: Uuid) -> Result<()> {
storage::retry_dead_event(&self.pool, event_id).await
}
pub async fn retry_all_dead(&self, endpoint_id: Uuid) -> Result<u64> {
storage::retry_all_dead(&self.pool, endpoint_id).await
}
pub async fn cleanup_delivered(&self, older_than: std::time::Duration) -> Result<u64> {
storage::cleanup_delivered(&self.pool, older_than.as_secs() as i64).await
}
pub async fn cleanup_dead(&self, older_than: std::time::Duration) -> Result<u64> {
storage::cleanup_dead(&self.pool, older_than.as_secs() as i64).await
}
pub async fn event(&self, id: Uuid) -> Result<Option<WebhookEvent>> {
storage::get_event(&self.pool, id).await
}
pub async fn recover_stuck_deliveries(&self, timeout: std::time::Duration) -> Result<u64> {
storage::recover_stuck_deliveries(&self.pool, timeout.as_secs() as i64).await
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
pub async fn run(&self) -> ! {
self.worker.run().await
}
pub async fn run_graceful<F: std::future::Future<Output = ()>>(&self, shutdown: F) {
self.worker.run_graceful(shutdown).await
}
pub async fn run_once(&self) -> Result<usize> {
self.worker.run_once().await
}
}