#框架 #服务 #注入 #注入 #依赖

app-frame

编译时依赖注入的应用程序框架,具有服务编排器

6个版本

0.3.1 2023年10月28日
0.3.0 2023年10月28日
0.2.1 2023年8月9日
0.1.1 2023年8月9日

#146配置

Download history 25/week @ 2024-03-13 4/week @ 2024-03-27 3/week @ 2024-04-03

每月52 次下载

MIT/Apache

97KB
1.5K SLoC

App Frame

github · crates.io · docs.rs

App Frame是一个具有服务编排器的编译时依赖注入的应用程序框架。它有两个主要目标

  1. 可靠地运行和监控多个长时间运行的服务,从任何错误中恢复。
  2. 减少手动构建具有复杂依赖图的复杂应用组件的样板代码和维护成本。您只需要描述您应用程序的组件。Rust编译器可以连接它们。

在编译时,框架保证在应用程序启动时注入所有必要的依赖项。在运行时,框架运行您的自定义初始化代码,然后启动您的服务,自动注入它们的依赖项,监控它们的健康状态,重启不健康的服务,并将它们的状况报告给外部的http健康检查。

应用程序框架增加了复杂性并模糊了控制流,但它们也可以通过设置和维护任务节省大量时间。要确定app-frame是否适合您的项目,请参阅权衡部分。

用法

[dependencies]
app-frame = "0.3.1"

App Frame提供宏以方便使用。如果它们看起来过于神秘或缺乏灵活性,您可以使用特质来实现应用程序的连接。

以下是一个简单的示例,说明使用宏与框架一起使用所需的最小样板代码,但实际上并没有运行任何有用的内容。

use app_frame::{application, service_manager::Application, Never};

#[tokio::main]
async fn main() -> anyhow::Result<Never> {
    MyApp.run().await
}

pub struct MyApp;

application!(self: MyApp);

以下是等效的应用程序,使用直接特质实现而不是宏

use app_frame::{application, service_manager::Application, Never};

#[tokio::main]
async fn main() -> anyhow::Result<Never> {
    MyApp.run().await
}

pub struct MyApp;

impl Initialize for MyApp {}

impl Serves for MyApp {
    fn services(&self) -> Vec<Box<dyn Service>> {
        vec![]
    }
}

为了充分利用app-frame,您应该定义依赖关系,并明确声明您希望作为应用程序一部分运行的所有组件。

使组件可注入

如果您希望app-frame通过宏或特质注入其依赖项,请使结构可注入

  • 宏:使用inject!宏将结构包装起来。在未来版本中,这将是一个属性风格的宏。
  • 特性: impl<T> From<&T> for C where T: Provides<D> 为组件 C 及其所有依赖 D 提供的。
/// Macro approach. This automatically implements:
/// impl<T> From<&T> for InitJob 
/// where 
///       T: Provides<Arc<dyn Repository>> + Provides<Component2> {...}
inject!(
    pub struct Component1 {
        repo: Arc<dyn Repository>,
        component2: Component2,
    }
);
/// 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.
impl<T> From<&T> for MyService
where
    T: Provides<Arc<dyn Repository>>,
{
    fn from(p: &T) -> Self {
        Self {
            repository: p.provide(),
            heartbeat: Arc::new(()),
            my_health_metric: true,
        }
    }
}

定义服务

App Frame 假设应用启动时会触发一些长时间运行的服务。要定义长时间运行的服务,可以实现 Service,或者实现 JobSelfConfiguredLoop

/// Implement Service when you already have some logic that runs forever
#[async_trait]
impl Service for MyService {
    async fn run_forever(&self) -> Never {
        // This is a trivial example. You can call something that you expect to run forever.
        loop {
            self.heartbeat.beat();
        }
    }

    fn heartbeat_ttl(&self) -> i32 {
        60
    }

    fn set_heartbeat(&mut self, heartbeat: Arc<dyn Heartbeat + 'static>) {
        self.heartbeat = heartbeat;
    }

    fn is_healthy(&self) -> bool {
        self.my_health_metric
    }
}
/// 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.
#[async_trait]
impl Job for JobToLoopForever {
    async fn run_once(&self) -> anyhow::Result<()> {
        self.solve_halting_problem()
    }
}

impl SelfConfiguredLoop for JobToLoopForever {
    fn loop_config(&self) -> LoopConfig {
        LoopConfig {
            delay_secs: 10,
            max_iteration_secs: 20,
        }
    }
}

声明应用

定义一个表示您的应用的 struct,并用任何配置或单例填充它。

框架自动构建的任何组件将为每个依赖它的组件实例化一次。如果您需要一个在创建后重用的单例,则需要手动实例化,如下例所示。

pub struct MyApp {
    db_singleton: Arc<DatabaseConnectionPoolSingleton>,
}

impl MyApp {
    pub fn new(database_config: &str) -> Self {
        Self {
            db_singleton: Arc::new(DatabaseConnectionPoolSingleton {
                conn: database_config.into(),
            }),
        }
    }
}

声明应用程序组件。

许多依赖注入框架只需要您定义组件,框架将找到它们并在需要时创建它们。App Frame 则更倾向于明确性。您必须在单一位置列出所有应用程序组件,但 App Frame 将确定如何将它们连接起来。这旨在使框架的控制流程更容易跟踪,尽管使用宏可能会抵消这一好处。

  • 宏:按照以下说明,在 application! 宏中列出您应用的全部组件。
  • 特性
    • impl Initialize for MyApp,提供在启动时需要运行的任何 Job
    • impl Serves for MyApp,提供需要持续运行的任何 Service
    • impl Provides<D> for MyApp 为每个依赖 D,这些依赖将由上述任何作业或服务直接或间接使用。

自定义服务编排

您可以通过使用 run_custom 方法启动您的应用来自定义监控和恢复行为。

// This is the default config, which is used when you call `MyApp::new().run()`
// See rustdocs for RunConfig and HealthEndpointConfig for more details.
MyApp::new()
    .run_custom(RunConfig {
        log_interval: 21600,
        attempt_recovery_after: 120,
        http_health_endpoint: Some(HealthEndpointConfig {
            port: 3417,
            success_status: StatusCode::OK,
            fail_status: StatusCode::INTERNAL_SERVER_ERROR,
        }),
    })
    .await

基于宏的全示例

此示例定义并注入各种类型的组件,以展示框架提供的各种功能。此代码实际上运行了一个应用,并将响应健康检查,表明有 2 个服务是健康的。

use std::sync::Arc;

use async_trait::async_trait;

use app_frame::{
    application,
    dependency_injection::Provides,
    inject,
    service::{Job, LoopConfig, LoopingJobService, SelfConfiguredLoop, Service},
    service_manager::{Application, Heartbeat},
    Never,
};

#[tokio::main]
async fn main() -> anyhow::Result<Never> {
    MyApp::new("db://host").run().await
}

pub struct MyApp {
    db_singleton: Arc<DatabaseConnectionPoolSingleton>,
}

impl MyApp {
    pub fn new(database_config: &str) -> Self {
        Self {
            db_singleton: Arc::new(DatabaseConnectionPoolSingleton {
                conn: database_config.into(),
            }),
        }
    }
}

// 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! {
    self: MyApp

    // init jobs are types implementing `Job` with a `run_once` function that
    // needs to run once during startup.
    // - constructed the same way as a component
    // - made available as a dependency, like a component
    // - wrap in curly braces for custom construction of an iterable of jobs.
    init [
        InitJob
    ]

    // Services are types with a `run_forever` function that needs to run for
    // the entire lifetime of the application.
    // - constructed the same way as a component
    // - made available as a dependency, like a component
    // - registered as a service and spawned on startup.
    // - wrap in curly braces for custom construction of an iterable of
    //   services.
    // - Use 'as WrapperType' if it needs to be wrapped in order to get
    //   something that implements `Service`. wrapping uses WrapperType::from().
    services [
        MyService,
        JobToLoopForever as LoopingJobService,
    ]

    // Components are items that will be provided as dependencies to anything
    // that needs it. This is similar to the types provided in the "provides"
    // section, except that components can be built exclusively from other
    // components and provided types, whereas "provides" items depend on other
    // state or logic.
    // - constructed via Type::from(MyApp). Use the inject! macro on the
    //   type to make this possible.
    // - Use `as dyn SomeTrait` if you also want to provide the type as the
    //   implementation for Arc<dyn SomeTrait>
    components [
        Component1,
        Component2,
        DatabaseRepository as dyn Repository,
    ]

    // Use this when you want to provide a value of some type that needs to either:
    // - be constructed by some custom code you want to write here.
    // - depend on some state that was initialized in MyApp.
    //
    // Syntax: Provide a list of the types you want to provide, followed by the
    // expression that can be used to instantiate any of those types.
    // ```
    // TypeToProvide: { let x = self.get_x(); TypeToProvide::new(x) },
    // Arc<dyn Trait>, Arc<ConcreteType>: Arc::new(ConcreteType::default()),
    // ```
    provided {
        Arc<DatabaseConnectionPoolSingleton>: self.db_singleton.clone(),
    }
}

inject!(
    pub struct InitJob {
        repo: Arc<dyn Repository>,
    }
);

#[async_trait]
impl Job for InitJob {
    async fn run_once(&self) -> anyhow::Result<()> {
        Ok(())
    }
}

inject!(
    pub struct JobToLoopForever {
        c1: Component1,
        c2: Component2,
    }
);

#[async_trait]
impl Job for JobToLoopForever {
    async fn run_once(&self) -> anyhow::Result<()> {
        Ok(())
    }
}

impl SelfConfiguredLoop for JobToLoopForever {
    fn loop_config(&self) -> LoopConfig {
        LoopConfig {
            delay_secs: 10,
            max_iteration_secs: 20,
        }
    }
}

inject!(
    pub struct Component1 {}
);

inject!(
    pub struct Component2 {
        repo: Arc<dyn Repository>,
    }
);

pub trait Repository: Send + Sync {}

pub struct MyService {
    repository: Arc<dyn Repository>,
    heartbeat: Arc<dyn Heartbeat + 'static>,
    my_health_metric: bool,
}

/// 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.
impl<T> From<&T> for MyService
where
    T: Provides<Arc<dyn Repository>>,
{
    fn from(p: &T) -> Self {
        Self {
            repository: p.provide(),
            heartbeat: Arc::new(()),
            my_health_metric: true,
        }
    }
}

#[async_trait]
impl Service for MyService {
    async fn run_forever(&self) -> Never {
        loop {
            self.heartbeat.beat();
        }
    }

    fn heartbeat_ttl(&self) -> i32 {
        60
    }

    fn set_heartbeat(&mut self, heartbeat: Arc<dyn Heartbeat + 'static>) {
        self.heartbeat = heartbeat;
    }

    fn is_healthy(&self) -> bool {
        self.my_health_metric
    }
}

inject!(
    pub struct DatabaseRepository {
        connection: Arc<DatabaseConnectionPoolSingleton>,
    }
);

impl Repository for DatabaseRepository {}

pub struct DatabaseConnectionPoolSingleton {
    conn: String,
}

权衡

应用程序框架通常不值得复杂,但它们在许多情况下都有实用性。App Frame 通常在复杂的后端 Web 服务中很有用,该服务与其他服务有很多连接,或者在以下条件满足时:

  • 需要并行运行多个可能会失败的长时间任务,并具有监控和恢复功能。
  • 您想使用 tokio 的事件循环来并行运行挂起函数。
  • 通过依赖倒置实现的解耦,其益处足以抵消引入抽象层级的复杂性。
  • 应用程序具有复杂的内部组件依赖图,您希望使将来更改依赖关系更简单。
  • 您希望在启动应用程序时,明确指定应实例化的每个组件。这是与其他大多数依赖注入框架的关键区别。
  • 您不介意使用只有阅读文档后才有意义的巨大宏。使用App Frame不需要宏,但您可以使用它们来显著减少样板代码。
  • 您愿意为了vtables和智能指针的间接性,在一定程度上妥协性能和可读性。

依赖项

~11–20MB
~263K SLoC