DelicateWhy fromactix-webMigration topoem

Author: Orange cannon

The orange Cannon has left his job and is looking for a new one. If you are interested in his work and need a Rust engineer, please contact him at [email protected].


What is delicate ?

Delicate a lightweight distributed task scheduling platform.

features

  • Friendly user interface: [front] easy to manage tasks and actuators, monitor their status, support manual maintenance of running tasks, etc.

  • Flexible operation: Flexible task operation, supports the maximum number of concurrent tasks on a single node, time zone Settings corresponding to crON expressions, scheduling mode (single, fixed, and repeated), manually triggering tasks at any time, manually terminating task instances, and viewing task logs online.

  • High availability: Delicate supports horizontal scaling. High availability and performance can be easily achieved by deploying as many Delicate servers and actuators as possible.

  • High performance: Lightweight and basic features speed up performance. ‘Delicate’ has a base resource overhead of around (less than 0.1% CPU usage, 10m memory).

  • Observability: There are many meaningful statistics that are presented regularly in the form of charts.

  • Upgrade: Dynamic upgrade of the system (by obtaining the latest source code and migrating the database).

  • Reusability: The actuator provides restful apis that allow users to apply and maintain custom tasks.

  • Rights management: Casbin-based rights management functions continuously optimize the experience.

Delicate architecture diagram:

See: github.com/BinChengZha…

Technology stack

  • Back end (Scheduler & Executor): Rust

  • (Actix-Web & Diesel & Delay-Timer & Serde & Tracing)

  • Poems & Tokio & Diesel & Delay-Timer & Serde & tracing

  • Front end: ANTD-admin (React)

  • Ui: Ant Design

  • Databases: mysql, Postgres (program support)

Why migrate topoem?

  • When using actix-Web iterations, attempts to upgrade core dependencies and introduce new features were limited by the fact that actix-Web 4 stable had not been officially released. The tech stack renovation is an urgent project, and I knew my chance came when I launched my poem.

  • I feel more flexible than EVER when I use poem and rely transparently on Tokio.

The tokio ecosystem components were used directly to replace some of the original Actix-Web components, and a large number of dependencies were upgraded without having to manually patch or use outdated dependencies.

aboutpoemA brief background to.

  1. The framework has extremely fast performance, consistent philosophy, and clear implementation.
  2. Based on thehyper, andtokioCombined, users have more control.

Migration highlights:

  1. Recombination of network components, different styles of maintenance application state.

  2. API level modification to avoid business logic adjustment.

Basic grooming before migration:

  • The handler in POEM is an Endpoint object that generates a Future. The framework’s collaboration with Tokio allows requests to be evaluated in a multi-threaded runtime.

    This is not the case with Actix-Web, which is internally composed of multiple single-threaded runtimes. Due to this subtle difference, the handler used for atex-Web cannot be used directly on poem because it needs to ensure the input state of each handler and Send values across.await.

  • The route of POEM is a nested Endpoint data structure, different from the original Atex-Web configuration.

  • Poems exposes most of its data structures to support Send for efficient use of thread resources, whereas Actix-Web does the opposite.

  • All middleware implementations need to be modified, all background tasks need to be modified, and all global states need to be adjusted.

  • Upgrade multiple dependencies while directly dependent on Tokio 1.0.

  • Full link test, and write migration notes.

Here are somepoem & actix-webContrast:

Routing side

In the previous implementation based on Atex-Web, a large number of routing groups were de-registered through configure, application status was registered through App_data, and middleware was registered through WRAP:

let app = App::new()
    .configure(actions::task::config)
    .configure(actions::user::config)
    .configure(actions::task_log::config)
    .configure(actions::executor_group::config)
    .configure(actions::executor_processor::config)
    .configure(actions::executor_processor_bind::config)
    .configure(actions::data_reports::config)
    .configure(actions::components::config)
    .configure(actions::operation_log::config)
    .configure(actions::user_login_log::config)
    .app_data(shared_delay_timer.clone())
    .app_data(shared_connection_pool.clone())
    .app_data(shared_scheduler_meta_info.clone())
    .wrap(components::session::auth_middleware())
    .wrap(components::session::session_middleware());
Copy the code

Routing configuration examples:

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(create_user)
        .service(show_users)
        .service(update_user)
        .service(delete_user)
        .service(login_user)
        .service(logout_user)
        .service(check_user)
        .service(change_password)
        .service(roles)
        .service(permissions)
        .service(append_permission)
        .service(delete_permission)
        .service(append_role)
        .service(delete_role);
}

Copy the code

Handler Example of processing a request:

#[post("/api/user/create")]
async fn create_user(
    web::Json(user): web::Json<model::QueryNewUser>,
    pool: ShareData<db::ConnectionPool>,
) -> HttpResponse {

    // do someting.
}
Copy the code

Based on the realization of POEM, a large number of routing groups are organized by Route, which can be nested multiple times. Application state and middleware are registered with, and all components have a common characteristic Endpoint:

let app = Route::new().nest_no_strip(
            "/api",
            Route::new()
                .nest_no_strip("/api/task", actions::task::route_config())
                .nest_no_strip("/api/user", actions::user::route_config())
                .nest_no_strip("/api/role", actions::role::route_config())
                .nest_no_strip("/api/task_log", actions::task_log::route_config())
                .nest_no_strip("/api/tasks_state", actions::data_reports::route_config())
                .nest_no_strip("/api/task_instance", actions::task_instance::route_config())
                .nest_no_strip("/api/binding", actions::components::binding::route_config())
                .nest_no_strip("/api/operation_log". actions::operation_log::route_config()) ) .with(shared_delay_timer) .with(shared_connection_pool) .with(shared_scheduler_meta_info) .with(shared_request_client) .with(components::session::auth_middleware()) .with(components::session::cookie_middleware());Copy the code

Route configuration examples in POEM:

pub fn route_config() -> Route {
    Route::new()
        .at("/api/user/create", post(create_user))
        .at("/api/user/list", post(show_users))
        .at("/api/user/update", post(update_user))
        .at("/api/user/delete", post(delete_user))
        .at("/api/user/login", post(login_user))
        .at("/api/user/logout", post(logout_user))
        .at("/api/user/check", post(check_user))
        .at("/api/user/change_password", post(change_password))
        .at("/api/user/roles", post(roles))
        .at("/api/user/permissions", post(permissions))
        .at("/api/user/append_permission", post(append_permission))
        .at("/api/user/delete_permission", post(delete_permission))
        .at("/api/user/append_role", post(append_role))
        .at("/api/user/delete_role", post(delete_role))
}
Copy the code

Sample handler processing request in POEM:

async fn create_user(
    web::Json(user): web::Json<model::QueryNewUser>,
    pool: ShareData<db::ConnectionPool>,
) -> HttpResponse {

    // do someting.
}
Copy the code

poemSubstitution of ideas:

handler

Handler in POEM is not much different from Atex-Web, only some extractor needs to be adjusted. For some obstructing tasks, switch to TOkio API for calculation

#[handler]

async fn show_task_log_detail(
    Json(query_params): Json<model::RecordId>,
    pool: Data<&Arc<db::ConnectionPool>>,
) -> impl IntoResponse {
    use db::schema::task_log_extend;

    if let Ok(conn) = pool.get() {
        let f_result = spawn_blocking::<_, Result<_, diesel::result::Error>>(move| | {let task_log_extend = task_log_extend::table
                .find(query_params.record_id.0) .first::<model::TaskLogExtend>(&conn)? ;Ok(task_log_extend)
        })
        .await;

        let log_extend = f_result
            .map(|log_extend_result| {
                Into::<UnifiedResponseMessages<model::TaskLogExtend>>::into(log_extend_result)
            })
            .unwrap_or_else(|e| {
                UnifiedResponseMessages::<model::TaskLogExtend>::error()
                    .customized_error_msg(e.to_string())
            });
        return Json(log_extend);
    }

    Json(UnifiedResponseMessages::<model::TaskLogExtend>::error())
}
Copy the code

Endpoint

An Endpoint abstracts the Trait of an HTTP request, which is the real face of all handlers.

You can implement an Endpoint to create your own Endpoint handler.

Here is the definition of an Endpoint:

/// An HTTP request handler.
#[async_trait::async_trait]
pub trait Endpoint: Send + Sync {
    /// Represents the response of the endpoint.
    type Output: IntoResponse;

    /// Get the response to the request.
    async fn call(&self, req: Request) -> Self::Output;
}
Copy the code

The Endpoint philosophy of POEM is very similar to that of Service in Tower, but poem is more concise. Moreover, POEM is compatible with Tower and can reuse its ecology and components.

/// `Service` provides a mechanism by which the caller is able to coordinate
/// readiness. `Service::poll_ready` returns `Ready` if the service expects that
/// it is able to process a request.
pub trait Service<Request> {
    /// Responses given by the service.
    type Response;

    /// Errors produced by the service.
    type Error;

    /// The future response value.
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    /// Returns `Poll::Ready(Ok(()))` when the service is able to process 
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;

    /// Process the request and return the response asynchronously.
    fn call(&mut self, req: Request) -> Self::Future;
}
Copy the code

IntoResponse

IntoResponse is an abstraction of the response data.

All Response types that can be converted to HTTP responses should be implemented IntoResponse, and they can be returned by handler.

pub trait IntoResponse: Send {
    /// Consume itself and return [`Response`].
    fn into_response(self) -> Response;

    /// Wrap an `impl IntoResponse` to add a header.
    fn with_header<K, V>(self, key: K, value: V) -> WithHeader<Self>
    where
        K: TryInto<HeaderName>,
        V: TryInto<HeaderValue>,
        Self: Sized,
    {
        let key = key.try_into().ok();
        let value = value.try_into().ok();

        WithHeader {
            inner: self,
            header: key.zip(value),
        }
    }

    /// Wrap an `impl IntoResponse` to set a status code.
    fn with_status(self, status: StatusCode) -> WithStatus<Self>
    where
        Self: Sized,
    {
        WithStatus {
            inner: self,
            status,
        }
    }

    /// Wrap an `impl IntoResponse` to set a body.
    fn with_body(self, body: impl Into<Body>) -> WithBody<Self>
    where
        Self: Sized,
    {
        WithBody {
            inner: self,
            body: body.into(),
        }
    }
}

Copy the code

middleware

Using POEMS makes middleware very easy. Here is a middlware example that adds logger-ID to a request:

// Unit-struct of logger-id for impl Middleware.
pub struct LoggerId;

impl<E: Endpoint> Middleware<E> for LoggerId {
    type Output = LoggerIdMiddleware<E>;

    fn transform(&self, ep: E) -> Self::Output {
        LoggerIdMiddleware { ep }
    }
}
// Wraps the original handler and logs the processing of the request internally.
pub struct LoggerIdMiddleware<E> {
    ep: E,
}

#[poem::async_trait]
impl<E: Endpoint> Endpoint for LoggerIdMiddleware<E> {
    type Output = E::Output;

    async fn call(&self, req: Request) -> Self::Output {
        let unique_id = get_unique_id_string();
        self.ep .call(req) .instrument(info_span! ("logger-", id = unique_id.deref()))
            .await}}Copy the code

Here is an example of a template for Actix-Web to implement Middlware, and the template code is a bit verbose and interesting indeed.

pub(crate) struct CasbinService;

impl<S, B> Transform<S> for CasbinService
where
    S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = ActixWebError>
        + 'static,
    S::Future: 'static,
    B: 'static,
{
    type Request = ServiceRequest;
    type Response = ServiceResponse<B>;
    type Error = ActixWebError;
    type InitError = ();
    type Transform = CasbinAuthMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ok(CasbinAuthMiddleware {
            service: Rc::new(RefCell::new(service)),
        })
    }
}

pub struct CasbinAuthMiddleware<S> {
    service: Rc<RefCell<S>>,
}


impl<S, B> Service for CasbinAuthMiddleware<S>
where
    S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = ActixWebError>
        + 'static,
    S::Future: 'static,
    B: 'static,
{
    type Request = ServiceRequest;
    type Response = ServiceResponse<B>;
    type Error = ActixWebError;
    type Future = MiddlewareFuture<Self::Response, Self::Error>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.service.poll_ready(cx)
    }

    fn call(&mut self, req: ServiceRequest) -> Self::Future {
        Box::pin(async move {

           // do something.
           return service.call(req).await; })}Copy the code

conclusion

  1. The migration involved 45 file changes and 4000 lines of code changes (2500 lines were added and 1579 lines deleted).

  2. Switching to POEM, the old dependencies are upgraded and the project is more flexible than ever as it relies transparently on the Tokio ecosystem. No more hand-making patches or using old dependencies.

  3. After migration, POEM & Tokio ecological bonus is easier to expand its functions, and also reduces the maintenance cost.

  4. This improves resource utilization and gives full play to the advantages of multiple cores without affecting performance indicators.

Thank you

In the process of migration, I couldn’t deal with some demands on poem directly. Then I opened several issues on poem, communicated with the author in less than one day, and supported this function in poem, which is very powerful!

  • I would like to thank the entire community and contributors. In particular,poemThe author:

The elder brother of the twisted dough-strips

  • Thank you for reporting typos in your documentation. Thank you very much.
  • Thank you for joining us, providing feedback, discussing features, and getting help!
  • I appreciate it, tooactix-webCommunity such a good work, because of the choice of technology, I decided to migrate topoem.

Repos:

poem

delicate