I'm using Rust, axum and utoipa with utoipa-axum for the first time.
I'm creating one crate for operation (query or command) because I need development compilations (edit -> build -> run cycle) to be the fastest possible so right now I have these directories:
/Cargo.toml (workspace one)
/src/app/src/main.rs (the main executable)
/src/ops/player/Cargo.toml
/src/ops/player/src/types.rs (common types for all the sub-crates)
/src/ops/player/create/Cargo.toml
/src/ops/player/update/Cargo.toml
/src/ops/player/play/Cargo.toml
other hundreds ops here
/src/rest/Cargo.toml
/src/rest/src/router.rs which contains:
use utoipa_axum::{router::OpenApiRouter, routes};
pub fn new() -> OpenApiRouter<Arc<State>> {
OpenApiRouter::new()
.nest(
"/player",
OpenApiRouter::<Arc<State>>::new()
.routes(routes!(player_create::handler))
.routes(routes!(player_update::handler))
.routes(routes!(player_play::handler))
)
.nest(
"/team",
OpenApiRouter::<Arc<State>>::new()
.routes(routes!(team_include_player::handler))
.routes(routes!(team_exclude_player::handler))
.routes(routes!(team_do_something::handler))
)
// and so on other hundreds routes
}
Right now if I edit one query or command, for example player_update, /src/rest/ is invalidated since it depends on /ops/player.
Is there a way to avoid invalidation and only rebuild the single crate I changed?
Is there a way to "dinamically register routes", maybe at runtime? Why not? Or only during development?
I'm currently doing something quite janky in my axum app to speed up my iterations. I'm using notify to watch certain directories. If anything writes to those it will just re-exec itself, which works well enough for me now.
// live reloading (restart server) on template dir change
let mut watcher = notify::recommended_watcher(|res: Result<Event, _>| match res {
Ok(event) => {
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
tracing::info!("watch event: {event:?}");
// Write timestamp before rebuild
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let _ = fs::write(".reload_timestamp", timestamp.to_string());
// this should rebuild and relaunch the server
let error = Command::new("cargo").arg("serve").exec();
panic!("exec error {error}");
}
}
Err(e) => tracing::error!("watch error: {:?}", e),
})?;
Assuming you can prove that changes to dependencies do not invalidate dependents, you might have some success with dynamic linking. This is mainly how bevy avoids unnecessary rebuilds: bevy Setup - Enable Fast Compiles.
The precise mechanism they use is documented in this crate: bevy_dylib - Rust
can also help to make slow compilations of web servers a bit less painful. Where normally if you try to do an http request while compiling you did get an error, with systemfd instead the request would block until the server is done compiling and has launched. This because systemfd keeps the tcp socket open even while the server is not running.
You can do something like systemfd --no-pid -s http::8080 -- cargo watch with this and then use the listenfd crate to get the socket to receive requests from (make sure to set the socket as nonblocking before converting it to a tokio socket!).
If subsecond works for you, you don't need this however.
Have you tried splitting the nested routes in separate crates? e.g. one crate with the player router (and possibly its subroutes), another crate with the team router, etc etc. Otherwise as the number of routes grows there is more and more codegen that needs to be done in this crate when any other crate changes.
@SkiFire13 Let's say I create the player and the team crate for their routes. And I call player::router::new() and team::router::new() in /rest/router.rs.
When I change the code in one of /player/ops/something isn't all the rest/router.rs still invalidated?
Does it help to have subcrates even if they are still dependencies of /rest/router.rs?
This is another big doubt. Logic leads me to think no, that it makes no sense if it is true that parents are being always invalidated.
Yes, but the difference is that the implementation of team::router::new() won't be recompiled, only the call to it will (and that will generally be much faster).
Depending on what that implementation is (especially when generics are involved) and how many other subrouters you have this can make a big difference.