config server launch

This commit is contained in:
appflowy 2021-08-21 22:02:05 +08:00
parent 49e5f38406
commit 54342850b2
16 changed files with 230 additions and 159 deletions

View File

@ -23,9 +23,11 @@ log = "0.4.14"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_repr = "0.1"
serde-aux = "1.0.1"
derive_more = {version = "0.99", features = ["display"]}
protobuf = {version = "2.20.0"}
uuid = { version = "0.8", features = ["serde", "v4"] }
config = { version = "0.10.1", default-features = false, features = ["yaml"] }
flowy-log = { path = "../rust-lib/flowy-log" }
flowy-user = { path = "../rust-lib/flowy-user" }

View File

@ -0,0 +1,9 @@
application:
port: 8000
host: 0.0.0.0
database:
host: "localhost"
port: 5433
username: "postgres"
password: "password"
database_name: "flowy"

View File

@ -0,0 +1,5 @@
application:
host: 127.0.0.1
base_url: "http://127.0.0.1"
database:
require_ssl: false

View File

@ -0,0 +1,4 @@
application:
host: 0.0.0.0
database:
require_ssl: true

View File

@ -2,6 +2,22 @@
set -x
set -eo pipefail
if ! [ -x "$(command -v psql)" ]; then
echo >&2 "Error: `psql` is not installed."
echo >&2 "install using brew: brew install libpq."
echo >&2 "link to /usr/local/bin: brew link --force libpq ail"
exit 1
fi
if ! [ -x "$(command -v sqlx)" ]; then
echo >&2 "Error: `sqlx` is not installed."
echo >&2 "Use:"
echo >&2 " cargo install --version=0.5.5 sqlx-cli --no-default-features --features postgres"
echo >&2 "to install it."
exit 1
fi
until psql -h "localhost" -U "${DB_USER}" -p "${DB_PORT}" -d "postgres" -c '\q';
do
>&2 echo "Postgres is still unavailable - sleeping"

View File

@ -0,0 +1,83 @@
use crate::{
config::{get_configuration, DatabaseSettings, Settings},
context::AppContext,
routers::*,
user_service::Auth,
ws_service::WSServer,
};
use actix::Actor;
use actix_web::{dev::Server, middleware, web, web::Data, App, HttpServer, Scope};
use sqlx::{postgres::PgPoolOptions, PgPool};
use std::{net::TcpListener, sync::Arc};
pub struct Application {
port: u16,
server: Server,
app_ctx: Arc<AppContext>,
}
impl Application {
pub async fn build(configuration: Settings) -> Result<Self, std::io::Error> {
let app_ctx = init_app_context(&configuration).await;
let address = format!(
"{}:{}",
configuration.application.host, configuration.application.port
);
let listener = TcpListener::bind(&address)?;
let port = listener.local_addr().unwrap().port();
let server = run(listener, app_ctx.clone())?;
Ok(Self {
port,
server,
app_ctx,
})
}
pub async fn run_until_stopped(self) -> Result<(), std::io::Error> { self.server.await }
}
pub fn run(listener: TcpListener, app_ctx: Arc<AppContext>) -> Result<Server, std::io::Error> {
let server = HttpServer::new(move || {
App::new()
.wrap(middleware::Logger::default())
.app_data(web::JsonConfig::default().limit(4096))
.service(ws_scope())
.service(user_scope())
.app_data(Data::new(app_ctx.ws_server.clone()))
.app_data(Data::new(app_ctx.db_pool.clone()))
.app_data(Data::new(app_ctx.auth.clone()))
})
.listen(listener)?
.run();
Ok(server)
}
fn ws_scope() -> Scope { web::scope("/ws").service(ws::start_connection) }
fn user_scope() -> Scope {
web::scope("/user").service(web::resource("/register").route(web::post().to(user::register)))
}
async fn init_app_context(configuration: &Settings) -> Arc<AppContext> {
let _ = flowy_log::Builder::new("flowy").env_filter("Debug").build();
let pg_pool = Arc::new(
get_connection_pool(&configuration.database)
.await
.expect("Failed to connect to Postgres."),
);
let ws_server = WSServer::new().start();
let auth = Arc::new(Auth::new(pg_pool.clone()));
let ctx = AppContext::new(ws_server, pg_pool, auth);
Arc::new(ctx)
}
pub async fn get_connection_pool(configuration: &DatabaseSettings) -> Result<PgPool, sqlx::Error> {
PgPoolOptions::new()
.connect_timeout(std::time::Duration::from_secs(2))
.connect_with(configuration.with_db())
.await
}

View File

@ -1,48 +0,0 @@
use crate::config::DatabaseConfig;
use std::{convert::TryFrom, sync::Arc};
pub struct Config {
pub http_port: u16,
pub database: Arc<DatabaseConfig>,
}
impl Config {
pub fn new() -> Self {
Config {
http_port: 3030,
database: Arc::new(DatabaseConfig::default()),
}
}
pub fn server_addr(&self) -> String { format!("0.0.0.0:{}", self.http_port) }
}
pub enum Environment {
Local,
Production,
}
impl Environment {
#[allow(dead_code)]
pub fn as_str(&self) -> &'static str {
match self {
Environment::Local => "local",
Environment::Production => "production",
}
}
}
impl TryFrom<String> for Environment {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.to_lowercase().as_str() {
"local" => Ok(Self::Local),
"production" => Ok(Self::Production),
other => Err(format!(
"{} is not a supported environment. Use either `local` or `production`.",
other
)),
}
}
}

View File

@ -0,0 +1,99 @@
use serde_aux::field_attributes::deserialize_number_from_string;
use sqlx::postgres::{PgConnectOptions, PgSslMode};
use std::convert::{TryFrom, TryInto};
#[derive(serde::Deserialize, Clone)]
pub struct Settings {
pub database: DatabaseSettings,
pub application: ApplicationSettings,
}
#[derive(serde::Deserialize, Clone)]
pub struct ApplicationSettings {
#[serde(deserialize_with = "deserialize_number_from_string")]
pub port: u16,
pub host: String,
pub base_url: String,
}
#[derive(serde::Deserialize, Clone)]
pub struct DatabaseSettings {
pub username: String,
pub password: String,
#[serde(deserialize_with = "deserialize_number_from_string")]
pub port: u16,
pub host: String,
pub database_name: String,
pub require_ssl: bool,
}
impl DatabaseSettings {
pub fn without_db(&self) -> PgConnectOptions {
let ssl_mode = if self.require_ssl {
PgSslMode::Require
} else {
PgSslMode::Prefer
};
PgConnectOptions::new()
.host(&self.host)
.username(&self.username)
.password(&self.password)
.port(self.port)
.ssl_mode(ssl_mode)
}
pub fn with_db(&self) -> PgConnectOptions { self.without_db().database(&self.database_name) }
}
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
let mut settings = config::Config::default();
let base_path = std::env::current_dir().expect("Failed to determine the current directory");
let configuration_dir = base_path.join("configuration");
settings.merge(config::File::from(configuration_dir.join("base")).required(true))?;
let environment: Environment = std::env::var("APP_ENVIRONMENT")
.unwrap_or_else(|_| "local".into())
.try_into()
.expect("Failed to parse APP_ENVIRONMENT.");
settings
.merge(config::File::from(configuration_dir.join(environment.as_str())).required(true))?;
// Add in settings from environment variables (with a prefix of APP and '__' as
// separator) E.g. `APP_APPLICATION__PORT=5001 would set
// `Settings.application.port`
settings.merge(config::Environment::with_prefix("app").separator("__"))?;
settings.try_into()
}
/// The possible runtime environment for our application.
pub enum Environment {
Local,
Production,
}
impl Environment {
pub fn as_str(&self) -> &'static str {
match self {
Environment::Local => "local",
Environment::Production => "production",
}
}
}
impl TryFrom<String> for Environment {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.to_lowercase().as_str() {
"local" => Ok(Self::Local),
"production" => Ok(Self::Production),
other => Err(format!(
"{} is not a supported environment. Use either `local` or `production`.",
other
)),
}
}
}

View File

@ -1,5 +0,0 @@
host = "localhost"
port = 5433
username = "postgres"
password = "password"
database_name = "flowy"

View File

@ -1,33 +0,0 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub struct DatabaseConfig {
username: String,
password: String,
port: u16,
host: String,
database_name: String,
}
impl DatabaseConfig {
pub fn connect_url(&self) -> String {
format!(
"postgres://{}:{}@{}:{}/{}",
self.username, self.password, self.host, self.port, self.database_name
)
}
pub fn set_env_db_url(&self) {
let url = self.connect_url();
std::env::set_var("DATABASE_URL", url);
}
}
impl std::default::Default for DatabaseConfig {
fn default() -> DatabaseConfig {
let toml_str: &str = include_str!("config.toml");
let config: DatabaseConfig = toml::from_str(toml_str).unwrap();
config.set_env_db_url();
config
}
}

View File

@ -1,3 +0,0 @@
mod database;
pub use database::*;

View File

@ -1,7 +1,5 @@
mod config;
mod configuration;
mod const_define;
mod database;
pub use config::*;
pub use configuration::*;
pub use const_define::*;
pub use database::*;

View File

@ -1,25 +1,18 @@
use crate::{config::Config, user_service::Auth, ws_service::WSServer};
use crate::{user_service::Auth, ws_service::WSServer};
use actix::Addr;
use sqlx::PgPool;
use std::sync::Arc;
pub struct AppContext {
pub config: Arc<Config>,
pub ws_server: Addr<WSServer>,
pub db_pool: Arc<PgPool>,
pub auth: Arc<Auth>,
}
impl AppContext {
pub fn new(
config: Arc<Config>,
ws_server: Addr<WSServer>,
db_pool: Arc<PgPool>,
auth: Arc<Auth>,
) -> Self {
pub fn new(ws_server: Addr<WSServer>, db_pool: Arc<PgPool>, auth: Arc<Auth>) -> Self {
AppContext {
config,
ws_server,
db_pool,
auth,

View File

@ -1,7 +1,7 @@
mod config;
pub mod application;
pub mod config;
mod context;
mod entities;
mod routers;
pub mod startup;
pub mod user_service;
pub mod ws_service;

View File

@ -1,10 +1,11 @@
use backend::startup::{init_app_context, run};
use backend::{application::Application, config::get_configuration};
use std::net::TcpListener;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let app_ctx = init_app_context().await;
let listener =
TcpListener::bind(app_ctx.config.server_addr()).expect("Failed to bind server address");
run(app_ctx, listener)?.await
let configuration = get_configuration().expect("Failed to read configuration.");
let application = Application::build(configuration).await?;
application.run_until_stopped().await?;
Ok(())
}

View File

@ -1,50 +0,0 @@
use crate::{
config::Config,
context::AppContext,
routers::*,
user_service::Auth,
ws_service::WSServer,
};
use actix::Actor;
use actix_web::{dev::Server, middleware, web, web::Data, App, HttpServer, Scope};
use sqlx::PgPool;
use std::{net::TcpListener, sync::Arc};
pub fn run(app_ctx: Arc<AppContext>, listener: TcpListener) -> Result<Server, std::io::Error> {
let server = HttpServer::new(move || {
App::new()
.wrap(middleware::Logger::default())
.app_data(web::JsonConfig::default().limit(4096))
.service(ws_scope())
.service(user_scope())
.app_data(Data::new(app_ctx.ws_server.clone()))
.app_data(Data::new(app_ctx.db_pool.clone()))
.app_data(Data::new(app_ctx.auth.clone()))
})
.listen(listener)?
.run();
Ok(server)
}
fn ws_scope() -> Scope { web::scope("/ws").service(ws::start_connection) }
fn user_scope() -> Scope {
web::scope("/user").service(web::resource("/register").route(web::post().to(user::register)))
}
pub async fn init_app_context() -> Arc<AppContext> {
let _ = flowy_log::Builder::new("flowy").env_filter("Debug").build();
let config = Arc::new(Config::new());
// TODO: what happened when PgPool connect fail?
let db_pool = Arc::new(
PgPool::connect(&config.database.connect_url())
.await
.expect("Failed to connect to Postgres."),
);
let ws_server = WSServer::new().start();
let auth = Arc::new(Auth::new(db_pool.clone()));
let ctx = AppContext::new(config, ws_server, db_pool, auth);
Arc::new(ctx)
}