🛠️

TemporalでSubscriptionのWorkflowを組む(初めてのWorkflow実行編)

2023/02/10に公開

Temporalとは

TemporalはWorkflow Engineなのですが、日本語での簡単な紹介は以下の記事で書いたことがあります。

https://kenfdev.hateblo.jp/entry/2022/01/16/165946

AWSのStep Functionsを知っている方は、それとジャンルは似たものと思ってOKかと思います(とても雑ですが)。

公式サイトは以下
https://temporal.io/

SubscriptionのWorkflowを組んで見る

Temporalにはサンプルコードがたくさんあるのですが、その中でもこの記事では「Build a subscription workflow with Temporal and TypeScript」をWalkthroughしていきたいと思います。

https://learn.temporal.io/tutorials/typescript/subscriptions/

ワークフローの概要

組みたいワークフローとしては以下のとおり

  1. ユーザーがサインアップしたらウェルカムメッセージ送信し、無料トライアル期間を TrialPeriod の間有効化する
  2. TrialPeriod が終わったら、課金処理を開始する
    • 無料トライアル期間内にユーザーがキャンセルしたらキャンセルメールを送信する
  3. 課金処理
    1. MaxBillingPeriods を超えていない限り
    2. BillingPeriodChargeAmound顧客に請求する
    3. 2が終わったら BillingPeriod 待機する
    4. 待機中に顧客がキャンセルをしたら、サブスクリプションキャンセルメールを送信する
    5. サブスクリプションが完了( MaxBillingPeriods を超えた)したら、サブスクリプション終了メールを送信する
  4. サブスクリプション購読中の任意のタイミングで顧客に関する以下の情報が取得可能とする
    • 請求した金額
    • 現在のサブスクリプション期間(手動での調整用 e.g. 返金など)

プロジェクトの作成

hello-world のサンプルをベースに作っていく。

npx @temporalio/create@latest subscription-tutorial --sample hello-world

Activityの作成

ワークフローの黒字にあたる部分が外界に対してのアクション(顧客への請求、メール送信)なので、TemporalのActivityとして実装します。

実際のロジックは組まずに console.log だけで「やったこと」にします。まずはウェルカムメッセージのメール送信とサブスクリプションについてのメール送信を作ります。

export async function sendWelcomeEmail(email: string) {
  console.log(`Sending welcome email to ${email}`);
}

export async function sendSubscriptionOverEmail(email: string) {
  console.log(`Sending subscription over email to ${email}`);
}

無料トライアルのワークフローを sleep で実装

では、上で作ったActivityを使ってワークフローを組みます。 @temporalio/workflow が用意してくれる sleep を使って、ウェルカムメッセージから trialPeriod の間待ってから、サブスクリプション完了のメールを送信します。

import { sleep, proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';

const { sendWelcomeEmail, sendSubscriptionOverEmail } = proxyActivities<typeof activities>({
  startToCloseTimeout: '1 minute',
});

export async function subscriptionWorkflow(email: string, trialPeriod: string | number): Promise<void> {
  await sendWelcomeEmail(email);
  await sleep(trialPeriod);
  await sendSubscriptionOverEmail(email);
}

このワークフローを実行してみます。そのためには呼び出し側である client.ts を以下のように書き換えます。

import { Connection, Client } from '@temporalio/client';
import { subscriptionWorkflow } from './workflows';
import { nanoid } from 'nanoid';

async function run() {
  const connection = await Connection.connect({ address: 'temporal:7233' });

  const client = new Client({ connection });

  const handle = await client.workflow.start(subscriptionWorkflow, {
    args: ['hoge@example.com', '30 seconds'],
    taskQueue: 'subscription-tutorial',
    // in practice, use a meaningful business ID, like customerId or transactionId
    workflowId: 'workflow-' + nanoid(),
  });
  console.log(`Started workflow ${handle.workflowId}`);

// ...
}

// ...
});

ポイントとしてはClientから subscriptionWorkflow を呼び出していて、引数として args には ['hoge@example.com', '30 seconds'] を渡しているところです。ちゃんとこの args は型も意識してくれるので、間違えると実装時に怒ってくれます。

Temporalで、ClientからのリクエストがWorkerに到達するイメージは以下のような感じです(たぶん)。このClientを、Node.jsだったらExpressだったり、Next.jsのサーバーサイドのコードから使うイメージです。

ワークフローの実行

では実際に実行してみます。まずはWorkerを起動しておきます。

npm run start.watch

# "start.watch": "nodemon src/worker.ts"

うまくいくとログに [INFO] Worker state changed { state: 'RUNNING' } と表示されます。Workerの準備ができたので、Clientからワークフローの実行をします。

npm run workflow
# "workflow": "ts-node src/client.ts"

すると、以下のようにClient側でログが出力されます

Started workflow workflow-ooTIVjejtBtZJv6mQcqcV

そして、Worker側では以下のログが出力されます。メール送信処理が走っています。

Sending welcome email to hoge@example.com

ここでしばらく待機(無料期間は30秒に設定しています)。すると、30秒後に以下がWorker側のログに出力されます。

Sending subscription over email to hoge@example.com

簡単ですがワークフローの実行は正常に完了していることがわかります。

ワークフローの確認(UI)

Elasticsearchが立ち上がっていると、ワークフローの確認をUIから行うことができます。

下記が特にポイントとなるイベントですね。

  • WorkflowExecutionStarted: subscriptionWorkflow
  • ActivityTaskScheduled: sendWelcomeEmail
  • TimerStarted: 30 seconds
  • ActifityTaskScheduled: sendSubscriptionOverEmail
  • WorkflowExecutionCompleted

Step Functionsみたいに視覚的にも見えるようになったらさらに良さそうですね。

おわりに

ということでワークフローの1, 2に関して実行できるところまで確認できました。ただし、2の中の「 無料トライ合う期間内にユーザーがキャンセルしたらキャンセルメールを送信する」を実現できていないのはなかなか致命的です。続きについては次回やっていきましょう。

現在の実装状況

  1. ✅ユーザーがサインアップしたらウェルカムメッセージ送信し、無料トライアル期間を TrialPeriod の間有効化する
  2. TrialPeriod が終わったら、課金処理を開始する
    • ❌無料トライアル期間内にユーザーがキャンセルしたらキャンセルメールを送信する
  3. 課金処理
    1. MaxBillingPeriods を超えていない限り
    2. BillingPeriodChargeAmound顧客に請求する
    3. 2が終わったら BillingPeriod 待機する
    4. 待機中に顧客がキャンセルをしたら、サブスクリプションキャンセルメールを送信する
    5. サブスクリプションが完了( MaxBillingPeriods を超えた)したら、サブスクリプション終了メールを送信する
  4. サブスクリプション購読中の任意のタイミングで顧客に関する以下の情報が取得可能とする
    • 請求金額
    • 現在のサブスクリプション期間(手動での調整用 e.g. 返金など)

このデモを試したい人は以下のリポジトリでVSCodeのdevcontainerを用意しているので、うまくいけばそのまま動作確認できます。
https://github.com/kenfdev/temporal-tutorial/tree/main/subscription-tutorial

続き
https://zenn.dev/kenfdev/articles/237e902d409d05

Discussion