Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

attempt at deploying on shuttle.rs #2

Closed
wants to merge 16 commits into from
61 changes: 57 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions Cargo.toml
Expand Up @@ -9,7 +9,12 @@ repository = "https://github.com/qwandor/dancelist"
keywords = ["folk", "dance", "balfolk", "website"]
categories = ["web-programming"]

[lib]
# required by shuttle
crate-type = ["cdylib"]

[dependencies]
anyhow = "1.0.56"
askama = "0.11.0"
axum = { version = "0.5.0", features = ["headers"] }
chrono = { version = "0.4.19", features = ["serde"] }
Expand All @@ -27,7 +32,11 @@ serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.78"
serde_urlencoded = "0.7.1"
serde_yaml = "0.8.23"
# FIXME: shuttle doesn't seem to let you point at shuttle-service as a git dep?
# Should be unblocked by https://github.com/getsynth/shuttle/pull/118
shuttle-service = "0.2.5"
stable-eyre = "0.2.2"
sync_wrapper = "0.1.1"
tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] }
toml = "0.5.8"
tower-http = { version = "0.2.1", features = ["fs"] }
Expand Down
2 changes: 1 addition & 1 deletion dancelist.example.toml
Expand Up @@ -3,7 +3,7 @@ public_dir = "/usr/share/dancelist"

# The file, directory or URL from which to read events data. This will probably be from the
# dancelist-data repository.
events = "/var/lib/dancelist"
events = "https://raw.githubusercontent.com/qwandor/dancelist-data/release/events.yaml"

# The address on which the server should listen.
bind_address = "0.0.0.0:3002"
Expand Down
8 changes: 7 additions & 1 deletion src/config.rs
Expand Up @@ -57,12 +57,18 @@ impl Config {
}
}

impl Default for Config {
fn default() -> Self {
toml::from_str("").unwrap()
}
}

fn default_public_dir() -> PathBuf {
Path::new("public").to_path_buf()
}

fn default_events() -> String {
"events".to_string()
"https://raw.githubusercontent.com/qwandor/dancelist-data/release/events.yaml".to_string()
}

fn default_bind_address() -> SocketAddr {
Expand Down
130 changes: 83 additions & 47 deletions src/main.rs → src/lib.rs
Expand Up @@ -23,56 +23,28 @@ mod model;
use crate::{
config::Config,
controllers::{bands, callers, cities, index, organisations, reload},
errors::internal_error,
importers::{balfolknl, folkbalbende, webfeet},
model::events::Events,
};
use axum::{
routing::{get, get_service, post},
http::header,
routing::{get, post},
Extension, Router,
};
use eyre::Report;
use log::info;
use schemars::schema_for;
use shuttle_service::{IntoService, Service};
use std::{
env,
process::exit,
net::SocketAddr,
sync::{Arc, Mutex},
};
use tokio::runtime::Runtime;
use tower_http::services::ServeDir;

#[tokio::main]
async fn main() -> Result<(), Report> {
stable_eyre::install()?;
pretty_env_logger::init();
color_backtrace::install();

let args: Vec<String> = env::args().collect();
if args.len() == 1 {
serve().await
} else if args.len() == 2 && args[1] == "schema" {
// Output JSON schema for events.
print!("{}", event_schema()?);
Ok(())
} else if args.len() >= 2 && args.len() <= 3 && args[1] == "validate" {
validate(args.get(2).map(String::as_str)).await
} else if args.len() >= 2 && args.len() <= 3 && args[1] == "cat" {
concatenate(args.get(2).map(String::as_str)).await
} else if args.len() == 2 && args[1] == "balbende" {
import_balbende().await
} else if args.len() == 2 && args[1] == "webfeet" {
import_webfeet().await
} else if args.len() == 2 && args[1] == "balfolknl" {
import_balfolknl().await
} else {
eprintln!("Invalid command.");
exit(1);
}
}

/// Load events from the given file, directory or URL, or from the one in the config file if no path
/// is provided.
async fn load_events(path: Option<&str>) -> Result<Events, Report> {
pub async fn load_events(path: Option<&str>) -> Result<Events, Report> {
if let Some(path) = path {
Events::load_events(path).await
} else {
Expand All @@ -81,35 +53,35 @@ async fn load_events(path: Option<&str>) -> Result<Events, Report> {
}
}

async fn validate(path: Option<&str>) -> Result<(), Report> {
pub async fn validate(path: Option<&str>) -> Result<(), Report> {
let events = load_events(path).await?;
println!("Successfully validated {} events.", events.events.len());

Ok(())
}

async fn concatenate(path: Option<&str>) -> Result<(), Report> {
pub async fn concatenate(path: Option<&str>) -> Result<(), Report> {
let events = load_events(path).await?;
print!("{}", serde_yaml::to_string(&events)?);
Ok(())
}

async fn import_balbende() -> Result<(), Report> {
pub async fn import_balbende() -> Result<(), Report> {
let events = folkbalbende::import_events().await?;
print_events(&events)
}

async fn import_webfeet() -> Result<(), Report> {
pub async fn import_webfeet() -> Result<(), Report> {
let events = webfeet::import_events().await?;
print_events(&events)
}

async fn import_balfolknl() -> Result<(), Report> {
pub async fn import_balfolknl() -> Result<(), Report> {
let events = balfolknl::import_events().await?;
print_events(&events)
}

fn print_events(events: &Events) -> Result<(), Report> {
pub fn print_events(events: &Events) -> Result<(), Report> {
let yaml = serde_yaml::to_string(events)?;
let yaml = yaml.replacen(
"---",
Expand All @@ -120,8 +92,7 @@ fn print_events(events: &Events) -> Result<(), Report> {
Ok(())
}

async fn serve() -> Result<(), Report> {
let config = Config::from_file()?;
pub async fn setup_app(config: &Config) -> Result<Router, Report> {
let events = Events::load_events(&config.events).await?;
let events = Arc::new(Mutex::new(events));

Expand All @@ -136,13 +107,24 @@ async fn serve() -> Result<(), Report> {
.route("/cities", get(cities::cities))
.route("/organisations", get(organisations::organisations))
.route("/reload", post(reload::reload))
.nest(
"/stylesheets",
get_service(ServeDir::new(config.public_dir.join("stylesheets")))
.handle_error(internal_error),
.route(
"/stylesheets/main.css",
get(|| async {
(
[(header::CONTENT_TYPE, "text/css")],
include_str!("../public/stylesheets/main.css"),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discussed this with Andrew - edit-compile-serve iteration times on axum are pretty slow, so his previous get_service(ServeDir::new(config.public_dir.join("stylesheets"))) approach is super valuable if you want to iterate quickly on your CSS.

)
}),
)
.layer(Extension(events));

Ok(app)
}

pub async fn serve() -> Result<(), Report> {
let config = Config::from_file()?;
let app = setup_app(&config).await?;

info!("Listening on {}", config.bind_address);
axum::Server::bind(&config.bind_address)
.serve(app.into_make_service())
Expand All @@ -151,8 +133,62 @@ async fn serve() -> Result<(), Report> {
Ok(())
}

pub async fn serve_shuttle(addr: SocketAddr) -> Result<(), Report> {
println!("in shuttle_service()");
log::warn!("in shuttle_service()");
let config = Config::default();
let app = setup_app(&config).await?;

println!("Listening on {}", config.bind_address);
log::warn!("Listening on {}", config.bind_address);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't get any logging out of shuttle. I don't they set anything up to consume logs before loading the .so, so all logging goes nowhere. println statements are visible in the docker logs though.

axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;

Ok(())
}
// We can't use the web-axum feature because there is no released 0.5 version on crates.io yet.
// We can't use the shuttle_service::main macro because that needs a SimpleService, and orphan rules
// do not allow us to impl anything useful for SimpleService outside of the shuttle_service crate.
struct MyService;
impl MyService {
Copy link
Author

@alsuren alsuren Apr 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that we can't use the shuttle_service::main macro (due to interactions with the orphan rule) is super-annoying. That said, it feels philosophically aligned with the "batteries included" approach that shuttle has first-party support for all of the things, so they can handle automatic injection of logging and databases etc, and things "just work".

I was about to suggest documenting the "impl Service" workaround with an example, but maybe that's not the answer. Maybe it would be better to allow users to supply their own version of shuttle-service as a git dependency or something? I don't know.

Relatedly, Andrew suggests that maybe breaking with the .crate packaging format would be beneficial - some companies might not be comfortable with pushing their source code elsewhere? (the kind of company that prefers to run their own jenkins instances might not be the target market for shuttle though). If shuttle could ship a docker-compose based setup for building the .so file, then we might be able to sidestep the "no git dependencies" issue? I heard a rumour that synced volumes via virtiofs are pretty fast these days? I've not tried it myself though. Also, it's hard to beat the "just send it to us and we'll build it on our infini-core beast of a server that also has a cache of all of your crate's deps already" approach [edit: unless you're already committed to compiling locally for local dev?].

fn new() -> Self {
println!("in MyService::new()");
log::warn!("in MyService::new()");
Self
}
}

impl IntoService for MyService {
type Service = Self;

fn into_service(self) -> Self::Service {
println!("in into_service()");
log::warn!("in into_service()");
self
}
}

fn eyre_to_anyhow(e: Report) -> anyhow::Error {
let e: Box<dyn std::error::Error + Send + Sync + 'static> = e.into();
anyhow::anyhow!(dbg!(e))
}

impl Service for MyService {
fn bind(&mut self, addr: SocketAddr) -> Result<(), shuttle_service::error::Error> {
println!("in bind()");
log::warn!("in bind()");
let rt = Runtime::new().unwrap();
rt.block_on(serve_shuttle(addr)).map_err(eyre_to_anyhow)?;
println!("out bind()");
log::warn!("out bind()");
Ok(())
}
}
shuttle_service::declare_service!(MyService, MyService::new);

/// Returns the JSON schema for events.
fn event_schema() -> Result<String, Report> {
pub fn event_schema() -> Result<String, Report> {
let schema = schema_for!(Events);
Ok(serde_json::to_string_pretty(&schema)?)
}
Expand Down