App Frame
App Frame is a compile-time dependency-injected application framework with a service orchestrator. It has two general goals:
- Reliably run and monitor multiple long-running services, recovering from any errors.
- Reduce the boilerplate and maintenance cost of manually constructing application components that have a complex dependency graph. You should only need to describe your application's components. The rust compiler can wire them up.
At compile-time, the framework guarantees that all necessary dependencies will be injected upon application startup. At runtime, the framework runs your custom initialization code, then starts your services, automatically injects their dependencies, monitors their health, restarts unhealthy services, and reports their status to external http health checks.
Application frameworks add complexity and obscure control flow, but they can also save a lot of time with setup and maintenance tasks. To be sure app-frame is right for your project, see the Trade-offs section.
Usage
[]
= "0.3.1"
App Frame provides macros for convenience. If they seem too esoteric or inflexible, you can instead implement traits to wire up your application.
This trivial example illustrates the bare minimum boilerplate to use the framework with its macros, but doesn't actually run anything useful.
use ;
async
;
application!;
Here is the equivalent application, with direct trait implementation instead of macros:
use ;
async
;
To take full advantage of app-frame, you should define dependency relationships and explicitly declare every component that you want to run as part of your application.
Make Components Injectible
Make a struct injectible if you want its dependencies to be injected by app-frame, with either a macro or trait:
- macro: Wrap the struct with the
inject!macro. In a future release, this will be an attribute-style macro. - traits:
impl<T> From<&T> for C where T: Provides<D>for the componentCand each of its dependenciesD.
/// Macro approach. This automatically implements:
/// impl<T> From<&T> for InitJob
/// where
/// T: Provides<Arc<dyn Repository>> + Provides<Component2> {...}
inject!;
/// Trait approach
///
/// This is practical here since only one item needs to be injected,
/// and the others can be set to default values. The inject macro
/// does not yet support defaults.
Define Services
App Frame is built with the assumption that it will trigger some long running services on startup. To define a long-running service, either implement Service, or implement Job and SelfConfiguredLoop.
/// Implement Service when you already have some logic that runs forever
/// Implementing these traits will let you use a job that is expected to terminate
/// after a short time. It becomes a service that runs the job repeatedly.
Declare the app
Define a struct representing your app and populate it with any configuration or singletons.
Any components that are automatically constructed by the framework will be instantiated once for every component that depends on it. If you want a singleton that is created once and reused, it needs to be instantiated manually, like this example.
Declare application components.
Many dependency injection frameworks only need you to define your components, and the framework will locate them and create them as needed. App Frame instead prefers explicitness. You must list all application components in a single place, but App Frame will figure out how to wire them together. This is intended to make the framework's control flow easier to follow, though this benefit may be offset by using macros.
- macro: List all of your app's components in the
application!macro as documented below. - traits:
impl Initialize for MyApp, providing anyJobs that need to run at startup.impl Serves for MyApp, providing anyServices that need to run continuously.impl Provides<D> for MyAppfor each dependencyDthat will be needed either directly or transitively by any job or service provided above.
Customize Service Orchestration
You can customize monitoring and recovery behavior by starting your app with the run_custom method.
// This is the default config, which is used when you call `MyApp::new().run()`
// See rustdocs for RunConfig and HealthEndpointConfig for more details.
new
.run_custom
.await
Full macro-based example
This example defines and injects various types of components to illustrate the various features provided by the framework. This code actually runs an application, and will respond to health checks indicating that 2 services are healthy.
use Arc;
use async_trait;
use ;
async
// Including a type here implements Provides<ThatType> for MyApp.
//
// Struct definitions wrapped in the `inject!` macro get a From<T>
// implementation where T: Provides<U> for each field of type U in the struct.
// When those structs are provided as a component here, they will be constructed
// with the assumption that MyApp impl Provides<U> for each of those U's
//
// All the types provided here are instantiated separately each time they are
// needed. If you want to support a singleton pattern, you need to construct the
// singletons in the constructor for this type and wrap them in an Arc. Then you
// can provide them in the "provided" section by cloning the Arc.
application!
inject!;
inject!;
inject!;
inject!;
/// This is how you provide a custom alternative to the `inject!` macro, it is
/// practical here since only one item needs to be injected, and the others can
/// be set to default values.
inject!;
Trade-offs
Application frameworks are often not worth the complexity, but they do have utility in many cases. App Frame would typically be useful in a complicated backend web service with a lot of connections to other services, or whenever the following conditions are met:
- Multiple fallible long running tasks need to run indefinitely in parallel with monitoring and recovery.
- You want to use tokio's event loop to run suspending functions in parallel.
- The decoupling achieved by dependency inversion is beneficial enough to be worth the complexity of introducing layers of abstraction.
- The application has a complex dependency graph of internal components, and you'd like to make it easier to change the dependency relationships in the future.
- You want to be explicit, in a single place, about every component that should actually be instantiated when the app starts. This is a key differentiator from most other dependency injection frameworks.
- You don't mind gigantic macros that only make sense after reading documentation. Macros are not required to use App Frame, but you can use them to significantly reduce boilerplate.
- You are willing to compromise some performance and readability with the indirection of vtables and smart pointers.