1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

typescript で非同期リソース作成のロールバック関数を合成する

Posted at

動機

マイクロサービスの Saga パターンのように処理が途中で失敗したときにロールバックしてそれまで作成したリソースを削除していくような関数が欲しい。

具体的には、

このような型の失敗する可能性がある非同期リソース作成関数と

// 失敗する可能性がある
async function createResource(o: {}): Promise<{resourceId: ResourceId;}>;

このような型の失敗する可能性がある非同期リソース削除関数があったとき

// 失敗する可能性がある
async function deleteResource(o: {resourceId: ResourceId;}): Promise<{}>;

リソース作成を続けて行いたいので

const resourceAId = await createResourceA({});
const resourceBId = await createResourceB({});
return {resourceAId, resourceBId};

と書きたい。

けれど、もし createResourceB が失敗したときに、作成済みの resourceAId を削除したいので、 try-catch を使って

const resourceAId = await createResourceA({});
try{
  const resourceBId = await createResourceB({});
  return {resourceAId, resourceBId};
}catch(err){
  await deleteResourceA(resourceAId);
  throw err;
}

のように書かねばならず、煩雑である。

そこでこの2つの関数を合成して

const createResourceRollbackable = createRollbackable(
  createResource,
  deleteResource,
  ({}, { resourceId }) => ({ resourceId })
);

非同期継続コールバック関数の中でエラーが起きた時に、それまでに作成したリソースを削除する関数を生成したい

const {
  resourceAId,
  resourceBId
} =  await createResourceARollbackable({}, async ({ resourceAId }) => {
  return await createResourceBRollbackable({}, async ({ resourceBId }) => {
    return {resourceAId, resourceBId};
  });
});

合成関数の実装

そのような合成関数の実装は以下のようになる

/** callback と delete_ の両方のエラーを返しうる */
export function createRollbackable<T, U, V, W>(
  create: (o: T) => Promise<U>,
  delete_: (o: V) => Promise<W>,
  composeDeleteArg: (o: T, p: U) => V
): <RET>(o: T, callback: (o: U) => Promise<RET>) => Promise<RET> {
  const stack = new Error().stack;
  return async (o, callback) => {
    const resource = await create(o);
    const delopt = composeDeleteArg(o, resource);
    try {
      const ret = await callback(resource);
      return ret;
    } catch (err) {
      console.error(
        "createWithFunc error",
        stack,
        util.inspect([o, delopt], { depth: 30 }),
        err
      );
      await delete_(delopt);
      throw err;
    }
  };
}

モナド

このパターンは明らかにモナドなので do 構文が欲しい。
JS で do 構文を実現するにはジェネレータを使えばよい。

具体的には

function*(){
  const resourceAId = yield createResourceARollbackable({});
  const resourceBId = yield createResourceBRollbackable({});
  return {resourceAId, resourceBId};
}

のように書けたらうれしいし書けるはず。

createResourceARollbackable が継続を受け取らず、 Promise とは異なるモナド値を返せばよい。
しかし Promise とは異なるモナドなのでこのコンテキストでは await できず不便である。

function*(){
  const resourceAId = yield createResourceARollbackable({});
  const resourceBId = yield createResourceBRollbackable({});
  await sleep(1000); // <- ここは Rollbackable モナドの文脈なので await できない!
  return {resourceAId, resourceBId};
}

そこで runRollbackAsyncliftAsync のような関数が導入できるはずだ。

const {resourceAId, resourceBId} = await runRollbackAsync(function*(){
  const resourceAId = yield createResourceARollbackable({});
  const resourceBId = yield createResourceBRollbackable({});
  yield liftAsync(async ()=>{
    await sleep(1000);
  });
  return {resourceAId, resourceBId};
});
await sleep(1000);
return {resourceAId, resourceBId};

しかし TypeScript の型システムの Generator の扱いの面倒くささから実装はあきらめた。

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?