1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-23 15:51:06 +01:00

document settings crate (#271)

This commit is contained in:
Rob Ede 2022-07-31 21:12:19 +02:00 committed by GitHub
parent ab3f591307
commit b054733854
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 250 additions and 30 deletions

View File

@ -62,7 +62,7 @@ async fn main() -> std::io::Result<()> {
.await .await
} }
/// Initialize the logging infrastructure /// Initialize the logging infrastructure.
fn init_logger(settings: &Settings) { fn init_logger(settings: &Settings) {
if !settings.actix.enable_log { if !settings.actix.enable_log {
return; return;

View File

@ -21,19 +21,19 @@ num-workers = "default"
# Takes a string value: Either "default", or an integer N > 0 e.g. "6". # Takes a string value: Either "default", or an integer N > 0 e.g. "6".
backlog = "default" backlog = "default"
# Sets the maximum per-worker number of concurrent connections. All socket listeners # Sets the per-worker maximum number of concurrent connections. All socket listeners
# will stop accepting connections when this limit is reached for each worker. # will stop accepting connections when this limit is reached for each worker.
# By default max connections is set to a 25k. # By default max connections is set to a 25k.
# Takes a string value: Either "default", or an integer N > 0 e.g. "6". # Takes a string value: Either "default", or an integer N > 0 e.g. "6".
max-connections = "default" max-connections = "default"
# Sets the maximum per-worker concurrent connection establish process. All listeners # Sets the per-worker maximum concurrent connection establish process. All listeners
# will stop accepting connections when this limit is reached. It can be used to limit # will stop accepting connections when this limit is reached. It can be used to limit
# the global TLS CPU usage. By default max connections is set to a 256. # the global TLS CPU usage. By default max connections is set to a 256.
# Takes a string value: Either "default", or an integer N > 0 e.g. "6". # Takes a string value: Either "default", or an integer N > 0 e.g. "6".
max-connection-rate = "default" max-connection-rate = "default"
# Set server keep-alive setting. By default keep alive is set to 5 seconds. # Set server keep-alive preference. By default keep alive is set to 5 seconds.
# Takes a string value: Either "default", "disabled", "os", # Takes a string value: Either "default", "disabled", "os",
# or a string of the format "N seconds" where N is an integer > 0 e.g. "6 seconds". # or a string of the format "N seconds" where N is an integer > 0 e.g. "6 seconds".
keep-alive = "default" keep-alive = "default"

View File

@ -2,12 +2,20 @@ use std::{env::VarError, io, num::ParseIntError, path::PathBuf, str::ParseBoolEr
use toml::de::Error as TomlError; use toml::de::Error as TomlError;
/// Convenience type alias for `Result<T, AtError>`.
pub type AtResult<T> = std::result::Result<T, AtError>; pub type AtResult<T> = std::result::Result<T, AtError>;
#[derive(Clone, Debug)] /// Errors that can be returned from methods in this crate.
#[derive(Debug, Clone)]
pub enum AtError { pub enum AtError {
/// Environment variable does not exists or is invalid.
EnvVarError(VarError), EnvVarError(VarError),
/// File already exists on disk.
FileExists(PathBuf), FileExists(PathBuf),
/// Invalid value.
#[allow(missing_docs)]
InvalidValue { InvalidValue {
expected: &'static str, expected: &'static str,
got: String, got: String,
@ -15,10 +23,20 @@ pub enum AtError {
line: u32, line: u32,
column: u32, column: u32,
}, },
/// I/O error.
IoError(ioe::IoError), IoError(ioe::IoError),
/// Value is not a boolean.
ParseBoolError(ParseBoolError), ParseBoolError(ParseBoolError),
/// Value is not an integer.
ParseIntError(ParseIntError), ParseIntError(ParseIntError),
/// Value is not an address.
ParseAddressError(String), ParseAddressError(String),
/// Error deserializing as TOML.
TomlError(TomlError), TomlError(TomlError),
} }

View File

@ -1,9 +1,68 @@
//! Easily manage Actix Web's settings from a TOML file and environment variables. //! Easily manage Actix Web's settings from a TOML file and environment variables.
//!
//! To get started add a [`Settings::parse_toml("./Server.toml")`](Settings::parse_toml) call to the
//! top of your main function. This will create a template file with descriptions of all the
//! configurable settings. You can change or remove anything in that file and it will be picked up
//! the next time you run your application.
//!
//! Overriding parts of the file can be done from values using [`Settings::override_field`] or from
//! the environment using [`Settings::override_field_with_env_var`].
//!
//! # Examples
//!
//! See examples folder on GitHub for complete example.
//!
//! ```ignore
//! # use actix_web::{
//! # get,
//! # middleware::{Compress, Condition, Logger},
//! # web, App, HttpServer,
//! # };
//! use actix_settings::{ApplySettings as _, Mode, Settings};
//!
//! #[actix_web::main]
//! async fn main() -> std::io::Result<()> {
//! let mut settings = Settings::parse_toml("./Server.toml")
//! .expect("Failed to parse `Settings` from Server.toml");
//!
//! // If the environment variable `$APPLICATION__HOSTS` is set,
//! // have its value override the `settings.actix.hosts` setting:
//! Settings::override_field_with_env_var(&mut settings.actix.hosts, "APPLICATION__HOSTS")?;
//!
//! init_logger(&settings);
//!
//! HttpServer::new({
//! // clone settings into each worker thread
//! let settings = settings.clone();
//!
//! move || {
//! App::new()
//! // Include this `.wrap()` call for compression settings to take effect
//! .wrap(Condition::new(
//! settings.actix.enable_compression,
//! Compress::default(),
//! ))
//!
//! // add request logger
//! .wrap(Logger::default())
//!
//! // make `Settings` available to handlers
//! .app_data(web::Data::new(settings.clone()))
//!
//! // add request handlers as normal
//! .service(index)
//! }
//! })
//! // apply the `Settings` to Actix Web's `HttpServer`
//! .apply_settings(&settings)
//! .run()
//! .await
//! }
//! ```
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible, missing_debug_implementations)] #![warn(future_incompatible, missing_docs, missing_debug_implementations)]
// #![warn(missing_docs)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
@ -37,15 +96,21 @@ pub use self::settings::{
NumWorkers, Timeout, Tls, NumWorkers, Timeout, Tls,
}; };
/// Wrapper for server and application-specific settings.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(bound = "A: Deserialize<'de>")] #[serde(bound = "A: Deserialize<'de>")]
pub struct BasicSettings<A> { pub struct BasicSettings<A> {
/// Actix Web server settings.
pub actix: ActixSettings, pub actix: ActixSettings,
/// Application-specific settings.
pub application: A, pub application: A,
} }
/// Convenience type alias for [`BasicSettings`] with no defined application-specific settings.
pub type Settings = BasicSettings<NoSettings>; pub type Settings = BasicSettings<NoSettings>;
/// Marker type representing no defined application-specific settings.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[non_exhaustive] #[non_exhaustive]
pub struct NoSettings {/* NOTE: turning this into a unit struct will cause deserialization failures. */} pub struct NoSettings {/* NOTE: turning this into a unit struct will cause deserialization failures. */}
@ -54,14 +119,16 @@ impl<A> BasicSettings<A>
where where
A: de::DeserializeOwned, A: de::DeserializeOwned,
{ {
/// NOTE **DO NOT** mess with the ordering of the tables in this template. // NOTE **DO NOT** mess with the ordering of the tables in the default template.
/// Especially the `[application]` table needs to be last in order // Especially the `[application]` table needs to be last in order
/// for some tests to keep working. // for some tests to keep working.
/// Default settings file contents.
pub(crate) const DEFAULT_TOML_TEMPLATE: &'static str = include_str!("./defaults.toml"); pub(crate) const DEFAULT_TOML_TEMPLATE: &'static str = include_str!("./defaults.toml");
/// Parse an instance of `Self` from a `TOML` file located at `filepath`. /// Parse an instance of `Self` from a TOML file located at `filepath`.
/// If the file doesn't exist, it is generated from the default `TOML` ///
/// template, after which the newly generated file is read in and parsed. /// If the file doesn't exist, it is generated from the default TOML template, after which the
/// newly generated file is read in and parsed.
pub fn parse_toml<P>(filepath: P) -> AtResult<Self> pub fn parse_toml<P>(filepath: P) -> AtResult<Self>
where where
P: AsRef<Path>, P: AsRef<Path>,
@ -73,43 +140,62 @@ where
} }
let mut f = File::open(filepath)?; let mut f = File::open(filepath)?;
// TODO: don't bail on metadata fail
let mut contents = String::with_capacity(f.metadata()?.len() as usize); let mut contents = String::with_capacity(f.metadata()?.len() as usize);
f.read_to_string(&mut contents)?; f.read_to_string(&mut contents)?;
Ok(toml::from_str::<Self>(&contents)?) Ok(toml::from_str::<Self>(&contents)?)
} }
/// Parse an instance of `Self` straight from the default `TOML` template. /// Parse an instance of `Self` straight from the default TOML template.
// TODO: make infallible
// TODO: consider "template" rename
pub fn from_default_template() -> AtResult<Self> { pub fn from_default_template() -> AtResult<Self> {
Self::from_template(Self::DEFAULT_TOML_TEMPLATE) Self::from_template(Self::DEFAULT_TOML_TEMPLATE)
} }
/// Parse an instance of `Self` straight from the default `TOML` template. /// Parse an instance of `Self` straight from the default TOML template.
// TODO: consider "template" rename
pub fn from_template(template: &str) -> AtResult<Self> { pub fn from_template(template: &str) -> AtResult<Self> {
Ok(toml::from_str(template)?) Ok(toml::from_str(template)?)
} }
/// Write the default `TOML` template to a new file, to be located /// Writes the default TOML template to a new file, located at `filepath`.
/// at `filepath`. Return a `Error::FileExists(_)` error if a ///
/// file already exists at that location. /// # Errors
/// Returns a [`FileExists`](crate::AtError::FileExists) error if a file already exists at that
/// location.
pub fn write_toml_file<P>(filepath: P) -> AtResult<()> pub fn write_toml_file<P>(filepath: P) -> AtResult<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let filepath = filepath.as_ref(); let filepath = filepath.as_ref();
let contents = Self::DEFAULT_TOML_TEMPLATE.trim();
if filepath.exists() { if filepath.exists() {
return Err(AtError::FileExists(filepath.to_path_buf())); return Err(AtError::FileExists(filepath.to_path_buf()));
} }
let mut file = File::create(filepath)?; let mut file = File::create(filepath)?;
file.write_all(contents.as_bytes())?; file.write_all(Self::DEFAULT_TOML_TEMPLATE.trim().as_bytes())?;
file.flush()?; file.flush()?;
Ok(()) Ok(())
} }
/// Attempts to parse `value` and override the referenced `field`.
///
/// # Examples
/// ```
/// use actix_settings::{Settings, Mode};
///
/// # fn inner() -> actix_settings::AtResult<()> {
/// let mut settings = Settings::from_default_template()?;
/// assert_eq!(settings.actix.mode, Mode::Development);
///
/// Settings::override_field(&mut settings.actix.mode, "production")?;
/// assert_eq!(settings.actix.mode, Mode::Production);
/// # Ok(()) }
/// ```
pub fn override_field<F, V>(field: &mut F, value: V) -> AtResult<()> pub fn override_field<F, V>(field: &mut F, value: V) -> AtResult<()>
where where
F: Parse, F: Parse,
@ -119,6 +205,22 @@ where
Ok(()) Ok(())
} }
/// Attempts to read an environment variable, parse it, and override the referenced `field`.
///
/// # Examples
/// ```
/// use actix_settings::{Settings, Mode};
///
/// std::env::set_var("OVERRIDE__MODE", "production");
///
/// # fn inner() -> actix_settings::AtResult<()> {
/// let mut settings = Settings::from_default_template()?;
/// assert_eq!(settings.actix.mode, Mode::Development);
///
/// Settings::override_field_with_env_var(&mut settings.actix.mode, "OVERRIDE__MODE")?;
/// assert_eq!(settings.actix.mode, Mode::Production);
/// # Ok(()) }
/// ```
pub fn override_field_with_env_var<F, N>(field: &mut F, var_name: N) -> AtResult<()> pub fn override_field_with_env_var<F, N>(field: &mut F, var_name: N) -> AtResult<()>
where where
F: Parse, F: Parse,
@ -132,6 +234,7 @@ where
} }
} }
/// Extension trait for applying parsed settings to the server object.
pub trait ApplySettings { pub trait ApplySettings {
/// Apply a [`BasicSettings`] value to `self`. /// Apply a [`BasicSettings`] value to `self`.
/// ///
@ -200,9 +303,9 @@ where
self = match settings.actix.client_timeout { self = match settings.actix.client_timeout {
Timeout::Default => self, Timeout::Default => self,
Timeout::Milliseconds(n) => { Timeout::Milliseconds(n) => {
self.client_disconnect_timeout(Duration::from_millis(n as u64)) self.client_request_timeout(Duration::from_millis(n as u64))
} }
Timeout::Seconds(n) => self.client_disconnect_timeout(Duration::from_secs(n as u64)), Timeout::Seconds(n) => self.client_request_timeout(Duration::from_secs(n as u64)),
}; };
self = match settings.actix.client_shutdown { self = match settings.actix.client_shutdown {

View File

@ -2,7 +2,9 @@ use std::{path::PathBuf, str::FromStr};
use crate::AtError; use crate::AtError;
/// A specialized `FromStr` trait that returns [`AtError`] errors
pub trait Parse: Sized { pub trait Parse: Sized {
/// Parse `Self` from `string`.
fn parse(string: &str) -> Result<Self, AtError>; fn parse(string: &str) -> Result<Self, AtError>;
} }

View File

@ -37,9 +37,13 @@ static ADDR_LIST_REGEX: Lazy<Regex> = Lazy::new(|| {
.expect("Failed to compile regex: ADDRS_REGEX") .expect("Failed to compile regex: ADDRS_REGEX")
}); });
/// A host/port pair for the server to bind to.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
pub struct Address { pub struct Address {
/// Host part of address.
pub host: String, pub host: String,
/// Port part of address.
pub port: u16, pub port: u16,
} }

View File

@ -4,9 +4,20 @@ use serde::de;
use crate::{AtError, AtResult, Parse}; use crate::{AtError, AtResult, Parse};
/// The maximum number of pending connections.
///
/// This refers to the number of clients that can be waiting to be served. Exceeding this number
/// results in the client getting an error when attempting to connect. It should only affect servers
/// under significant load.
///
/// Generally set in the 642048 range. The default value is 2048. Takes a string value: Either
/// "default", or an integer N > 0 e.g. "6".
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Backlog { pub enum Backlog {
/// The default number of connections. See struct docs.
Default, Default,
/// A specific number of connections.
Manual(usize), Manual(usize),
} }

View File

@ -6,11 +6,24 @@ use serde::de;
use crate::{AtError, AtResult, Parse}; use crate::{AtError, AtResult, Parse};
/// The server keep-alive preference.
///
/// By default keep alive is set to 5 seconds. Takes a string value: Either "default", "disabled",
/// "os", or a string of the format "N seconds" where N is an integer > 0 e.g. "6 seconds".
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum KeepAlive { pub enum KeepAlive {
/// The default keep-alive as defined by Actix Web.
Default, Default,
/// Disable keep-alive.
Disabled, Disabled,
/// Let the OS determine keep-alive duration.
///
/// Note: this is usually quite long.
Os, Os,
/// A specific keep-alive duration (in seconds).
Seconds(usize), Seconds(usize),
} }

View File

@ -4,9 +4,17 @@ use serde::de;
use crate::{AtError, AtResult, Parse}; use crate::{AtError, AtResult, Parse};
/// The maximum per-worker concurrent TLS connection limit.
///
/// All listeners will stop accepting connections when this limit is reached. It can be used to
/// limit the global TLS CPU usage. By default max connections is set to a 256. Takes a string
/// value: Either "default", or an integer N > 0 e.g. "6".
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MaxConnectionRate { pub enum MaxConnectionRate {
/// The default connection limit. See struct docs.
Default, Default,
/// A specific connection limit.
Manual(usize), Manual(usize),
} }

View File

@ -4,9 +4,17 @@ use serde::de;
use crate::{AtError, AtResult, Parse}; use crate::{AtError, AtResult, Parse};
/// The maximum per-worker number of concurrent connections.
///
/// All socket listeners will stop accepting connections when this limit is reached for each worker.
/// By default max connections is set to a 25k. Takes a string value: Either "default", or an
/// integer N > 0 e.g. "6".
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MaxConnections { pub enum MaxConnections {
/// The default number of connections. See struct docs.
Default, Default,
/// A specific number of connections.
Manual(usize), Manual(usize),
} }

View File

@ -24,17 +24,42 @@ pub use self::tls::Tls;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct ActixSettings { pub struct ActixSettings {
/// List of addresses for the server to bind to.
pub hosts: Vec<Address>, pub hosts: Vec<Address>,
pub mode: mode::Mode,
/// Marker of intended deployment environment.
pub mode: Mode,
/// True if the [`Compress`](actix_web::middleware::Compress) middleware should be enabled.
pub enable_compression: bool, pub enable_compression: bool,
/// True if the [`Logger`](actix_web::middleware::Logger) middleware should be enabled.
pub enable_log: bool, pub enable_log: bool,
/// The number of workers that the server should start.
pub num_workers: NumWorkers, pub num_workers: NumWorkers,
/// The maximum number of pending connections.
pub backlog: Backlog, pub backlog: Backlog,
/// The per-worker maximum number of concurrent connections.
pub max_connections: MaxConnections, pub max_connections: MaxConnections,
/// The per-worker maximum concurrent TLS connection limit.
pub max_connection_rate: MaxConnectionRate, pub max_connection_rate: MaxConnectionRate,
/// Server keep-alive preference.
pub keep_alive: KeepAlive, pub keep_alive: KeepAlive,
/// Timeout duration for reading client request header.
pub client_timeout: Timeout, pub client_timeout: Timeout,
/// Timeout duration for connection shutdown.
pub client_shutdown: Timeout, pub client_shutdown: Timeout,
/// Timeout duration for graceful worker shutdown.
pub shutdown_timeout: Timeout, pub shutdown_timeout: Timeout,
/// TLS (HTTPS) configuration.
pub tls: Tls, pub tls: Tls,
} }

View File

@ -2,10 +2,14 @@ use serde::Deserialize;
use crate::{AtResult, Parse}; use crate::{AtResult, Parse};
/// Marker of intended deployment environment.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Mode { pub enum Mode {
/// Marks development environment.
Development, Development,
/// Marks production environment.
Production, Production,
} }

View File

@ -4,9 +4,16 @@ use serde::de;
use crate::{AtError, AtResult, Parse}; use crate::{AtError, AtResult, Parse};
/// The number of workers that the server should start.
///
/// By default the number of available logical cpu cores is used. Takes a string value: Either
/// "default", or an integer N > 0 e.g. "6".
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum NumWorkers { pub enum NumWorkers {
/// The default number of workers. See struct docs.
Default, Default,
/// A specific number of workers.
Manual(usize), Manual(usize),
} }

View File

@ -6,10 +6,16 @@ use serde::de;
use crate::{AtError, AtResult, Parse}; use crate::{AtError, AtResult, Parse};
/// A timeout duration in milliseconds or seconds.
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Timeout { pub enum Timeout {
/// The default timeout. Depends on context.
Default, Default,
/// Timeout in milliseconds.
Milliseconds(usize), Milliseconds(usize),
/// Timeout in seconds.
Seconds(usize), Seconds(usize),
} }
@ -34,18 +40,22 @@ impl Parse for Timeout {
}) })
} }
} }
match string { match string {
"default" => Ok(Timeout::Default), "default" => Ok(Timeout::Default),
string if !FMT.is_match(string) => invalid_value!(string), string if !FMT.is_match(string) => invalid_value!(string),
string => match (DIGITS.find(string), UNIT.find(string)) { string => match (DIGITS.find(string), UNIT.find(string)) {
(None, _) => invalid_value!(string), (None, _) | (_, None) => invalid_value!(string),
(_, None) => invalid_value!(string),
(Some(dmatch), Some(umatch)) => { (Some(digits), Some(unit)) => {
let digits = &string[dmatch.start()..dmatch.end()]; let digits = &string[digits.range()];
let unit = &string[umatch.start()..umatch.end()]; let unit = &string[unit.range()];
match (digits.parse(), unit) { match (digits.parse(), unit) {
(Ok(v), "milliseconds") => Ok(Timeout::Milliseconds(v)), (Ok(n), "milliseconds") => Ok(Timeout::Milliseconds(n)),
(Ok(v), "seconds") => Ok(Timeout::Seconds(v)), (Ok(n), "seconds") => Ok(Timeout::Seconds(n)),
_ => invalid_value!(string), _ => invalid_value!(string),
} }
} }

View File

@ -2,10 +2,17 @@ use std::path::PathBuf;
use serde::Deserialize; use serde::Deserialize;
/// TLS (HTTPS) configuration.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[doc(alias = "ssl", alias = "https")]
pub struct Tls { pub struct Tls {
/// Tru if accepting TLS connections should be enabled.
pub enabled: bool, pub enabled: bool,
/// Path to certificate `.pem` file.
pub certificate: PathBuf, pub certificate: PathBuf,
/// Path to private key `.pem` file.
pub private_key: PathBuf, pub private_key: PathBuf,
} }