rwf2 / Rocket

A web framework for Rust.
https://rocket.rs
Other
24.48k stars 1.56k forks source link

Single Port Multi-Apps support #394

Open SuperHacker-liuan opened 7 years ago

SuperHacker-liuan commented 7 years ago

Feature Requests

1. Why you believe this feature is necessary.

Single Port Multi-App (SPMA) is an important enterprise feature. For a large project in large enterprise, it is often need to split whole project into multiple sub-projects, each subproject will subcontract to another developing team. Each subproject has its own static files, templates, source codes, dependencies and etc, it act as an independent app, and provides some communication interfaces to other apps. The whole project will be a batch of apps work together with inter-app communication. Every app may be accessed from end-user, therefore they should share a same IP/hostname and HTTP port 80. Consider JavaEE supported by tomcat, A large site may be in the following directory structure.

tomcat
|-bin
|-dir etc.
|-wabapp
  |-site_root
  | |-statics
  | |-WEB-INF //src
  | |-template
  |-app0
  | |-like site root
  |-app1
  | |-like site root
  |-app2
    |-like site root

Architecture and data in tomcat is as below.

|App0        |App1        |App2        |site root   |
|own data    |own data    |own data    |own data    |
|own srv     |own srv     |own srv     |own srv     |
|own DBpool  |own DBpool  |own DBpool  |own DBpool  |
--------------------------------------------------------
|  tomcat midware                                     |
| Data base connection pool     (pool)                |
| Other shared services                               |
| Same port  8080                                     |

Every app has its own data, static files, services, libs, pools(like redis) and etc. And they can also share a common used DBPool (like Oracle) in middle ware. Resource from middle ware can be accessed from each app, rosource from app are isolated to its own.

In this architecture, enterprise architectures can easily split whole project into multiple isolated apps, which can reduce a lot of cost and can prevent faliure cascade between apps.

2. A convincing use-case for this feature.

2.1 workspace style

A large project with many apps may be formed as a workspace. The directory structrue of the workspace may be as below.

workspace/
|-server/ : The Rocket server.
| |-src/ :  This is seems like bin/ dir in tomcat. Pools, srvs used by all apps also defined here
| |-configs: like Rocket.toml, Cargo.toml, etc.
| |-Deploy.toml: Maybe needed, if architect won't write hard code in src to do the app deployments.
| |-libs/: This dir provides some shared libraries used only in this project
| |-apps/ : this dir is provides for apps, each app should be designed as a rust lib.
|   |-site_root/ : Subcontract Team send this folder to main architecture.
|   | |-src/
|   | |-statics/
|   | |-templates/
|   | |-tests/ : For site app test.
|   | |-configs/ : For configs used by app frameworks, such as application.xml, sqls.toml, struts.ini and etc.
|   | |-Cargo.toml
|   |-app0/ : Every app has its own application context, such as src, statics, templates pools, srvs and etc.
|   | |-src/
|   | |-statics/
|   | |-templates/
|   | |-test/
|   | |-Cargo.toml
|   |-app1/ : same as app0
|   |-app2/
|   |-.../
|-Cargo.toml: workspace spec
|-misc files: i.e. LICENSE

In this style, architecture can collect apps from subcontract team, put them in apps/ folder, and write some deploy code in server/src/ or server/Deploy.toml. Finally run cargo build --release in server/ dir to launch the project. The launce code maybe in following style.

extern crate app0;
extern crate app1;

fn main() {
    Rocket::ignite()
        .deploy(app0::context(), ContextRoot("app0", "../apps/app0")) // hard code deploy
        .deploy(app1::context(), ContextRoot("app1", "../apps/app1"))
        .deploy_file("Deploy.toml") // Deploy by parsing Deploy.toml, may be this should be automatically called in launch() if a Deploy.toml can be found.
        .mount("/", route![...]) // If the server_root app NOT PROVIDED, Rocket server can act as a default server root.
        .catch(errors![...]) // If app not provide some error catchers, fallback to use error catchers here.
        .launch();
}

The app::context() should mount Routes used by app own, and optional provide error catchers, farings and etc. It app::context() looks like a Rocket::ignite(), except it won't launch().

fn context() -> Deploy {
    Deploy::ignite() // It looks like a booster rocket. The booster rocket never launch standalone.
        .mount()
        .catch()
        .attach()
}

ContextRoot() need two important information, web base url (app0 means https://xxx.xxx/app0/), and app resource files' path (relative to server dir or absolute path in filesystem). Respath is important in finding statics, templates, configs and etc. Base url is useful in Cookie path and finding root(Redirect) path in app. Apps’ Redirect should have two type, they should explicit distingushed. Like below.

// In app0
Redirect::to("/otherurl") //Means redirect to /app0/otherurl
Redirect::to("http://xxx.com/") //I consider it means a relative path, redirect to /app0/currentpath/http:/xxx.com/
Redirect::absolute("/otherurl") //Means redirect to /otherurl
Redirect::absolute("http://xxx.com/") //Redirect to http://xxx.com/

//Maybe can let Redirect absolute url in following style is better.
Redirect::to("/otherurl").absolute();
Redirect::to("http://xxx.com/").absolute();

This design is important to avoid path confusion when move app to other sites. This feature should be confirmed before rocket 1.0 release.

3. Why this feature can't or shouldn't exist outside of Rocket.

Rocket provides a whole solution to deploy a http server, and willing to become a middleware (From Fairings guide ). So the SPMA feature, which is heavily use in enterprise projects, should not lacked. Every app should provide a way to run in standalone mode(currently supported) and deploy mode(support by SPMA). Deploy mode is useful in many scence such as virtual host providers (they may not provides many ports to launch, therefore deploy mode is necessary). In my comprehention, Rocket will act as a middleware like tomcat. Features currently implemented (0.3.2) is standard like java servlet. Some commercial Rocket container can be distributed based on Rocket, more and more frameworks can be used, apps can be compiled into a dynlilb, and dynamically deployed into the Rocket containers(dynamic deploy maybe version 2.0 feature)

SergioBenitez commented 7 years ago

I believe the vast majority of this is possible today by taking advantage of launch fairings for each sub-application. Let me walk through a tiny example of what I have in mind with sub-applications dashboard and home.

  1. First, we create a Cargo workspace. Each sub-application has its own crate with both a lib and bin. We also create one more crate with only a bin to combine our sub applications.

  2. We write each sub-application as if it was a regular Rocket application with one caveat: all configuration is done via a function that takes an instance of Rocket and some arbitrary Config structure and returns an instance of Result<Rocket, Rocket>. That function is then used in main. Each sub-application would thus look something like:

     extern crate rocket;
    
     use rocket::Rocket;
     use rocket::fairing::AdHoc;
    
     #[get("/hello")]
     fn index() -> &'static str {
         "Hello, world!"
     }
    
     // I use `mountpoint` here for illustration, but the idea is you'd pass any sub-app
     // specific config like this.
     pub fn app(rocket: Rocket, mountpoint: &str) -> Result<Rocket, Rocket> {
         Ok(rocket.mount(mountpoint, routes![index]))
     }
    
     fn main() {
         rocket::ignite()
             .attach(AdHoc::on_attach(|rocket| app(rocket, "/")))
             .launch();
     }
  3. Each sub-application is attached in the main application. As long as all apps use different types for their managed state (Rocket will error otherwise, so no concern of accidental collision is present), everything will work as expected. This would look like:

     extern crate dashboard;
     extern crate home;
    
     fn main() {
         rocket::ignite()
             .attach(AdHoc::on_attach(|rocket| dashboard::app(rocket, "/dashboard")))
             .attach(AdHoc::on_attach(|rocket| home::app(rocket, "/app")))
             .launch();
     }

I believe this accomplishes most of what you're looking for. The only missing feature from your request is a Redirect that's aware of sub-applications, though I'm not sure I understand what you're really asking for there. In any case, it seems that it would be pretty simple to build outside of Rocket, given how I've laid out the structure here.

In all, I don't think anything needs to change in Rocket to support what you'd like. It seems to me that launch fairings give you 95% of what you're looking for, and the other 5% could be a tiny set of types, functions, and methods that make things slightly nicer to use. All of those things could live outside of Rocket.

SuperHacker-liuan commented 7 years ago

@SergioBenitez

Test Case

Code

Below is /workspace/apps/home/src/lib.rs, which is a sub-app.

#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;
extern crate rocket_contrib;
extern crate serde_json;
#[macro_use]
extern crate serde_derive;

use std::path::{Path, PathBuf};

use rocket::Rocket;
use rocket::response::{NamedFile, Redirect};
use rocket_contrib::Template;

#[get("/<file..>")]
fn statics(file: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::new("statics/").join(file)).ok()
}

#[get("/")]
fn works() -> &'static str {
    "It works!"
}

#[derive(Serialize)]
struct TemplateContext {
    name: String,
}

#[get("/template")]
fn template() -> Template {
    let context = TemplateContext {
        name: "template works!".to_string(),
    };
    Template::render("template", &context)
}

#[get("/redirect")]
fn redirect() -> Redirect {
    Redirect::to("/")
}

pub fn deploy(rocket: Rocket, mountpoint: &str) -> Result<Rocket, Rocket> {
    let rocket = rocket.mount(mountpoint, routes![works, statics, template, redirect])
        .attach(Template::fairing());
    Ok(rocket)
}

Standalone mode

If which code run in standalone mode with mode.

// /workspace/apps/home/src/main.rs
extern crate rocket;
extern crate home;

use rocket::fairing::AdHoc;

fn main() {
    rocket::ignite()
        .attach(AdHoc::on_attach(|r| home::deploy(r, "/")))
        .launch();
}

All test will be passed. http://localhost:8000/ -> It works! http://localhost:8000/redirect -> redirect to http://localhost:8000/ http://localhost:8000/somefile.txt -> get /workspace/apps/home/statics/somefile.txt http://localhost:8000/template -> render /workspace/apps/home/templates.html.hbs

Deploy Mode

If I run it in deploy mode, cargo run in /workspace/server/

// /workspace/server/src/main.rs
extern crate rocket;
extern crate home;

use rocket::fairing::AdHoc;

fn main() {
    rocket::ignite()
        .attach(AdHoc::on_attach(|rocket| home::deploy(rocket, "/home")))
        .catch(errors::errors())
        .launch();
}

Test result is: http://localhost:8000/home/ -> It works! http://localhost:8000/home/redirect -> redirect to http://localhost:8000/ Failed, actually it should redirect to http://localhost:8000/home/ http://localhost:8000/home/somefile.txt -> 404 Not Found http://localhost:8000/template -> 500 Internal Error

Horrible feeling for maintainers

It means, one app has different behaviours in standalone(for developer team) mode and deploy(for production use) mode. We cannot deploy home app in out of box mode. Developers have to use conditions everywhere in home source code to deal with these two scenes.

More worth is. If boss decide to reuse home app in another site, and deploy it at http://xxx.com/user, developer had to add third condition to adapt which requirement.

Some day, boss decide to make home open source. Users around the world will deploy it in their own sites, but the app base will be named as room/, center/, etc./, all of them have to change source code in hunders of thouands lines, which is horrible.

Why these different behaviours happen

http://localhost:8000/home/redirect

In standalone mode, Redirect::to("/") means redirect to http://host.domain/ In deploy mode, the site base changed http://host.domain/home, but Redirect::to("/") still redirct Location to http://host.domain/, which means Redirect provide a redirect to the absolute path of the site. In my opinion, to avoid path confusion in different deploy path(which happens when reused by different projects), developers should use relative path, other than absolute path. I have just try, I can write Redirect::("./") to fix this bug, I failed to comprehent semantic of Redirect::to() in redirect examples I suggest to use relative redirect in redirect example

http://localhost:8000/home/somefile.txt

When run in deploy mode, the working directory(WD) is /workspace/server/, so the Path::new("statics/").join(file) will be /workspace/server/statics/${file}, rather than /workspace/apps/home/statics/${file}.

It means, rocket lacks of a built-in context root(CR) setting. Every depeloper have to implement their own CR handling.

http://localhost:8000/home/template

Same as above, built-in Template root path is ${WD}/${template_dir}, rather than ${CR}/${app_template_dir}. So Template render cannot found where the hbs file is, Cause a 500 Internal Error. Even if developers have been implemented the CR handler, Template will still ignore it, unless developer change the rocket_contrib::Template's implementation.

Feature request

  1. Rockt provides a standard(struct, API and etc.) to set the CR. Avoid using std::path::Path in apps, unless they need to access OS resources. Comprehent WD and the relative path to WD is a almost impossible for many outsourcing developers. Especially developers from javaweb. They don't know what's the difference between middleware WD and app CR.

  2. Make template render depends on CR API, other than middleware WD.

  3. Provide specifications describing the standard(i.e. Fairings must depend on CR) to develop fairings for rocket middleware when Rocket/1.0 release. Just like Java servlet specification. This can avoid chaos from framwork libraries developer. Its an important step to make rocket ready for enterprise use. We know, Struts, Spring are all depends on java servlet. java servlet and interceptors, which are the standard to develop java web frameworks.

SuperHacker-liuan commented 7 years ago

Maybe you can provide an example for how to do sub-app deploy on https://rocket.rs/ guide.

SuperHacker-liuan commented 7 years ago

Context Isolating

Another important feature is Context Isolating. Let's see what actually rocket do in deploy mode by using faring.

// /workspace/server/src/main.rs
 rocket::ignite()
        .attach(AdHoc::on_attach(|rocket| home::deploy(rocket, "/home")))
        .catch(errors())
        .launch();
// /workspcae/apps/home/src/lib.rs
    let rocket = rocket.mount(mountpoint, routes![works, statics, template, redirect])
        .attach(Template::fairing());
    Ok(rocket)

// It means
    rocket::ignite()
        .mount("/home", routes![works, statics, template, redirect])
        .attach(Template::fairing())
        .catch(errors())
        .launch();

apps/home and server share the same context. If I have multiple app, each need to attach its own framework fairings, their context will be polluted by each other.

What I really need in SPMA support is isolate the context (private configs / statics / templates / counters / session manager), and share the basic resource (connection pool / HTTP eventloop / HTTP port / error pages / access_logs).