動機
マイクロサービスの 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};
}
そこで runRollbackAsync
や liftAsync
のような関数が導入できるはずだ。
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 の扱いの面倒くささから実装はあきらめた。