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

Is it possible to modify the routes list at runtime? #1517

Closed
TheLonelyAdventurer opened this issue Jan 27, 2021 · 4 comments
Closed

Is it possible to modify the routes list at runtime? #1517

TheLonelyAdventurer opened this issue Jan 27, 2021 · 4 comments

Comments

@TheLonelyAdventurer
Copy link

TheLonelyAdventurer commented Jan 27, 2021

Rocket version: latest (I think it's 0.4.6)

Hello, I'm a novice with Rust and Rocket.

I was thinking it would be nice to have the possibility to add new routes at runtime (if it's not already possible to do so).

I'm coming from Java and the usual use-case is when deploying new servlets and serving them without reloading the application container (see modules or OSGi-related technologies). In my specific case, I'm creating my own website with Rocket to practice my Rust skills and I would prefer to just add new routes on-the-fly or replace them (maybe mimicing the common osgi path observer approach and some FFI).

I am not sure since I'm a beginner, but maybe it is possible to impl Clone for the Rocket trait by providing a cloneable thread-safe box (I'm thinking about something like Arc<Rocket>).

What I would like to achieve is something like these examples below.

Hypothetical "add new route" request:

POST /admin/routes/new
name=hello&applicationPath=./libs/helloRoute.dll
#[post("/admin/routes/new?<name>&<applicationPath>")]
fn newRoute(name: &str, applicationPath: &str, rocket: Rocket) {
    // ...
    rocket.mount(name, ffiRouteFn);
    // ...
}

Hypothetical "unregister route" request:

DELETE /admin/route/hello
#[delete("/admin/routes/<name>")]
fn newRoute(name: &str, rocket: Rocket) {
    // ...
    rocket.unmount(name);
    // ...
}

Thank you for your replies in advance.

@jebrosen
Copy link
Collaborator

There are a few obstacles to supporting this sort of feature. It is possible in principle, though I am not sure what exactly the costs would be and whether they align with Rocket's goals.

  • There is currently no way to access Rocket itself at runtime. A large part of this is that launch() takes self by-move. We could however add a proxy type, so that e.g. #[post(...)] fn add_route(routes: ModifiableRoutes) could be used instead.
  • Rocket's Request type contains a reference to the route (an &Route), and the routes themselves are stored in a dynamically-resizable container. This means that adding or removing routes would necesitate a very different approach, probably introducing a performance hit somewhere - either on every request to clone the Route, or on every access to the request's Route by using some sort of index instead of a single pointer.

More broadly, several aspects of Rocket (and/or Rust) benefit greatly from things being known at compile-time or at launch-time and can be optimized accordingly. That is not to say that dynamic approaches like yours are impossible, simply more difficult to implement and/or less performant.

That said, there are a few potential workarounds that come to mind that would work "inside" of Rocket's capabilities.

  • Swapping out the entire Rocket instance and re-listening, which isn't quite hot-swapping in the way I think you meant. This will probably only be possible with Clean shutdown? #180 and will still have several downsides and corner cases.
  • Mounting some kind of catch-all route (for example with Handler), and managing and dispatching the dynamically added routes yourself.

Off the top of my head, I don't know of the most popular Rust web frameworks having similar features - but I would be interested to know if someone else does happen to be doing this already.

@TheLonelyAdventurer
Copy link
Author

There are a few obstacles to supporting this sort of feature. It is possible in principle, though I am not sure what exactly the costs would be and whether they align with Rocket's goals.

Hm, I see.

There is currently no way to access Rocket itself at runtime. A large part of this is that launch() takes self by-move. We could however add a proxy type, so that e.g. #[post(...)] fn add_route(routes: ModifiableRoutes) could be used instead.

I think it would be a good compromise if it's going to be a thing. Other context-related objects are already being injected this way (I'm referring to Form<> and others) if I'm not worng.

Rocket's Request type contains a reference to the route (an &Route), and the routes themselves are stored in a dynamically-resizable container. This means that adding or removing routes would necesitate a very different approach, probably introducing a performance hit somewhere - either on every request to clone the Route, or on every access to the request's Route by using some sort of index instead of a single pointer.

More broadly, several aspects of Rocket (and/or Rust) benefit greatly from things being known at compile-time or at launch-time and can be optimized accordingly. That is not to say that dynamic approaches like yours are impossible, simply more difficult to implement and/or less performant.

I think I understand. Though, just wondering (I'm still thinking like I would in another language), would something like a linked list (via RefCell::borrow_mut both sides) instead of a Vec<> be an option? This way you would still have a HashMap<> (I'm referring to the Router struct) but the elements list can grow and be sorted without having to clone the entire structure again (or maybe I'm missing something?). I have read this article which seems convincing.

That said, there are a few potential workarounds that come to mind that would work "inside" of Rocket's capabilities.

Swapping out the entire Rocket instance and re-listening, which isn't quite hot-swapping in the way I think you meant. This will probably only be possible with #180 and will still have several downsides and corner cases.

Mounting some kind of catch-all route (for example with Handler), and managing and dispatching the dynamically added routes yourself.

I think an approach like this would work:

use std::path::PathBuf;

#[get("/plugin/<path..>")]
fn process_plugin_request(path: PathBuf) {
    // [...] code required to obtain the foreign procedure and match with route subpath
    ffiPluginFn(...);
}

@jebrosen
Copy link
Collaborator

(I'm still thinking like I would in another language)

My (limited) understanding of Java's object model, to use your original comparison, is that every garbage-collected object has "built-in" synchronization overhead that is similar to what we would need to add here - of course, a lot of the actual performance impact depends on the specific usage patterns. My concern is not that this is a bad or too-difficult proposal in itself, but that implementing this or similar features would "undo" some of the performance benefits that are gained by using Rust/Rocket instead of another framework or language - which for some users could be the reason to use it in the first place.

(or maybe I'm missing something?)

One particular issue you might have missed is that one thread may try to "remove" a route that another thread(s) are running that route - so instead of RefCell, we would need the comparatively more expensive thread-safe Mutex and/or Arc. These are of course solvable, but I expect any solution to this to negatively impact performance compared to the status quo in some way - either when adding new routes, or on each incoming request, or both.

I'm, personally speaking, happy to consider this sort of functionality (and even offer some guidance on how Rocket would have to change to support it), but I do expect it to be performance-sensitive enough to need significant effort in auditing and testing.

@TheLonelyAdventurer
Copy link
Author

(I'm still thinking like I would in another language)

My (limited) understanding of Java's object model, to use your original comparison, is that every garbage-collected object has "built-in" synchronization overhead that is similar to what we would need to add here - of course, a lot of the actual performance impact depends on the specific usage patterns. My concern is not that this is a bad or too-difficult proposal in itself, but that implementing this or similar features would "undo" some of the performance benefits that are gained by using Rust/Rocket instead of another framework or language - which for some users could be the reason to use it in the first place.

Ah, right, I see.

(or maybe I'm missing something?)

One particular issue you might have missed is that one thread may try to "remove" a route that another thread(s) are running that route - so instead of RefCell, we would need the comparatively more expensive thread-safe Mutex and/or Arc. These are of course solvable, but I expect any solution to this to negatively impact performance compared to the status quo in some way - either when adding new routes, or on each incoming request, or both.

Ah, right! Yeah they would be more appropriate in this context.

I see your concern; it might introduce unwanted bottlenecks to request handling and routing.

I'm, personally speaking, happy to consider this sort of functionality (and even offer some guidance on how Rocket would have to change to support it), but I do expect it to be performance-sensitive enough to need significant effort in auditing and testing.

I think I got it now. Well, I am not fluent enough with Rust to do it myself yet ahah

Thank you for your replies; I'll try to dispatch the requests manually then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants