Enforce type safety in config (#63)

* Add type-safe wrapper types to deployement_cfg

* Before deleting redundnat macros

* Store error messages as data

* Significant progress on type safety

* Add type safety to RedisConfig
This commit is contained in:
Daniel Sockwell 2019-10-08 20:35:26 -04:00 committed by GitHub
parent e19524738b
commit c281418f25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 493 additions and 232 deletions

55
Cargo.lock generated
View File

@ -416,6 +416,8 @@ dependencies = [
"serde 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"strum 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"strum_macros 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)",
"url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1059,6 +1061,14 @@ dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "proc-macro2"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quick-error"
version = "1.2.2"
@ -1072,6 +1082,14 @@ dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quote"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand"
version = "0.6.5"
@ -1467,6 +1485,22 @@ dependencies = [
"unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "strum"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "strum_macros"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "subtle"
version = "1.0.0"
@ -1482,6 +1516,16 @@ dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "syn"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "synstructure"
version = "0.10.1"
@ -1859,6 +1903,11 @@ name = "unicode-xid"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "url"
version = "2.1.0"
@ -2127,8 +2176,10 @@ dependencies = [
"checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b"
"checksum pretty_env_logger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df8b3f4e0475def7d9c2e5de8e5a1306949849761e107b360d03e98eafaffd61"
"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
"checksum proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "90cf5f418035b98e655e9cdb225047638296b862b42411c4e45bb88d700f7fc0"
"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
"checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db"
"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
"checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412"
"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
@ -2178,8 +2229,11 @@ dependencies = [
"checksum state_machine_future 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "530e1d624baae485bce12e6647acb76aafa253346ee8a16751974eed5a24b13d"
"checksum string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b639411d0b9c738748b5397d5ceba08e648f4f1992231aa859af1a017f31f60b"
"checksum stringprep 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1"
"checksum strum 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6138f8f88a16d90134763314e3fc76fa3ed6a7db4725d6acf9a3ef95a3188d22"
"checksum strum_macros 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0054a7df764039a6cd8592b9de84be4bec368ff081d203a7d5371cbfa8e65c81"
"checksum subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
"checksum syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)" = "a1393e4a97a19c01e900df2aec855a29f71cf02c402e2f443b8d2747c25c5dbe"
"checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf"
"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
"checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e"
@ -2216,6 +2270,7 @@ dependencies = [
"checksum unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1967f4cdfc355b37fd76d2a954fb2ed3871034eb4f26d60537d88795cfc332a9"
"checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20"
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
"checksum url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75b414f6c464c879d7f9babf951f23bc3743fb7313c081b2e6ca719067ea9d61"
"checksum urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3df3561629a8bb4c57e5a2e4c43348d9e29c7c29d9b1c4c1f47166deca8f37ed"
"checksum utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7"

View File

@ -19,6 +19,8 @@ uuid = { version = "0.7", features = ["v4"] }
dotenv = "0.14.0"
postgres-openssl = { git = "https://github.com/sfackler/rust-postgres.git"}
url = "2.1.0"
strum = "0.16.0"
strum_macros = "0.16.0"
[dev-dependencies]
criterion = "0.3"

View File

@ -1,102 +1,31 @@
use crate::{err, maybe_update};
use std::{
collections::HashMap,
fmt,
net::{IpAddr, Ipv4Addr},
os::unix::net::UnixListener,
time::Duration,
};
use super::{deployment_cfg_types::*, EnvVar};
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct DeploymentConfig<'a> {
pub env: String,
pub log_level: String,
pub address: IpAddr,
pub port: u16,
pub unix_socket: Option<UnixListener>,
pub env: Env,
pub log_level: LogLevel,
pub address: FlodgattAddr,
pub port: Port,
pub unix_socket: Socket,
pub cors: Cors<'a>,
pub sse_interval: Duration,
pub ws_interval: Duration,
pub sse_interval: SseInterval,
pub ws_interval: WsInterval,
}
pub struct Cors<'a> {
pub allowed_headers: Vec<&'a str>,
pub allowed_methods: Vec<&'a str>,
}
impl fmt::Debug for Cors<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"allowed headers: {:?}\n allowed methods: {:?}",
self.allowed_headers, self.allowed_methods
)
}
}
impl Default for DeploymentConfig<'_> {
fn default() -> Self {
Self {
env: "development".to_string(),
log_level: "error".to_string(),
address: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
port: 4000,
unix_socket: None,
cors: Cors {
allowed_methods: vec!["GET", "OPTIONS"],
allowed_headers: vec!["Authorization", "Accept", "Cache-Control"],
},
sse_interval: Duration::from_millis(100),
ws_interval: Duration::from_millis(100),
}
}
}
impl DeploymentConfig<'_> {
pub fn from_env(env_vars: HashMap<String, String>) -> Self {
Self::default()
.maybe_update_env(env_vars.get("NODE_ENV").map(String::from))
.maybe_update_env(env_vars.get("RUST_ENV").map(String::from))
.maybe_update_address(
env_vars
.get("BIND")
.map(|a| err::unwrap_or_die(a.parse().ok(), "BIND must be a valid address")),
)
.maybe_update_port(
env_vars
.get("PORT")
.map(|port| err::unwrap_or_die(port.parse().ok(), "PORT must be a number")),
)
.maybe_update_unix_socket(
env_vars
.get("SOCKET")
.map(|s| UnixListener::bind(s).unwrap()),
)
.maybe_update_log_level(env_vars.get("RUST_LOG").map(|level| match level.as_ref() {
l @ "trace" | l @ "debug" | l @ "info" | l @ "warn" | l @ "error" => l.to_string(),
_ => err::die_with_msg("Invalid log level specified"),
}))
.maybe_update_sse_interval(
env_vars
.get("SSE_UPDATE_INTERVAL")
.map(|str| Duration::from_millis(str.parse().unwrap())),
)
.maybe_update_ws_interval(
env_vars
.get("WS_UPDATE_INTERVAL")
.map(|str| Duration::from_millis(str.parse().unwrap())),
)
.log()
}
maybe_update!(maybe_update_env; env: String);
maybe_update!(maybe_update_port; port: u16);
maybe_update!(maybe_update_address; address: IpAddr);
maybe_update!(maybe_update_unix_socket; Some(unix_socket: UnixListener));
maybe_update!(maybe_update_log_level; log_level: String);
maybe_update!(maybe_update_sse_interval; sse_interval: Duration);
maybe_update!(maybe_update_ws_interval; ws_interval: Duration);
fn log(self) -> Self {
log::warn!("Using deployment configuration:\n {:#?}", &self);
self
pub fn from_env(env: EnvVar) -> Self {
let mut cfg = Self {
env: Env::default().maybe_update(env.get("NODE_ENV")),
log_level: LogLevel::default().maybe_update(env.get("RUST_LOG")),
address: FlodgattAddr::default().maybe_update(env.get("BIND")),
port: Port::default().maybe_update(env.get("PORT")),
unix_socket: Socket::default().maybe_update(env.get("SOCKET")),
sse_interval: SseInterval::default().maybe_update(env.get("SSE_FREQ")),
ws_interval: WsInterval::default().maybe_update(env.get("WS_FREQ")),
cors: Cors::default(),
};
cfg.env = cfg.env.maybe_update(env.get("RUST_ENV"));
log::info!("Using deployment configuration:\n {:#?}", &cfg);
cfg
}
}

View File

@ -0,0 +1,104 @@
use crate::from_env_var;
use std::{
fmt,
net::{IpAddr, Ipv4Addr},
os::unix::net::UnixListener,
str::FromStr,
time::Duration,
};
use strum_macros::{EnumString, EnumVariantNames};
from_env_var!(
/// The current environment, which controls what file to read other ENV vars from
let name = Env;
let default: EnvInner = EnvInner::Development;
let (env_var, allowed_values) = ("RUST_ENV", format!("one of: {:?}", EnvInner::variants()));
let from_str = |s| EnvInner::from_str(s).ok();
);
from_env_var!(
/// The address to run Flodgatt on
let name = FlodgattAddr;
let default: IpAddr = IpAddr::V4("127.0.0.1".parse().expect("hardcoded"));
let (env_var, allowed_values) = ("BIND", "a valid address (e.g., 127.0.0.1)".to_string());
let from_str = |s| match s {
"localhost" => Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
_ => s.parse().ok(),
};
);
from_env_var!(
/// How verbosely Flodgatt should log messages
let name = LogLevel;
let default: LogLevelInner = LogLevelInner::Warn;
let (env_var, allowed_values) = ("RUST_LOG", "a valid address (e.g., 127.0.0.1)".to_string());
let from_str = |s| LogLevelInner::from_str(s).ok();
);
from_env_var!(
/// A Unix Socket to use in place of a local address
let name = Socket;
let default: Option<UnixListener> = None;
let (env_var, allowed_values) = ("SOCKET", "a valid Unix Socket".to_string());
let from_str = |s| match UnixListener::bind(s).ok() {
Some(socket) => Some(Some(socket)),
None => None,
};
);
from_env_var!(
/// The time between replies sent via WebSocket
let name = WsInterval;
let default: Duration = Duration::from_millis(100);
let (env_var, allowed_values) = ("WS_FREQ", "a valid Unix Socket".to_string());
let from_str = |s| s.parse().map(Duration::from_millis).ok();
);
from_env_var!(
/// The time between replies sent via Server Sent Events
let name = SseInterval;
let default: Duration = Duration::from_millis(100);
let (env_var, allowed_values) = ("WS_FREQ", "a number of milliseconds".to_string());
let from_str = |s| s.parse().map(Duration::from_millis).ok();
);
from_env_var!(
/// The port to run Flodgatt on
let name = Port;
let default: u16 = 4000;
let (env_var, allowed_values) = ("PORT", "a number between 0 and 65535".to_string());
let from_str = |s| s.parse().ok();
);
/// Permissions for Cross Origin Resource Sharing (CORS)
pub struct Cors<'a> {
pub allowed_headers: Vec<&'a str>,
pub allowed_methods: Vec<&'a str>,
}
impl std::default::Default for Cors<'_> {
fn default() -> Self {
Self {
allowed_methods: vec!["GET", "OPTIONS"],
allowed_headers: vec!["Authorization", "Accept", "Cache-Control"],
}
}
}
impl fmt::Debug for Cors<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"allowed headers: {:?}\n allowed methods: {:?}",
self.allowed_headers, self.allowed_methods
)
}
}
#[derive(EnumString, EnumVariantNames, Debug)]
#[strum(serialize_all = "snake_case")]
pub enum LogLevelInner {
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(EnumString, EnumVariantNames, Debug)]
#[strum(serialize_all = "snake_case")]
pub enum EnvInner {
Production,
Development,
}

View File

@ -1,17 +1,58 @@
//! Configuration defaults. All settings with the prefix of `DEFAULT_` can be overridden
//! by an environmental variable of the same name without that prefix (either by setting
//! the variable at runtime or in the `.env` file)
mod deployment_cfg;
mod deployment_cfg_types;
mod postgres_cfg;
mod redis_cfg;
mod redis_cfg_types;
pub use self::{
deployment_cfg::DeploymentConfig, postgres_cfg::PostgresConfig, redis_cfg::RedisConfig,
deployment_cfg::DeploymentConfig,
postgres_cfg::PostgresConfig,
redis_cfg::RedisConfig,
redis_cfg_types::{RedisInterval, RedisNamespace},
};
use std::collections::HashMap;
use url::Url;
// **NOTE**: Polling Redis is much more time consuming than polling the `Receiver`
// (on the order of 10ms rather than 50μs). Thus, changing this setting
// would be a good place to start for performance improvements at the cost
// of delaying all updates.
pub struct EnvVar(pub HashMap<String, String>);
impl std::ops::Deref for EnvVar {
type Target = HashMap<String, String>;
fn deref(&self) -> &HashMap<String, String> {
&self.0
}
}
impl Clone for EnvVar {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl EnvVar {
fn update_with_url(mut self, url_str: &str) -> Self {
let url = Url::parse(url_str).unwrap();
let none_if_empty = |s: String| if s.is_empty() { None } else { Some(s) };
self.maybe_add_env_var("REDIS_PORT", url.port());
self.maybe_add_env_var("REDIS_PASSWORD", url.password());
self.maybe_add_env_var("REDIS_USERNAME", none_if_empty(url.username().to_string()));
self.maybe_add_env_var("REDIS_DB", none_if_empty(url.path()[1..].to_string()));
for (k, v) in url.query_pairs().into_owned() {
match k.to_string().as_str() {
"password" => self.maybe_add_env_var("REDIS_PASSWORD", Some(v.to_string())),
"db" => self.maybe_add_env_var("REDIS_DB", Some(v.to_string())),
_ => crate::err::die_with_msg(format!(
r"Unsupported parameter {} in REDIS_URL.
Flodgatt supports only `password` and `db` parameters.",
k
)),
}
}
self
}
fn maybe_add_env_var(&mut self, key: &str, maybe_value: Option<impl ToString>) {
if let Some(value) = maybe_value {
self.0.insert(key.to_string(), value.to_string());
}
}
}
#[macro_export]
macro_rules! maybe_update {
@ -29,3 +70,44 @@ macro_rules! maybe_update {
None => Self { ..self }
}
})}
#[macro_export]
macro_rules! from_env_var {
($(#[$outer:meta])*
let name = $name:ident;
let default: $type:ty = $inner:expr;
let (env_var, allowed_values) = ($env_var:tt, $allowed_values:expr);
let from_str = |$arg:ident| $body:expr;
) => {
pub struct $name(pub $type);
impl std::fmt::Debug for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl std::ops::Deref for $name {
type Target = $type;
fn deref(&self) -> &$type {
&self.0
}
}
impl std::default::Default for $name {
fn default() -> Self {
$name($inner)
}
}
impl $name {
fn inner_from_str($arg: &str) -> Option<$type> {
$body
}
pub fn maybe_update(self, var: Option<&String>) -> Self {
if let Some(value) = var {
Self(Self::inner_from_str(value).unwrap_or_else(|| {
crate::err::env_var_fatal($env_var, value, $allowed_values)
}))
} else {
self
}
}
}
};
}

View File

@ -1,5 +1,5 @@
use super::EnvVar;
use crate::{err, maybe_update};
use std::collections::HashMap;
use url::Url;
#[derive(Debug)]
@ -30,7 +30,7 @@ fn none_if_empty(item: &str) -> Option<String> {
impl PostgresConfig {
/// Configure Postgres and return a connection
pub fn from_env(env_vars: HashMap<String, String>) -> Self {
pub fn from_env(env_vars: EnvVar) -> Self {
// use openssl::ssl::{SslConnector, SslMethod};
// use postgres_openssl::MakeTlsConnector;
// let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();

View File

@ -1,87 +1,50 @@
use crate::{err, maybe_update};
use std::{collections::HashMap, time::Duration};
use url::Url;
use super::redis_cfg_types::*;
use crate::config::EnvVar;
fn none_if_empty(item: &str) -> Option<String> {
Some(item).filter(|i| !i.is_empty()).map(String::from)
}
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct RedisConfig {
pub user: Option<String>,
pub password: Option<String>,
pub port: u16,
pub host: String,
pub db: Option<String>,
pub namespace: Option<String>,
pub polling_interval: Duration,
}
impl Default for RedisConfig {
fn default() -> Self {
Self {
user: None,
password: None,
db: None,
port: 6379,
host: "127.0.0.1".to_string(),
namespace: None,
polling_interval: Duration::from_millis(100),
}
}
pub user: RedisUser,
pub password: RedisPass,
pub port: RedisPort,
pub host: RedisHost,
pub db: RedisDb,
pub namespace: RedisNamespace,
// **NOTE**: Polling Redis is much more time consuming than polling the `Receiver` (~1ms
// compared to ~50μs). Thus, changing this setting with REDIS_POLL_INTERVAL may be a good
// place to start for performance improvements at the cost of delaying all updates.
pub polling_interval: RedisInterval,
}
impl RedisConfig {
pub fn from_env(env_vars: HashMap<String, String>) -> Self {
match env_vars.get("REDIS_URL") {
Some(url) => {
log::warn!("REDIS_URL env variable set. Connecting to Redis with that URL and ignoring any values set in REDIS_HOST or DB_PORT.");
Self::from_url(Url::parse(url).unwrap())
}
None => RedisConfig::default()
.maybe_update_host(env_vars.get("REDIS_HOST").map(String::from))
.maybe_update_port(env_vars.get("REDIS_PORT").map(|p| err::unwrap_or_die(
p.parse().ok(),"REDIS_PORT must be a number."))),
}
.maybe_update_namespace(env_vars.get("REDIS_NAMESPACE").map(String::from))
.maybe_update_polling_interval(env_vars.get("REDIS_POLL_INTERVAL")
.map(|str| Duration::from_millis(str.parse().unwrap()))).log()
}
const USER_SET_WARNING: &'static str =
"Redis user specified, but Redis did not ask for a username. Ignoring it.";
const DB_SET_WARNING: &'static str =
r"Redis database specified, but PubSub connections do not use databases.
For similar functionality, you may wish to set a REDIS_NAMESPACE";
fn from_url(url: Url) -> Self {
let mut password = url.password().as_ref().map(|str| str.to_string());
let mut db = none_if_empty(&url.path()[1..]);
for (k, v) in url.query_pairs() {
match k.to_string().as_str() {
"password" => { password = Some(v.to_string());},
"db" => { db = Some(v.to_string())},
_ => { err::die_with_msg(format!("Unsupported parameter {} in REDIS_URL.\n Flodgatt supports only `password` and `db` parameters.", k))}
}
}
let user = none_if_empty(url.username());
if let Some(user) = &user {
log::error!(
"Username {} provided, but Redis does not need a username. Ignoring it",
user
);
}
RedisConfig {
user,
host: err::unwrap_or_die(url.host_str(), "Missing or invalid host in REDIS_URL")
.to_string(),
port: err::unwrap_or_die(url.port(), "Missing or invalid port in REDIS_URL"),
namespace: None,
password,
db,
polling_interval: Duration::from_millis(100),
}
}
pub fn from_env(env: EnvVar) -> Self {
let env = match env.get("REDIS_URL").map(|s| s.clone()) {
Some(url_str) => env.update_with_url(&url_str),
None => env,
};
maybe_update!(maybe_update_host; host: String);
maybe_update!(maybe_update_port; port: u16);
maybe_update!(maybe_update_namespace; Some(namespace: String));
maybe_update!(maybe_update_polling_interval; polling_interval: Duration);
let cfg = RedisConfig {
user: RedisUser::default().maybe_update(env.get("REDIS_USER")),
password: RedisPass::default().maybe_update(env.get("REDIS_PASSWORD")),
port: RedisPort::default().maybe_update(env.get("REDIS_PORT")),
host: RedisHost::default().maybe_update(env.get("REDIS_HOST")),
db: RedisDb::default().maybe_update(env.get("REDIS_DB")),
namespace: RedisNamespace::default().maybe_update(env.get("REDIS_NAMESPACE")),
polling_interval: RedisInterval::default().maybe_update(env.get("REDIS_POLL_INTERVAL")),
};
fn log(self) -> Self {
log::warn!("Redis configuration:\n{:#?},", &self);
self
if cfg.db.is_some() {
log::warn!("{}", Self::DB_SET_WARNING);
}
if cfg.user.is_some() {
log::warn!("{}", Self::USER_SET_WARNING);
}
log::info!("Redis configuration:\n{:#?},", &cfg);
cfg
}
}

View File

@ -0,0 +1,60 @@
use crate::from_env_var;
use std::{
net::{IpAddr, Ipv4Addr},
time::Duration,
};
//use std::{fmt, net::IpAddr, os::unix::net::UnixListener, str::FromStr, time::Duration};
//use strum_macros::{EnumString, EnumVariantNames};
from_env_var!(
/// The host address where Redis is running
let name = RedisHost;
let default: IpAddr = IpAddr::V4("127.0.0.1".parse().expect("hardcoded"));
let (env_var, allowed_values) = ("REDIS_HOST", "a valid address (e.g., 127.0.0.1)".to_string());
let from_str = |s| match s {
"localhost" => Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
_ => s.parse().ok(),
};
);
from_env_var!(
/// The port Redis is running on
let name = RedisPort;
let default: u16 = 6379;
let (env_var, allowed_values) = ("REDIS_PORT", "a number between 0 and 65535".to_string());
let from_str = |s| s.parse().ok();
);
from_env_var!(
/// How frequently to poll Redis
let name = RedisInterval;
let default: Duration = Duration::from_millis(100);
let (env_var, allowed_values) = ("REDIS_POLL_INTERVAL", "a number of milliseconds".to_string());
let from_str = |s| s.parse().map(Duration::from_millis).ok();
);
from_env_var!(
/// The password to use for Redis
let name = RedisPass;
let default: Option<String> = None;
let (env_var, allowed_values) = ("REDIS_PASSWORD", "any string".to_string());
let from_str = |s| Some(Some(s.to_string()));
);
from_env_var!(
/// An optional Redis Namespace
let name = RedisNamespace;
let default: Option<String> = None;
let (env_var, allowed_values) = ("REDIS_NAMESPACE", "any string".to_string());
let from_str = |s| Some(Some(s.to_string()));
);
from_env_var!(
/// A user for Redis (not supported)
let name = RedisUser;
let default: Option<String> = None;
let (env_var, allowed_values) = ("REDIS_USER", "any string".to_string());
let from_str = |s| Some(Some(s.to_string()));
);
from_env_var!(
/// The database to use with Redis (no current effect for PubSub connections)
let name = RedisDb;
let default: Option<String> = None;
let (env_var, allowed_values) = ("REDIS_DB", "any string".to_string());
let from_str = |s| Some(Some(s.to_string()));
);

View File

@ -6,6 +6,17 @@ pub fn die_with_msg(msg: impl Display) -> ! {
std::process::exit(1);
}
pub fn env_var_fatal(env_var: &str, supplied_value: &str, allowed_values: String) -> ! {
eprintln!(
r"FATAL ERROR: {var} is set to `{value}`, which is invalid.
{var} must be {allowed_vals}.",
var = env_var,
value = supplied_value,
allowed_vals = allowed_values
);
std::process::exit(1);
}
#[macro_export]
macro_rules! dbg_and_die {
($msg:expr) => {
@ -14,7 +25,7 @@ macro_rules! dbg_and_die {
std::process::exit(1);
};
}
pub fn unwrap_or_die<T: Display>(s: Option<T>, msg: &str) -> T {
pub fn unwrap_or_die<T>(s: Option<T>, msg: &str) -> T {
s.unwrap_or_else(|| {
eprintln!("FATAL ERROR: {}", msg);
std::process::exit(1)

View File

@ -14,11 +14,12 @@ fn main() {
Some("development") | None => ".env",
Some(_) => err::die_with_msg("Unknown ENV variable specified.\n Valid options are: `production` or `development`."),
}).ok();
let env_vars: HashMap<_, _> = dotenv::vars().collect();
let env_vars_map: HashMap<_, _> = dotenv::vars().collect();
let env_vars = config::EnvVar(env_vars_map);
pretty_env_logger::init();
let cfg = config::DeploymentConfig::from_env(env_vars.clone());
let redis_cfg = config::RedisConfig::from_env(env_vars.clone());
let cfg = config::DeploymentConfig::from_env(env_vars.clone());
let postgres_cfg = config::PostgresConfig::from_env(env_vars.clone());
let client_agent_sse = ClientAgent::blank(redis_cfg);
@ -28,7 +29,7 @@ fn main() {
warn!("Streaming server initialized and ready to accept connections");
// Server Sent Events
let sse_update_interval = cfg.ws_interval;
let sse_update_interval = *cfg.ws_interval;
let sse_routes = sse::extract_user_or_reject(pg_conn.clone())
.and(warp::sse())
.map(
@ -49,7 +50,7 @@ fn main() {
.recover(err::handle_errors);
// WebSocket
let ws_update_interval = cfg.ws_interval;
let ws_update_interval = *cfg.ws_interval;
let websocket_routes = ws::extract_user_or_reject(pg_conn.clone())
.and(warp::ws::ws2())
.map(move |user: user::User, ws: Ws2| {
@ -79,9 +80,9 @@ fn main() {
.allow_methods(cfg.cors.allowed_methods)
.allow_headers(cfg.cors.allowed_headers);
let server_addr = net::SocketAddr::new(cfg.address, cfg.port);
let server_addr = net::SocketAddr::new(*cfg.address, cfg.port.0);
if let Some(_socket) = cfg.unix_socket {
if let Some(_socket) = cfg.unix_socket.0.as_ref() {
dbg_and_die!("Unix socket support not yet implemented");
} else {
warp::serve(websocket_routes.or(sse_routes).with(cors)).run(server_addr);

View File

@ -39,6 +39,7 @@ pub fn extract_user_or_reject(pg_conn: PostgresConn) -> BoxedFilter<(User,)> {
.and_then(move |q| User::from_query(q, pg_conn.clone()))
.boxed()
}
#[cfg(test)]
mod test {
use super::*;

View File

@ -1,7 +1,11 @@
//! Receives data from Redis, sorts it by `ClientAgent`, and stores it until
//! polled by the correct `ClientAgent`. Also manages sububscriptions and
//! unsubscriptions to/from Redis.
use super::{config, redis_cmd, redis_stream, redis_stream::RedisConn};
use super::{
config::{self, RedisInterval, RedisNamespace},
redis_cmd, redis_stream,
redis_stream::RedisConn,
};
use crate::pubsub_cmd;
use futures::{Async, Poll};
use serde_json::Value;
@ -14,8 +18,8 @@ use uuid::Uuid;
pub struct Receiver {
pub pubsub_connection: net::TcpStream,
secondary_redis_connection: net::TcpStream,
pub redis_namespace: Option<String>,
redis_poll_interval: time::Duration,
pub redis_namespace: RedisNamespace,
redis_poll_interval: RedisInterval,
redis_polled_at: time::Instant,
timeline: String,
manager_id: Uuid,
@ -139,7 +143,7 @@ impl futures::stream::Stream for Receiver {
/// been polled lately.
fn poll(&mut self) -> Poll<Option<Value>, Self::Error> {
let timeline = self.timeline.clone();
if self.redis_polled_at.elapsed() > self.redis_poll_interval {
if self.redis_polled_at.elapsed() > *self.redis_poll_interval {
redis_stream::AsyncReadableStream::poll_redis(self);
self.redis_polled_at = time::Instant::now();
}

View File

@ -1,5 +1,9 @@
use super::receiver::Receiver;
use crate::{config, redis_to_client_stream::redis_cmd};
use crate::{
config::{self, RedisInterval, RedisNamespace},
err,
redis_to_client_stream::redis_cmd,
};
use futures::{Async, Poll};
use serde_json::Value;
use std::{io::Read, io::Write, net, time};
@ -8,46 +12,86 @@ use tokio::io::AsyncRead;
pub struct RedisConn {
pub primary: net::TcpStream,
pub secondary: net::TcpStream,
pub namespace: Option<String>,
pub polling_interval: time::Duration,
pub namespace: RedisNamespace,
pub polling_interval: RedisInterval,
}
fn send_password(mut conn: net::TcpStream, password: &str) -> net::TcpStream {
conn.write_all(&redis_cmd::cmd("auth", &password)).unwrap();
let mut buffer = vec![0u8; 5];
conn.read_exact(&mut buffer).unwrap();
let reply = String::from_utf8(buffer.to_vec()).unwrap();
if reply != "+OK\r\n" {
err::die_with_msg(format!(
r"Incorrect Redis password. You supplied `{}`.
Please supply correct password with REDIS_PASSWORD environmental variable.",
password,
))
};
conn
}
fn set_db(mut conn: net::TcpStream, db: &str) -> net::TcpStream {
conn.write_all(&redis_cmd::cmd("SELECT", &db)).unwrap();
conn
}
fn send_test_ping(mut conn: net::TcpStream) -> net::TcpStream {
conn.write_all(b"PING\r\n").unwrap();
let mut buffer = vec![0u8; 7];
conn.read_exact(&mut buffer).unwrap();
let reply = String::from_utf8(buffer.to_vec()).unwrap();
match reply.as_str() {
"+PONG\r\n" => (),
"-NOAUTH" => err::die_with_msg(
r"Invalid authentication for Redis.
Redis reports that it needs a password, but you did not provide one.
You can set a password with the REDIS_PASSWORD environmental variable.",
),
"HTTP/1." => err::die_with_msg(
r"The server at REDIS_HOST and REDIS_PORT is not a Redis server.
Please update the REDIS_HOST and/or REDIS_PORT environmental variables.",
),
_ => err::die_with_msg(format!(
"Could not connect to Redis for unknown reason. Expected `+PONG` reply but got {}",
reply
)),
};
conn
}
impl RedisConn {
pub fn new(redis_cfg: config::RedisConfig) -> Self {
let addr = format!("{}:{}", redis_cfg.host, redis_cfg.port);
let mut pubsub_connection =
net::TcpStream::connect(addr.clone()).expect("Can connect to Redis");
pubsub_connection
.set_read_timeout(Some(time::Duration::from_millis(10)))
.expect("Can set read timeout for Redis connection");
pubsub_connection
let addr = net::SocketAddr::from((*redis_cfg.host, *redis_cfg.port));
let conn_err = |e| {
err::die_with_msg(format!(
"Could not connect to Redis at {}:{}.\n Error detail: {}",
*redis_cfg.host, *redis_cfg.port, e,
))
};
let update_conn = |mut conn| {
if let Some(password) = redis_cfg.password.clone() {
conn = send_password(conn, &password);
}
conn = send_test_ping(conn);
conn.set_read_timeout(Some(time::Duration::from_millis(10)))
.expect("Can set read timeout for Redis connection");
if let Some(db) = &*redis_cfg.db {
conn = set_db(conn, db);
}
conn
};
let (primary_conn, secondary_conn) = (
update_conn(net::TcpStream::connect(addr).unwrap_or_else(conn_err)),
update_conn(net::TcpStream::connect(addr).unwrap_or_else(conn_err)),
);
primary_conn
.set_nonblocking(true)
.expect("set_nonblocking call failed");
let mut secondary_redis_connection =
net::TcpStream::connect(addr).expect("Can connect to Redis");
secondary_redis_connection
.set_read_timeout(Some(time::Duration::from_millis(10)))
.expect("Can set read timeout for Redis connection");
if let Some(password) = redis_cfg.password {
pubsub_connection
.write_all(&redis_cmd::cmd("auth", &password))
.unwrap();
secondary_redis_connection
.write_all(&redis_cmd::cmd("auth", password))
.unwrap();
}
if let Some(db) = redis_cfg.db {
pubsub_connection
.write_all(&redis_cmd::cmd("SELECT", &db))
.unwrap();
secondary_redis_connection
.write_all(&redis_cmd::cmd("SELECT", &db))
.unwrap();
}
Self {
primary: pubsub_connection,
secondary: secondary_redis_connection,
primary: primary_conn,
secondary: secondary_conn,
namespace: redis_cfg.namespace,
polling_interval: redis_cfg.polling_interval,
}
@ -72,13 +116,18 @@ impl<'a> AsyncReadableStream<'a> {
if let Async::Ready(num_bytes_read) = async_stream.poll_read(&mut buffer).unwrap() {
let raw_redis_response = async_stream.as_utf8(buffer, num_bytes_read);
dbg!(&raw_redis_response);
if raw_redis_response.starts_with("-NOAUTH") {
eprintln!(
err::die_with_msg(
r"Invalid authentication for Redis.
Do you need a password?
If so, set it with the REDIS_PASSWORD environmental variable"
If so, set it with the REDIS_PASSWORD environmental variable.",
);
} else if raw_redis_response.starts_with("HTTP") {
err::die_with_msg(
r"The server at REDIS_HOST and REDIS_PORT is not a Redis server.
Please update the REDIS_HOST and/or REDIS_PORT environmental variables with the correct values.",
);
std::process::exit(1);
}
receiver.incoming_raw_msg.push_str(&raw_redis_response);
@ -89,7 +138,7 @@ If so, set it with the REDIS_PASSWORD environmental variable"
};
let mut msg = RedisMsg::from_raw(&receiver.incoming_raw_msg);
let prefix_to_skip = match &receiver.redis_namespace {
let prefix_to_skip = match &*receiver.redis_namespace {
Some(namespace) => format!("{}:timeline:", namespace),
None => "timeline:".to_string(),
};