Shuttleはいいぞ~、ナナオです。

今回で三回目になります、Shuttleのチュートリアル記事です。

複数のサービスを動かす

shuttle_runtime::Serviceを実装してあげることで、Webサービスとスケジューラを同時に動かすようなユースケースが実装可能になります。

Welcome to Shuttle's Docs - Start Building Backends Fast - Shuttle

こちらのチュートリアルではdiscordのBOTを動かしていますね。

以前までの実装にスケジューラを追加して、継続的にログにhello worldを出力させてみましょう。

ただactix-webでの実装がわからなかったので、axumの実装に直しました。

また、スケジューラにはtokio-cron-schedulerを使用しました。

依存関係は以下のとおりです。

[dependencies]
axum = "0.6.20"
shuttle-runtime = "0.31.0"
tokio = "1.26.0"
tokio-cron-scheduler = "0.9.4"

では、以下が完成したコードです。

use axum::{routing::get, Router, response::IntoResponse};
use tokio_cron_scheduler::{Job, JobScheduler};

async fn hello_world() -> impl IntoResponse {
    "Hello World!"
}

pub struct MultipleService {
    router: Router,
    scheduler: JobScheduler,
}

impl MultipleService {
    pub async fn new() -> Result<MultipleService, shuttle_runtime::Error> {
        let router = Router::new()
            .route("/", get(hello_world));

        let mut scheduler = JobScheduler::new().await.unwrap();

        scheduler.add(
            Job::new("1/10 * * * * *", |_uuid, _l| {
                println!("I run every 10 seconds");
            }).unwrap()
        ).await.unwrap();

        Ok(Self {
            router,
            scheduler,
        })
    }
}

#[shuttle_runtime::async_trait]
impl shuttle_runtime::Service for MultipleService {
    async fn bind(mut self, addr: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> {
        let server = axum::Server::bind(&addr);

        let (_runner_hdl, _axum_hdl) =
            tokio::join!(self.scheduler.start(), server.serve(self.router.into_make_service()));

        Ok(())
    }
}

#[shuttle_runtime::main]
async fn main() -> Result<MultipleService, shuttle_runtime::Error> {
    MultipleService::new().await
}

順番に説明します。

まず、複数のサービスを動かすベースとなるMultipleServiceを定義してあげます。

フィールドにはWebサービスのためのrouterと、スケジューラサービスのschedulerを定義しました。

pub struct MultipleService {
    router: Router,
    scheduler: JobScheduler,
}

このベースとなるサービスをエントリーポイントから初期化して呼び出すためのnew関数を実装します。

ここではスケジューラの初期化及びジョブの追加とルータの初期化処理を行い、構造体にセットする処理を実装しました。

impl MultipleService {
    pub async fn new() -> Result<MultipleService, shuttle_runtime::Error> {
        let router = Router::new()
            .route("/", get(hello_world));

        let mut scheduler = JobScheduler::new().await.unwrap();

        scheduler.add(
            Job::new("1/10 * * * * *", |_uuid, _l| {
                println!("I run every 10 seconds");
            }).unwrap()
        ).await.unwrap();
    
        scheduler.set_shutdown_handler(Box::new(|| {
            Box::pin(async move {
                println!("Shut down done");
            })
        }));

        Ok(Self {
            router,
            scheduler,
        })
    }
}

そして、この独自に作ったサービスをShuttleで動かすために、shuttle_runtime::ServiceMultipleServiceに実装してあげます。

bind関数は引数としてアドレスを持っているので、このアドレスをWebサーバーに渡してあげた後、tokio::joinを使用してWebサーバーとスケジューラを非同期に動かしています。

#[shuttle_runtime::async_trait]
impl shuttle_runtime::Service for MultipleService {
    async fn bind(mut self, addr: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> {
        let server = axum::Server::bind(&addr);

        let (_runner_hdl, _axum_hdl) =
            tokio::join!(self.scheduler.start(), server.serve(self.router.into_make_service()));

        Ok(())
    }
}

あとはShuttleでMultipleServiceのインスタンスを渡してあげれば、スケジューラとWebサーバーがどちらも動くようになります。

#[shuttle_runtime::main]
async fn main() -> Result<MultipleService, shuttle_runtime::Error> {
    MultipleService::new().await
}

実際に動かしてみます。

> cargo shuttle run
...中略...
2023-11-04T11:18:38.540+09:00 [Runtime] shuttle-runtime executable started (version 0.31.0)
2023-11-04T11:18:38.540+09:00 [Runtime] ===============================================================
2023-11-04T11:18:38.540+09:00 [Runtime] Shuttle's default tracing subscriber is initialized!
2023-11-04T11:18:38.541+09:00 [Runtime] To disable the subscriber and use your own,
2023-11-04T11:18:38.541+09:00 [Runtime] turn off the default features for shuttle-runtime:
2023-11-04T11:18:38.541+09:00 [Runtime]
2023-11-04T11:18:38.541+09:00 [Runtime] shuttle-runtime = { version = "...", default-features = false }
2023-11-04T11:18:38.541+09:00 [Runtime] ===============================================================
2023-11-04T11:18:38.542+09:00 [Runtime] loading alpha service at C:\Users\Nanao\Project\study\shuttle-playground\target\debug\backend.exe
2023-11-04T11:18:38.915+09:00 [Runtime]  INFO tokio_cron_scheduler::job_scheduler: Uninited
2023-11-04T11:18:38.915+09:00 [Runtime]  INFO tokio_cron_scheduler::job_scheduler: Job creator created
No resources are linked to this service

    Starting backend on http://127.0.0.1:8000

2023-11-04T11:18:38.917+09:00 [Runtime] Starting on 127.0.0.1:8000
2023-11-04T11:18:41.446+09:00 [Runtime] I run every 10 seconds

スケジューラのログが出力されていることがわかります。

Webサーバーにアクセスした様子

Webサーバーのほうもちゃんと動いています!

ではこれをデプロイしてみましょう。

無事デプロイまでできました!

まとめ

今回は複数のサービスをShuttleで動かす方法について学びました。

Shuttle面白いですね。これでいろいろサービス作りたいな…と思ったんですが、無料プランだと3プロジェクトまでしか動かせないので注意が必要ですね。

それではまた。