Hi,
all code examples provided are pretty simple and contain not many routes; anyway, it is not clear how a more significant project should be structured.
In my company, we have tried multiple approaches with no luck.
For example:
pub fn start_server_one() {
actix_web::server::new(|| {
vec![
actix_web::App::new().resource("/one", |r| r.f(|req| actix_web::HttpResponse::Ok())),
actix_web::App::new().resource("/two", |r| r.f(|req| actix_web::HttpResponse::Ok()))
]})
.bind("127.0.0.1:8080").expect("Can not start")
.run();
}
pub fn start_server_two(app_one: actix_web::App, app_two: actix_web::App) {
actix_web::server::new(|| {
vec![
app_one,
app_two
]})
.bind("127.0.0.1:8080").expect("Can not start")
.run();
}
In this example, the function start_server_one() works as expected but it does not suite our needs because we have to declare hundreds of routes and writing all of them in a single function would be unmaintainable.
On the contrary, the function start_server_two(...) would allow us to independently define multiple actix_web::App instances (for example one for each domain module) and start all of them in a single place. Unfortunately, this function does not compile due to this error:
error[E0277]: the trait bound `actix_web::handler::RouteHandler<()> + 'static: std::marker::Send` is not satisfied
--> src/main.rs:19:5
|
19 | actix_web::server::new(|| {
| ^^^^^^^^^^^^^^^^^^^^^^ `actix_web::handler::RouteHandler<()> + 'static` cannot be sent between threads safely
|
= help: the trait `std::marker::Send` is not implemented for `actix_web::handler::RouteHandler<()> + 'static`
= note: required because of the requirements on the impl of `std::marker::Send` for `std::ptr::Unique<actix_web::handler::RouteHandler<()> + 'static>`
= note: required because it appears within the type `std::boxed::Box<actix_web::handler::RouteHandler<()> + 'static>`
= note: required because it appears within the type `actix_web::application::PrefixHandlerType<()>`
= note: required because of the requirements on the impl of `std::marker::Send` for `std::ptr::Unique<actix_web::application::PrefixHandlerType<()>>`
= note: required because it appears within the type `alloc::raw_vec::RawVec<actix_web::application::PrefixHandlerType<()>>`
= note: required because it appears within the type `std::vec::Vec<actix_web::application::PrefixHandlerType<()>>`
= note: required because it appears within the type `actix_web::application::ApplicationParts<()>`
= note: required because it appears within the type `std::option::Option<actix_web::application::ApplicationParts<()>>`
= note: required because it appears within the type `actix_web::App`
= note: required because it appears within the type `[closure@src/main.rs:19:28: 23:11 app_one:actix_web::App, app_two:actix_web::App]`
= note: required by `actix_web::server::new`
Is there any proposed approach for structuring a big project without losing modularity and maintainability?
I think it might be useful to consider moving your routes into several applications by using https://actix.rs/actix-web/actix_web/struct.App.html#method.configure
In this case one acts like main while others are imported
P.s. it is not exactly like creating two apps I guess, but it should be helpful to modularize your application creation code
@DoumanAsh
That does not solve the issue. In fact, what we desire is that each module is able to register itself on a central service; but with your solution is the central service that has to call the "configure" function of each module.
On the contrary, if the App was Sync, we could have a single App shared amongst all modules and each module could configure independently so the central service logic would not depend on the modules.
Well to be honest I think external code should not be able to arbitrary register itself in central app.
There should be certain separation of concerns so that you could easily swap out part of your app without modifying app itself.
So to me approach for providing fn registration(App) and using it by central app seems to be more graceful rather than trying to bump it all apps together.
But if you consider it unsuitable then ok.
Moving to topic of multiple apps, I think in your case you may also need to use boxed method of App if they may have different states.
But I'm not sure why exactly Rust complains here right now (you may need to mark closure with move though)
Looking again on how application is organized, the closure needs to be able create distinct instances of App so you cannot just pass it existing value without at least cloning it.
I think configure approach is the only way to do it properly at the moment
Btw comnsider following refactoring:
pub fn start_server_two() {
actix_web::server::new(|| {
vec![
app_one(),
app_two()
]})
.bind("127.0.0.1:8080").expect("Can not start")
.run();
}
I'm using a different approach personally, which makes the modules register themselves in the App.
It's not creating different instances though (since I do not have a need for separate apps).
I have a setup method at the root of my "controllers" folder that has the following signature, in which I create App Scopes for each main controller route prefix
pub fn setup(mut app: App<AppState>) -> App<AppState> {
app.scope("/myscope", self::mymodule::setup)
}
And I'm creating routes for each "controller" in their mod.rs
pub fn setup(s: Scope<AppState>) -> Scope<AppState> {
s.resource("/helloworld", |r| {
r.get().with_async(my_handler)
})
}
@DoumanAsh
your last example still has the problem that the central service has to call app_one() and app_two(), so it needs to be aware of the external modules.
@OtaK
I tried a similar approach with no luck. Could you please provide a small POC or a more complete code example?
You said that you approach permits the modules to register themselves but it's not clear how. Who is in charge of calling this method: pub fn setup(mut app: App<AppState>) -> App<AppState> ? If it is called into the actix_web::server::new(|| {...}) closure then it does not solve the issue.
Ah, @OtaK and I apparently had similar ideas. I actually just threw my sample up (ripped out of a project I'm working on) that also implements user signup/authorization/etc, if anyone's interested:
https://github.com/ryanmcgrath/jelly
I use this in production currently (for whatever definition a stealth project in production is). Works great for me.
@ufoscout it is not possible for module to register itself, something has to call module registration function.
I finally got something working!
It was more complex than what I expected but I am very happy because I learned a lot about threading and smart pointers in rust!
Here a full example:
extern crate actix_web;
use std::sync::{Arc, Mutex};
fn main() {
let service = MyCentralService::new();
service.register(Box::new(ModuleTwoRouter { module: Arc::new(ModuleTwo{})}));
service.register(Box::new(ModuleOneRouter { module: Arc::new(ModuleOne{})}));
service.start();
}
// Modules that want to register themselves need to implement this trait
trait Router: Send + Sync {
fn configure(&self) -> actix_web::App;
}
// A simple central service
struct MyCentralService {
routers: Arc<Mutex<Vec<Box<Router>>>>
}
impl MyCentralService {
fn new() -> MyCentralService {
MyCentralService{ routers: Arc::new(Mutex::new(vec![])) }
}
// Modules are registered calling this method
fn register(&self, router: Box<Router>) {
self.routers.lock().unwrap().push(router);
}
// It dynamically registers the modules and starts the server
fn start(&self) {
let clone = self.routers.clone();
actix_web::server::new(move || {
let mut apps = vec![];
for router in clone.lock().unwrap().iter() {
apps.push(router.configure());
}
apps
}).bind("127.0.0.1:8080")
.expect("Can not start")
.run();
}
}
// Example Module ONE - BEGIN
struct ModuleOne {}
impl ModuleOne {
fn hello(&self) -> String {
format!("Hello from Module One!")
}
}
struct ModuleOneRouter {
module: Arc<ModuleOne>
}
impl Router for ModuleOneRouter {
fn configure(&self) -> actix_web::App {
println!("Register module ONE", );
let module = self.module.clone();
actix_web::App::new().prefix("/one").resource("", move |r| r.f(move |_| module.hello()))
}
}
// Example Module ONE - END
// Example Module TWO - BEGIN
struct ModuleTwo {}
impl ModuleTwo {
fn hello(&self) -> String {
format!("Hello from Module Two!")
}
}
struct ModuleTwoRouter {
module: Arc<ModuleTwo>
}
impl Router for ModuleTwoRouter {
fn configure(&self) -> actix_web::App {
println!("Register module TWO", );
let module = self.module.clone();
actix_web::App::new().prefix("/two").resource("", move |r| r.f(move |_| module.hello()))
}
}
// Example Module TWO - END
This way each module can be implemented as an independent struct. The MyCentralService could also be passed as a reference into the modules so they can register by themselves.
To complete the example, this structure makes also quite easy to perform integration tests:
#[cfg(test)]
pub mod test {
extern crate actix_web;
use self::actix_web::{test};
pub fn new_test_server(service: super::MyCentralService) -> test::TestServer {
let clone = service.routers.clone();
test::TestServer::with_factory(move || {
let mut apps = vec![];
for router in clone.lock().unwrap().iter() {
apps.push(router.configure());
}
apps
})
}
#[test]
fn integration_test() {
// instead of a new service, use the one of your app
let service = super::MyCentralService::new();
// get a test server
let server = new_test_server(service);
// Do something with server
}
}
Most helpful comment
I finally got something working!
It was more complex than what I expected but I am very happy because I learned a lot about threading and smart pointers in rust!
Here a full example:
This way each module can be implemented as an independent struct. The MyCentralService could also be passed as a reference into the modules so they can register by themselves.