🛠️

TemporalでSubscriptionのWorkflowを組む(実行中のWorkflowへのSignal送信編)

2023/02/10に公開

前回からの続きとなります。
https://zenn.dev/kenfdev/articles/60ae42736f1851

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

本記事では「無料トライアル期間内にユーザーがキャンセルしたらキャンセルメールを送信する」をどのように実現するのかを見ていきます。

Signalの活用

動作中のワークフローにデータを何かしら送りたい場合に使うのがSignalになります。
https://docs.temporal.io/workflows#signal

Signalを使うには以下2つが必要:

  • Workflow側でハンドラーを実装
  • Client側から呼び出す実装

Signalの実装

Workflowの改修

まずはWorkflow側の実装を入れます。キャンセルのメール送信用のアクティビティを作っておきます(これまた console.log )。

export async function sendCancellationEmailDuringTrialPeriod(email: string) {
  console.log(`Sending trial cancellation email to ${email}`);
}

そしてWorkflowでSignalのハンドラを作りつつ、キャンセルメール送信のActivityも呼び出すようにコードを変更。

-import { sleep, proxyActivities } from '@temporalio/workflow';
+import * as wf from '@temporalio/workflow';
 import type * as activities from './activities';
 
-const { sendWelcomeEmail, sendSubscriptionOverEmail } = proxyActivities<typeof activities>({
+// actsでまとめて変数に入れるように変更
+const acts = wf.proxyActivities<typeof activities>({
   startToCloseTimeout: '1 minute',
 });

// Signalの宣言
+export const cancelSubscription = wf.defineSignal('cancelSignal');
+
 export async function subscriptionWorkflow(email: string, trialPeriod: string | number): Promise<void> {
-  await sendWelcomeEmail(email);
-  await sleep(trialPeriod);
-  await sendSubscriptionOverEmail(email);
+  let isCanceled = false; // キャンセルをトラックするための変数
+  wf.setHandler(cancelSubscription, () => void (isCanceled = true)); // ハンドラでキャンセルの状態を更新
+
+  await acts.sendWelcomeEmail(email);
+  await wf.sleep(trialPeriod);
+  // キャンセルの状態によって条件分岐させる
+  if (isCanceled) {
+    await acts.sendCancellationEmailDuringTrialPeriod(email);
+  } else {
+    await acts.sendSubscriptionOverEmail(email);
+  }
 }

Client側からキャンセルする実装

ではClient側からキャンセルしてみます。本来これをサーバーサイドなどで実装するのですが、簡易的にnpm scriptsでこちらも実装します。

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

  const workflowId = argv[2]; // コマンドライン引数からWorkflowIDを取得

  const client = new WorkflowClient({ connection });

  const handle = await client.getHandle(workflowId); // workflowのハンドラを取得
  await handle.signal(cancelSubscription); // キャンセル用のSignalを送信
}

Workflowの実行

では、実行してみます。まずはWorkflowの実行まで前回同様に行います。

# workerの起動
npm run start.watch

# workflowの開始
npm run workflow
# Started workflow workflow-w2WoZy1WH5fvDamaNZ_sd

これで前回同様にWorkflowが開始されます。Client側のログに出力しているworkflowIdをコピーしておきます「workflow-w2WoZy1WH5fvDamaNZ_sd」。

# worker側のログ
Sending welcome email to hoge@example.com

では、30秒以内に先程作成したキャンセル用のスクリプトも実行してみます。このとき先程取得したWorkflowIDも渡します。

npm run workflow:cancel -- workflow-w2WoZy1WH5fvDamaNZ_sd
# "workflow:cancel": "ts-node src/scripts/cancelSubscription.ts"

すると、どうやらキャンセルがうまくいってそうなログが出力されます。

cancelling workflow: workflow-w2WoZy1WH5fvDamaNZ_sd

そしてしばらくするとWorker側のログにキャンセルメールの送信ログが出力されています。

Sending trial cancellation email to hoge@example.com

めでたしめでたしと見せかけて、まだイケてないところがあります。それは「キャンセルしたにもかかわらず sleep が終わるまでキャンセルメールが送信されなかった」点です。バグってますね。

ということでこれを condition を使って直していきましょう。

condition でタイムアウト制御

sleep は必ず指定した時間待機することになります。なので今回のユースケースには向いていません。そこで使えるのが condition になります。 condition は、中に書いている評価が true になるまで待機できる機能です。そして、第2引数にはタイムアウトも指定することができる優れもの。

   wf.setHandler(cancelSubscription, () => void (isCanceled = true));
 
   await acts.sendWelcomeEmail(email);
-  await wf.sleep(trialPeriod);
-  if (isCanceled) {
+  if (await wf.condition(() => isCanceled, trialPeriod)) {
     await acts.sendCancellationEmailDuringTrialPeriod(email);
   } else {
     await acts.sendSubscriptionOverEmail(email);

上記のコードで「キャンセルされるか、無料トライアル期間が終われば」次の処理に遷移します。

再実行すると、 npm run workflow:cancel した直後にちゃんとキャンセルメールが送信されるのが確認できます。

おわりに

これで「無料トライアル期間内にユーザーがキャンセルしたらキャンセルメールを送信する」が実装できました。Signalを使って実行中のWorkflowにメッセージを送信する方法がわかりました。強力ですね。本記事はここまでとして、続きはまた次回。

現在の実装状況

  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/272a627d30b503

Discussion