use std::time::Duration;
use crate::sysdb::DEFAULT_SCHEMA;
pub const DATABASE_URL_ENV: &str = "DBOS_DATABASE_URL";
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Serializer {
#[default]
RustSerde,
}
impl Serializer {
pub fn name(&self) -> &str {
match self {
Serializer::RustSerde => "rust_serde",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
pub app_name: String,
pub database_url: String,
pub max_connections: u32,
pub schema: String,
pub executor_id: Option<String>,
pub app_version: Option<String>,
pub serializer: Serializer,
pub use_listen_notify: bool,
pub migrate: bool,
pub polling_concurrency: Option<u32>,
pub outcome_poll_interval: Option<Duration>,
pub listen_queues: Option<Vec<String>>,
pub notification_coalesce: Option<Duration>,
}
impl Config {
pub fn new(app_name: impl Into<String>, database_url: impl Into<String>) -> Self {
Self {
app_name: app_name.into(),
app_version: None,
database_url: database_url.into(),
max_connections: 10,
schema: DEFAULT_SCHEMA.to_owned(),
executor_id: None,
serializer: Serializer::default(),
use_listen_notify: true,
migrate: true,
polling_concurrency: None,
outcome_poll_interval: None,
listen_queues: None,
notification_coalesce: None,
}
}
pub fn from_env(app_name: impl Into<String>) -> Self {
Self::new(
app_name,
std::env::var(DATABASE_URL_ENV).unwrap_or_default(),
)
}
pub(crate) fn validate(&self) -> crate::Result<()> {
if self.database_url.is_empty() {
return Err(crate::Error::Config(format!(
"no database URL: set `database_url`, or the {DATABASE_URL_ENV} environment \
variable if the configuration came from `Config::from_env`"
)));
}
if self.schema.is_empty() {
return Err(crate::Error::Config("`schema` cannot be empty".to_owned()));
}
if self.max_connections == 0 {
return Err(crate::Error::Config(
"`max_connections` cannot be zero".to_owned(),
));
}
if self.outcome_poll_interval == Some(Duration::ZERO) {
return Err(crate::Error::Config(
"`outcome_poll_interval` cannot be zero".to_owned(),
));
}
Ok(())
}
pub(crate) fn outcome_poll_interval(&self) -> Duration {
self.outcome_poll_interval
.unwrap_or(DEFAULT_OUTCOME_POLL_INTERVAL)
}
}
pub(crate) const DEFAULT_OUTCOME_POLL_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_serializer_names_itself_as_the_column_records_it() {
assert_eq!(Serializer::default().name(), "rust_serde");
}
#[test]
fn validation_reports_a_missing_url_by_the_name_of_the_variable_that_sets_it() {
let err = Config::new("app", "").validate().unwrap_err();
assert!(err.to_string().contains(DATABASE_URL_ENV), "{err}");
}
#[test]
fn a_zero_outcome_poll_interval_is_a_busy_loop_and_is_refused() {
let config = |outcome_poll_interval| Config {
outcome_poll_interval,
..Config::new("app", "postgres://x")
};
assert_eq!(
config(None).outcome_poll_interval(),
Duration::from_secs(1),
"the interval every implementation polls at"
);
assert_eq!(
config(Some(Duration::from_millis(250))).outcome_poll_interval(),
Duration::from_millis(250)
);
let err = config(Some(Duration::ZERO)).validate().unwrap_err();
assert!(
err.to_string().contains("outcome_poll_interval"),
"unlike `notification_coalesce`, zero here is not a setting: {err}"
);
}
#[test]
fn functional_update_is_the_documented_way_to_adjust_a_config() {
let config = Config {
max_connections: 20,
..Config::new("app", "postgres://x")
};
assert_eq!(config.max_connections, 20);
assert_eq!(config.app_name, "app");
}
}