What would a persistent Nix evaluator look like?
Enter Flack
Because naming your webapp framework after a singer is what the cool kids do
[Roberta Flack] produced the single and her 1975 album of the same name under the pseudonym "Rubina Flake"
What does it look like?
I hope you like HTNL and the module system
{ lib, flack, inputs, ... }:
let
# ...
mkSlide = body:
h "html" [
(h "head" [
(h "title" "Flack the Planet!")
])
(h "body" body)
];
presentation = mkSlidesHtml {
index = mkSlide [
(h "article" [
...
])
];
};
servePresentation = req:
"${presentation req}/www/${joinPathToIndex (req.params.path or ["/"])}";
in
{
route = {
GET."/" = req: req.res 200 { } (servePresentation req);
GET."/talk/:...path" = req: req.res 200 { } (servePresentation req);
};
}
Middlewares, mountpoints, and routes
Rejected name: Nixpress
use = {
/*
This is a middleware.
If X-Auth-Token isn't "supersecret" then it'll return a 401 for all paths under /foo.
Obviously there is a timing sidechannel here, don't actually do this.
*/
"/foo" =
req: if req.get "X-Auth-Token" != "supersecret" then req.res 401 { } "Unauthorized" else req;
};
route = {
/*
This is a route.
bar is available in req.params.
Note the auth token above!
`curl -H 'X-Auth-Token: supersecret' http://localhost:2019/foo/myBar`
*/
GET."/foo/:...bar" =
req:
req.res 200 { "X-My-Header" = "value"; } {
inherit (req) pathComponents;
inherit (req.params) bar;
};
};
Using the nixops4 Rust bindings
https://github.com/nixops4/nix-bindings-rust
- Shoutout to Robert Hensing and John Ericson for getting us off Perl and onto Rust
- The Rust bindings are usable today
- Good enough to render this slide deck to /nix/store/hv9hrihywz5z05bzs82w66x1fypzw1zn-index.html*
- * With one caveat
Multithreading
- Nix historically has been a single threaded system operable by one user at a time, but all is not lost.
- The Boehm GC is thread-safe!
Just do this:
nix_bindings_expr::eval_state::gc_register_my_thread()
Cloning values using the Rust bindings atomically modifies their refcount
/// Evals a string and then calls the result.
fn call_fn(func: &str, st: &mut EvalState, value: &Value, dir: &str) -> std::io::Result<Value> {
let func_val = st
.eval_from_string(func, dir)
.map_err(std::io::Error::other)?;
st.call(func_val, value.clone()) // not an actual copy!
.map_err(std::io::Error::other)
}
Has to be a catch, right?
- EvalState will be:
- thread unsafe in NixOS/nix
- thread safe in DeterminateSystems/nix-src due to parallel eval
/// These need to be added if you want to use EvalState and Value in multiple threads
unsafe impl Send for EvalState { }
unsafe impl Send for Value { }
/// Gets a new EvalState for the specified store.
fn init_get_state(
store: Store,
) -> std::io::Result<(EvalState, ThreadRegistrationGuard)> {
let gc_guard = init_get_gc_guard()?;
let mut state_builder = nix_bindings_expr::eval_state::EvalStateBuilder::new(store)
.map_err(std::io::Error::other)?;
let state = state_builder.build().map_err(std::io::Error::other)?;
Ok((state, gc_guard))
}
What about the store?
- It's threadsafe, but set max-connections to something high because the default is 1
let cores = match std::thread::available_parallelism() {
Ok(val) => val,
Err(err) => {
warn!("Error getting parallelism, defaulting to 1: {:?}", err);
NonZero::new(1).unwrap()
}
};
let store_uri = url::Url::parse_with_params(
args.store.as_str(),
&[(
"max-connections",
format!("{}", cores).as_str(),
)],
)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
let store = nix_bindings_store::store::Store::open(Some(store_uri.to_string().as_str()), [])
.map_err(|e| std::io::Error::new(std::io::ErrorKind::ConnectionRefused, e))?;