はじめに
Azure Functions はちょくちょく使っているのですが、先日開催された Microsoft のウェビナーの中で、Durable Functions について触れられていて、今更ながら気になったので触ってみました。
ということで、習うより慣れろ!の精神でやっていきます。
Durable Functions って?
詳しい説明は Microsoft の公式ドキュメントにお任せするとして、ざっくり言うと通常のFunctionだとタイムアウトしてしまうような長時間のタスクや、関数の直列・並列実行等のフロー制御、ステート管理、リトライ制御などが簡潔に実装できる拡張機能だそうです。
Durable Functions を構成する関数
Drable Functions は以下の3つの役割を持つ関数で構成されます。
1. オーケストレーター関数
アクティビティ関数の実行を制御する関数です。
オーケストレーター関数の中から更に別のオーケストレーター関数を呼び出すこともできます。
2. アクティビティ関数
実際のタスクを実行する独立した関数です。
オーケストレーター関数で呼び出しを制御されます。
3. クライアント関数
処理の起点となる関数です。
オーケストレーター関数を呼び出します。
Durable Functions を触ってみよう!
準備が必要なもの
- VSCode にAzure Functions の拡張機能をインストールしておく
- Azure Storage Explorer をインストールしておく
- Azure Cosmos DB アカウントを作っておく
- Node.js をインストールしておく(執筆時のバージョンは
v18.14.0
)
何をつくるか
pokéAPI を使ってポケモンの名前と画像を取ってきて、Azure Storageに格納する
という1つの関数でも作れそうな至ってシンプルな処理を Durable Functions を使って無駄に大げさに作ってみようと思います。
(いつの間にか GraphQL 版が出ていた!)
全体の構成
- Cosmos DB のレコードが更新されると、Change Feed をトリガーしているクライアント関数(
cosmosdbStarter
)が起動する -
cosmosdbStarter
がオーケストレーター関数(orchestrator
)を呼び出す -
orchestrator
が2つの関数を呼び出す(並列実行)- pokéAPI(GraphQL)からポケモンの名前を取得するアクティビティ関数(
getPokeName
) - ポケモンの画像データを取得するためのオーケストレーター関数(
subOrchestrator
)-
subOrchestrator
が2つの関数を呼び出して結果を返す(直列実行)- pokéAPI(REST)からポケモンの画像URLを取得するアクティビティ関数(
getPokeImageUrl
) -
getPokeImageUrl
で取得した URL から画像データを取得するアクティビティ関数(getImageData
)
- pokéAPI(REST)からポケモンの画像URLを取得するアクティビティ関数(
-
- pokéAPI(GraphQL)からポケモンの名前を取得するアクティビティ関数(
-
orchestrator
が 2つの関数(getPokeName
、subOrchestrator
) の完了を待って、Azure Blob Storage に画像ファイルを保存するアクティビティ関数(outputResult
)を呼び出す
準備
データベース作成
Cosmos DB にデータベースとコンテナを作成します。
- データベース名
durable-func-demo
- コンテナ名
-
input
(パーティションキー:/id
)
-
実装!
関数の雛形は VSCode の Azure Functions 拡張機能でサクッと作れます。
以下のチュートリアルを参考に、関数プロジェクト、関数の作成を行います。
※この記事では TypeScript で実装しています
作成する関数
以下の関数たちを作成していきます。
No. | 関数名 | テンプレート | 説明 |
---|---|---|---|
1 | orchestrator | Durable Functions orchestrator | クライアント関数から呼び出されるオーケストレーター関数 |
2 | subOrchestrator | Durable Functions orchestrator |
orchestrator から呼び出されるオーケストレーター関数 |
3 | getPokeName | Durable Functions activity | pokéAPI(GraphQL)からポケモンの名前を取得するアクティビティ関数 |
4 | getPokeImageUrl | Durable Functions activity | pokéAPI(REST)からポケモンの画像URLを取得するアクティビティ関数 |
5 | getImageData | Durable Functions activity |
getPokeImageUrl で取得した URL から画像データを取得するアクティビティ関数 |
6 | outputResult | Durable Functions activity | Azure Blob Storage に取得した画像を保存するアクティビティ関数 |
7 | cosmosdbStarter | Azure Cosmos DB trigger | Change Feed をトリガーして orchestrator を呼び出すクライアント関数 |
※最初の orchestrator 関数を作成する際に使用するストレージを聞かれるので、今回は「Azure Storage」を選択します。
Durable Functions では実行状態を保存するためにストレージが使用されます。
必要なパッケージのインストール
VSCode の拡張機能で関数プロジェクトを作成すると、最低限必要な依存関係を含んだ package.json
が自動的に作成されているはずなので、追加で必要となるパッケージをインストールします。
npm i --save pokenode-ts graphql @apollo/client
npm i -D azurite # Azure Storage のエミュレーター
設定ファイルの編集
package.json
と同じく local.settings.json
という設定ファイルが作成されているはずなので、Cosmos DB と Azure Storage の接続情報を設定します。
{
"IsEncrypted": false,
"Values": {
- "AzureWebJobsStorage": "",
+ "AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node",
+ "CosmosDBConnection": "<replace with your cosmos db connection string>"
}
}
Azure Storage のエミュレーターを使用するので、 "AzureWebJobsStorage": "UseDevelopmentStorage=true"
を指定します。
Cosmos DB の接続先は自身の環境に合わせて書き換えてください。
※Cosmos DB もエミュレーターを使用したかったのですが、自己署名証明書関連のエラーで入力バインドが使えなかったため、断念
7. cosmosdbStarter (クライアント関数)
※実際の処理の流れに沿った方がわかりやすいと思い、上記の表とは並び順を変えています
import * as df from 'durable-functions';
import { AzureFunction, Context } from '@azure/functions';
import { InputItem } from '../lib/types';
/**
* Durable Functions のクライアント関数
* Cosmos DB のChange Feedをトリガーとして起動する
*/
const cosmosDBTrigger: AzureFunction = async function (
context: Context,
items?: InputItem[]
): Promise<void> {
const item = items?.[0];
if (!item) return;
const client = df.getClient(context);
// orchestrator を呼び出し
await client.startNew(
'orchestrator',
undefined, // instanceId
item
);
};
export default cosmosDBTrigger;
function.json
{
"bindings": [
{
"type": "cosmosDBTrigger",
"name": "items",
"direction": "in",
"leaseCollectionName": "leases",
"connectionStringSetting": "CosmosDBConnection",
"databaseName": "durable-func-demo",
"collectionName": "input",
"createLeaseCollectionIfNotExists": true
},
{
"name": "starter",
"type": "orchestrationClient",
"direction": "in"
}
],
"scriptFile": "../dist/cosmosdbStarter/index.js"
}
1. orchestrator (オーケストレーター関数)
※リトライオプションとかありますが、後で触れるのでここでは一旦気にしないでください
import * as df from 'durable-functions';
import { InputItem, Result } from '../lib/types';
const orchestrator = df.orchestrator(function* (context) {
const item = context.df.getInput<InputItem>();
// 1.ポケモンの名前を取得
context.df.callSubOrchestratorWithRetry;
const getPokeNameTask = context.df.callActivityWithRetry(
'getPokeName',
retryOption,
item
);
// 2.Sub Orchestrator 呼び出し
// 2-1.ポケモンのイメージURLを取得
// 2-2.URLから画像データ取得(base64)
const getPokeImageTask = context.df.callSubOrchestrator(
'subOrchestrator',
item,
`${context.df.instanceId}:sub`
);
// 1 と 2 を並列呼び出し
yield context.df.Task.all([getPokeNameTask, getPokeImageTask]);
// 1 と 2 の結果をまとめて...
const result: Result = {
no: item.no,
name: getPokeNameTask.result as string,
imageData: getPokeImageTask.result as string,
};
// 3.Blob Storage にファイルを保存
yield context.df.callActivity('outputResult', result);
context.log('Finished!!');
});
// リトライオプション
const retryOption = new df.RetryOptions(
// 1回目の再試行間隔
5000,
// 最大試行回数
3
);
export default orchestrator;
{
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
],
"scriptFile": "../dist/orchestrator/index.js"
}
3. getPokeName (アクティビティ関数)
import { AzureFunction, Context } from '@azure/functions';
import { getPokemonSpeciesName } from '../lib/poke-api/graphql';
import { InputItem } from '../lib/types';
/** ポケモンの名前取得 */
const getPokeName: AzureFunction = async function (
context: Context,
item?: InputItem
): Promise<string | null> {
if (!item) throw Error('Invalid input.');
const pokemonSpeciesName = await getPokemonSpeciesName(item.no);
return pokemonSpeciesName?.name ?? null;
};
export default getPokeName;
{
"bindings": [
{
"name": "item",
"type": "activityTrigger",
"direction": "in"
}
],
"scriptFile": "../dist/getPokeName/index.js"
}
2. subOrchestrator (オーケストレーター関数)
import * as df from 'durable-functions';
import { InputItem } from '../lib/types';
/** 2つの関数を直列で呼び出し */
const orchestrator = df.orchestrator(function* (context) {
const item = context.df.getInput<InputItem>();
// 1.ポケモンのイメージURLを取得
const url = yield context.df.callActivity('getPokeImageUrl', item);
// 2.URLから画像データ取得(base64)
const imageData = yield context.df.callActivity('getImageData', url);
return imageData;
});
export default orchestrator;
function.json
{
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
],
"scriptFile": "../dist/subOrchestrator/index.js"
}
4. getPokeImageUrl (アクティビティ関数)
index.ts / function.json
import { AzureFunction, Context } from '@azure/functions';
import { getPokemonSprites } from '../lib/poke-api/rest';
import { InputItem } from '../lib/types';
/** ポケモンのイメージURL取得 */
const getPokeImageUrl: AzureFunction = async function (
context: Context,
item?: InputItem
): Promise<string | null> {
if (!item) throw Error('Invalid input.');
const sprites = await getPokemonSprites(item.no);
return (
sprites.other?.['official-artwork'].front_default ?? sprites.front_default
);
};
export default getPokeImageUrl;
{
"bindings": [
{
"name": "item",
"type": "activityTrigger",
"direction": "in"
}
],
"scriptFile": "../dist/getPokeImageUrl/index.js"
}
5. getImageData (アクティビティ関数)
バイナリデータ(ArrayBuffer
) のままだと呼び出し元で復元できなかった(保存されない?)ので base64 文字列に変換して返しています。
index.ts / function.json
import { AzureFunction, Context } from '@azure/functions';
import { fetchImageAsBase64String } from '../lib/utils/image';
const activityFunction: AzureFunction = async function (
context: Context,
url?: string
): Promise<string> {
if (!url) throw Error('Invalid input.');
// base64 文字列に変換して返す
const imageData = await fetchImageAsBase64String(url);
return imageData;
};
export default activityFunction;
{
"bindings": [
{
"name": "url",
"type": "activityTrigger",
"direction": "in"
}
],
"scriptFile": "../dist/getImageData/index.js"
}
6. outputResult (アクティビティ関数)
出力するファイルのパス(path
)は、入力バインドのオブジェクトを参照して動的になるようにしています。
({プロパティ名}
で入力バインドで受け取ったオブジェクトのプロパティを参照できる)
index.ts / function.json
import { AzureFunction, Context } from '@azure/functions';
import { Result } from '../lib/types';
const activityFunction: AzureFunction = async function (
context: Context,
result?: Result
): Promise<void> {
if (!result) throw Error('Invalid input.');
// base64 文字列をバイナリに変換
context.bindings.outputBlob = Buffer.from(result.imageData, 'base64');
};
export default activityFunction;
{
"bindings": [
{
"name": "result",
"type": "activityTrigger",
"direction": "in"
},
{
"name": "outputBlob",
"type": "blob",
"dataType": "binary",
"path": "output/{no}_{name}.png",
"connection": "AzureWebJobsStorage",
"direction": "out"
}
],
"scriptFile": "../dist/outputResult/index.js"
}
その他細かいロジック
その他
type定義
/** Cosmos DB の入力用スキーマ */
type InputItem = {
id: string;
no: number;
};
/** 出力結果 */
type Result = {
no: number;
name: string;
imageData: string;
};
export { InputItem, Result };
PokéAPIのGraphQLクライアント
import 'isomorphic-fetch';
import {
ApolloClient,
gql,
InMemoryCache,
QueryOptions,
} from '@apollo/client/core';
type GraphQLQueryResultType = {
pokemonSpeciesName: {
pokemon_v2_pokemonspeciesname: PokemonSpeciesName[];
};
};
type PokemonSpeciesName = {
name: string;
};
const apolloClient = new ApolloClient({
uri: 'https://beta.pokeapi.co/graphql/v1beta',
headers: {
'X-Method-Used': 'graphql',
},
cache: new InMemoryCache(),
});
const query = async <T>(options: QueryOptions): Promise<T> => {
const { data, errors, error } = await apolloClient.query<T>(options);
if (errors || errors) throw Error(JSON.stringify({ error, errors }));
return data;
};
/** ポケモンの名前を取得 */
const getPokemonSpeciesName = async (
pokemonId: number
): Promise<PokemonSpeciesName | null> => {
const data = await query<GraphQLQueryResultType['pokemonSpeciesName']>({
query: gql`
query GetPokemonSpeciesName(
$where: pokemon_v2_pokemonspeciesname_bool_exp
) {
pokemon_v2_pokemonspeciesname(where: $where) {
name
}
}
`,
variables: {
where: {
// 図鑑No.
pokemon_species_id: {
_eq: pokemonId,
},
// 言語
language_id: {
_eq: 11, // 日本語
},
},
},
});
return data.pokemon_v2_pokemonspeciesname[0] ?? null;
};
export { getPokemonSpeciesName };
PokéAPIのRESTクライアント
import { PokemonClient, PokemonSprites } from 'pokenode-ts';
const api = new PokemonClient();
/** ポケモンの画像取得 */
const getPokemonSprites = async (
pokemonId: number
): Promise<PokemonSprites> => {
const pokemon = await api.getPokemonById(pokemonId);
return pokemon.sprites;
};
export { getPokemonSprites };
URL の画像データをbase64文字列で取得
/** URLから画像データを取得し、base64文字列に変換 */
const fetchImageAsBase64String = async (url: string) => {
const response = await fetch(url);
const blob = await response.blob();
const buffer = await blob.arrayBuffer();
const base64String = Buffer.from(buffer).toString('base64');
return base64String;
};
export { fetchImageAsBase64String };
最終的なディレクトリ構成
ディレクトリ構成
.
├── cosmosdbStarter
│ ├── function.json
│ └── index.ts
├── getImageData
│ ├── function.json
│ └── index.ts
├── getPokeImageUrl
│ ├── function.json
│ └── index.ts
├── getPokeName
│ ├── function.json
│ └── index.ts
├── host.json
├── lib
│ ├── poke-api
│ │ ├── graphql.ts
│ │ └── rest.ts
│ ├── types.ts
│ └── utils
│ └── image.ts
├── local.settings.json
├── orchestrator
│ ├── function.json
│ └── index.ts
├── outputResult
│ ├── function.json
│ └── index.ts
├── package-lock.json
├── package.json
├── subOrchestrator
│ ├── function.json
│ └── index.ts
└── tsconfig.json
動かしてみる
azurite の起動
azurite (Azure Storage のエミュレーター) を起動しておきます。
# -l はワークスペースのパス(デフォルトはカレントディレクトリ)
npx azurite -l azurite
ターミナルにこんな感じで表示されていればOKです。
Azurite Blob service is starting at http://127.0.0.1:10000
Azurite Blob service is successfully listening at http://127.0.0.1:10000
Azurite Queue service is starting at http://127.0.0.1:10001
Azurite Queue service is successfully listening at http://127.0.0.1:10001
Azurite Table service is starting at http://127.0.0.1:10002
Azurite Table service is successfully listening at http://127.0.0.1:10002
実行すると azurite
というディレクトリが出来上がります。
(ディレクトリ名は何でもOK)
Functions の起動
新しいターミナルを開いて、コマンド実行。
npm start
ターミナルに作成した関数が表示されれば準備OKです。
Functions:
cosmosdbStarter: cosmosDBTrigger
getImageData: activityTrigger
getPokeImageUrl: activityTrigger
getPokeName: activityTrigger
orchestrator: orchestrationTrigger
outputResult: activityTrigger
subOrchestrator: orchestrationTrigger
Cosmos DB にデータを登録
Cosmos DB データエクスプローラーから input
コンテナにデータを登録します(手動で)
"no" に該当する図鑑No.のポケモンデータを取得します。
データを保存すると関数が動いて、コンソールにログが表示されました。
[2023-03-03T14:44:38.087Z] Executing 'Functions.cosmosdbStarter' (Reason='New changes on collection input at 2023-03-03T14:44:38.0623085Z', Id=d1634844-c256-4cb0-bca4-63559ccfcfd8)
[2023-03-03T14:44:38.270Z] Executed 'Functions.cosmosdbStarter' (Succeeded, Id=d1634844-c256-4cb0-bca4-63559ccfcfd8, Duration=200ms)
[2023-03-03T14:44:38.319Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=7b689763-93a8-4d31-ae72-af1a13aca821)
[2023-03-03T14:44:38.369Z] Executed 'Functions.orchestrator' (Succeeded, Id=7b689763-93a8-4d31-ae72-af1a13aca821, Duration=54ms)
[2023-03-03T14:44:38.399Z] Executing 'Functions.subOrchestrator' (Reason='(null)', Id=160eea64-12f3-4c29-9c2a-f90b641b27f9)
[2023-03-03T14:44:38.408Z] Executing 'Functions.getPokeName' (Reason='(null)', Id=fa2b17ee-1fd9-4292-85ea-cc0715002a7b)
[2023-03-03T14:44:38.409Z] Executed 'Functions.subOrchestrator' (Succeeded, Id=160eea64-12f3-4c29-9c2a-f90b641b27f9, Duration=10ms)
[2023-03-03T14:44:38.420Z] Executing 'Functions.getPokeImageUrl' (Reason='(null)', Id=43495be2-68f3-4713-980b-85dd80e1005a)
[2023-03-03T14:44:38.598Z] Executed 'Functions.getPokeImageUrl' (Succeeded, Id=43495be2-68f3-4713-980b-85dd80e1005a, Duration=178ms)
[2023-03-03T14:44:38.629Z] Executing 'Functions.subOrchestrator' (Reason='(null)', Id=a29d284b-f32e-4687-bd8f-7a634e9ae44f)
[2023-03-03T14:44:38.639Z] Executed 'Functions.subOrchestrator' (Succeeded, Id=a29d284b-f32e-4687-bd8f-7a634e9ae44f, Duration=11ms)
[2023-03-03T14:44:38.651Z] Executing 'Functions.getImageData' (Reason='(null)', Id=712fd023-cfa0-4064-9524-3823af7cc055)
[2023-03-03T14:44:38.996Z] Executed 'Functions.getImageData' (Succeeded, Id=712fd023-cfa0-4064-9524-3823af7cc055, Duration=344ms)
[2023-03-03T14:44:39.064Z] Executing 'Functions.subOrchestrator' (Reason='(null)', Id=70af13e3-6d33-474c-ad5a-b5e6f398501c)
[2023-03-03T14:44:39.093Z] Executed 'Functions.subOrchestrator' (Succeeded, Id=70af13e3-6d33-474c-ad5a-b5e6f398501c, Duration=29ms)
[2023-03-03T14:44:39.145Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=3e8fd6d3-e7cb-4731-9630-dc02c4bc1921)
[2023-03-03T14:44:39.173Z] Executed 'Functions.orchestrator' (Succeeded, Id=3e8fd6d3-e7cb-4731-9630-dc02c4bc1921, Duration=32ms)
[2023-03-03T14:44:40.781Z] Executed 'Functions.getPokeName' (Succeeded, Id=fa2b17ee-1fd9-4292-85ea-cc0715002a7b, Duration=2375ms)
[2023-03-03T14:44:40.806Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=5bb2d58e-961c-4b30-8642-1c268d52accc)
[2023-03-03T14:44:40.824Z] Executed 'Functions.orchestrator' (Succeeded, Id=5bb2d58e-961c-4b30-8642-1c268d52accc, Duration=19ms)
[2023-03-03T14:44:40.869Z] Executing 'Functions.outputResult' (Reason='(null)', Id=8a1d3bee-a202-4964-bea2-83d0182336e0)
[2023-03-03T14:44:41.045Z] Executed 'Functions.outputResult' (Succeeded, Id=8a1d3bee-a202-4964-bea2-83d0182336e0, Duration=178ms)
[2023-03-03T14:44:41.062Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=07bf811a-d9ef-487e-b56f-7d9d1b369c65)
[2023-03-03T14:44:41.077Z] Finished!!
[2023-03-03T14:44:41.083Z] Executed 'Functions.orchestrator' (Succeeded, Id=07bf811a-d9ef-487e-b56f-7d9d1b369c65, Duration=21ms)
ログを見るとわかりますが、オーケストレーター関数(orchestrator
、subOrchestrator
)は起動してすぐに終了を何度か繰り返しています。
アクティビティ関数を呼び出すと一旦終了して、しかるべきタイミングで再度起動して続きの処理を実行...という動きになっています。(この辺りまだ理解が足りてない・・・)
この動きを「リプレイ」というのですが、従量課金だとリプレイ毎に課金される等注意すべきポイントがあるようです。
では、Azure Storage Explorer で Blob Storage に画像が保存されているか見てみましょう。
(ストレージアカウント
> エミュレーター
> Blob Container
を選択します)
やったー!ちゃんと保存されていました。
ファイル名も <図鑑No.>_<ポケモンの名前>.png
になっていますね。
リトライ制御
orchestrator
関数のコードでちらっと出てきていますが、アクティビティ関数を呼び出す際、callActivityWithRetry
という関数を使用すると、エラー時にリトライしてくれるようになります。
(この例では最大3回試行するようになっています)
また、今回は使いませんがcallSubOrchestratorWithRetry
という関数を使用するとオーケストレーターレベルでのリトライもできるようです。
const orchestrator = df.orchestrator(function* (context) {
// 1.Function1_ポケモンの名前を取得
const getPokeNameTask = context.df.callActivityWithRetry( // ←これ
'getPokeName',
retryOption,
item
);
});
// リトライオプション
const retryOption = new df.RetryOptions(
// 1回目の再試行間隔
5000,
// 最大試行回数
3
);
このままでは特にエラーが発生しないので、無理やりエラーを起こして動きを確認してみます。
/** ポケモンの名前取得 */
const getPokeName: AzureFunction = async function (
context: Context,
item?: InputItem
): Promise<string | null> {
if (!item) throw Error('Invalid input.');
+ thirdTimeLucky();
const pokemonSpeciesName = await getPokemonSpeciesName(item.no);
return pokemonSpeciesName?.name ?? null;
};
+// 2回エラーを起こす
+let errorCount = 0;
+const thirdTimeLucky = () => {
+ if (errorCount++ < 2) throw Error(`エラー ${errorCount} 回目`);
+ errorCount = 0;
+};
今度は最初に2回エラーでリトライして、3度目の正直で成功して処理が完走しました。
[2023-03-03T14:56:13.841Z] Executing 'Functions.cosmosdbStarter' (Reason='New changes on collection input at 2023-03-03T14:56:13.8161202Z', Id=49a629d4-3a80-4c45-bbba-af0432300f53)
[2023-03-03T14:56:14.018Z] Executed 'Functions.cosmosdbStarter' (Succeeded, Id=49a629d4-3a80-4c45-bbba-af0432300f53, Duration=191ms)
[2023-03-03T14:56:14.060Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=6b1415bd-ad09-407d-b65e-7305465fb32f)
[2023-03-03T14:56:14.101Z] Executed 'Functions.orchestrator' (Succeeded, Id=6b1415bd-ad09-407d-b65e-7305465fb32f, Duration=44ms)
[2023-03-03T14:56:14.130Z] Executing 'Functions.subOrchestrator' (Reason='(null)', Id=b0c19ceb-2057-4671-9c26-5a89b6180355)
[2023-03-03T14:56:14.142Z] Executed 'Functions.subOrchestrator' (Succeeded, Id=b0c19ceb-2057-4671-9c26-5a89b6180355, Duration=12ms)
+[2023-03-03T14:56:14.146Z] Executing 'Functions.getPokeName' (Reason='(null)', Id=42d25139-7446-46c7-9dda-8f6671beb6db)
[2023-03-03T14:56:14.159Z] Executing 'Functions.getPokeImageUrl' (Reason='(null)', Id=810f3844-8e20-4030-bceb-2cf40c22b99e)
+[2023-03-03T14:56:14.172Z] Executed 'Functions.getPokeName' (Failed, Id=42d25139-7446-46c7-9dda-8f6671beb6db, Duration=27ms)
+[2023-03-03T14:56:14.172Z] System.Private.CoreLib: Exception while executing function: Functions.getPokeName. System.Private.CoreLib: Result: Failure
+[2023-03-03T14:56:14.172Z] Exception: エラー 1 回目
(・・・中略・・・)
[2023-03-03T14:56:14.230Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=d02ac3e7-513f-419a-b5e7-f6525dc3bee2)
[2023-03-03T14:56:14.254Z] Executed 'Functions.orchestrator' (Succeeded, Id=d02ac3e7-513f-419a-b5e7-f6525dc3bee2, Duration=26ms)
[2023-03-03T14:56:14.319Z] Executed 'Functions.getPokeImageUrl' (Succeeded, Id=810f3844-8e20-4030-bceb-2cf40c22b99e, Duration=160ms)
[2023-03-03T14:56:14.337Z] Executing 'Functions.subOrchestrator' (Reason='(null)', Id=81c76bd5-55f5-4531-a6a8-d2dcafbb4448)
[2023-03-03T14:56:14.342Z] Executed 'Functions.subOrchestrator' (Succeeded, Id=81c76bd5-55f5-4531-a6a8-d2dcafbb4448, Duration=4ms)
[2023-03-03T14:56:14.360Z] Executing 'Functions.getImageData' (Reason='(null)', Id=c35b70c7-7117-4f3a-9f30-6d57aa970d84)
[2023-03-03T14:56:14.683Z] Executed 'Functions.getImageData' (Succeeded, Id=c35b70c7-7117-4f3a-9f30-6d57aa970d84, Duration=323ms)
[2023-03-03T14:56:14.751Z] Executing 'Functions.subOrchestrator' (Reason='(null)', Id=01d2b9c3-7c81-465e-9a1e-40b4d9cfcf62)
[2023-03-03T14:56:14.785Z] Executed 'Functions.subOrchestrator' (Succeeded, Id=01d2b9c3-7c81-465e-9a1e-40b4d9cfcf62, Duration=35ms)
[2023-03-03T14:56:14.839Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=cbb1f0e9-5a18-4b4f-b167-dfd51b293037)
[2023-03-03T14:56:14.851Z] Executed 'Functions.orchestrator' (Succeeded, Id=cbb1f0e9-5a18-4b4f-b167-dfd51b293037, Duration=15ms)
[2023-03-03T14:56:18.499Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=10f54ef5-6d34-4c87-bcce-88ccb3cc3a40)
[2023-03-03T14:56:18.510Z] Executed 'Functions.orchestrator' (Succeeded, Id=10f54ef5-6d34-4c87-bcce-88ccb3cc3a40, Duration=11ms)
+[2023-03-03T14:56:18.521Z] Executing 'Functions.getPokeName' (Reason='(null)', Id=a18ca7fe-b8dc-4cd7-b5c5-73caf7fa2661)
+[2023-03-03T14:56:18.524Z] Executed 'Functions.getPokeName' (Failed, Id=a18ca7fe-b8dc-4cd7-b5c5-73caf7fa2661, Duration=3ms)
+[2023-03-03T14:56:18.524Z] System.Private.CoreLib: Exception while executing function: Functions.getPokeName. System.Private.CoreLib: Result: Failure
+[2023-03-03T14:56:18.524Z] Exception: エラー 2 回目
(・・・中略・・・)
[2023-03-03T14:56:18.559Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=602334a9-c468-4cce-b089-fbf7277d29e0)
[2023-03-03T14:56:18.584Z] Executed 'Functions.orchestrator' (Succeeded, Id=602334a9-c468-4cce-b089-fbf7277d29e0, Duration=25ms)
[2023-03-03T14:56:25.242Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=8b36e313-f152-417d-9d6a-7392e6342efa)
[2023-03-03T14:56:25.253Z] Executed 'Functions.orchestrator' (Succeeded, Id=8b36e313-f152-417d-9d6a-7392e6342efa, Duration=10ms)
+[2023-03-03T14:56:25.268Z] Executing 'Functions.getPokeName' (Reason='(null)', Id=54600c9a-2679-4e3c-b582-ff0fb6aeea0e)
+[2023-03-03T14:56:26.858Z] Executed 'Functions.getPokeName' (Succeeded, Id=54600c9a-2679-4e3c-b582-ff0fb6aeea0e, Duration=1590ms)
[2023-03-03T14:56:26.883Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=a9553d70-c07b-446e-a6fc-eda68e99ff1f)
[2023-03-03T14:56:26.903Z] Executed 'Functions.orchestrator' (Succeeded, Id=a9553d70-c07b-446e-a6fc-eda68e99ff1f, Duration=21ms)
[2023-03-03T14:56:26.934Z] Executing 'Functions.outputResult' (Reason='(null)', Id=ae8ea978-23ab-4f87-bb83-2a62a039ae0f)
[2023-03-03T14:56:27.121Z] Executed 'Functions.outputResult' (Succeeded, Id=ae8ea978-23ab-4f87-bb83-2a62a039ae0f, Duration=188ms)
[2023-03-03T14:56:27.141Z] Executing 'Functions.orchestrator' (Reason='(null)', Id=2c1ee745-8aab-4761-bc09-c80afd0bc1d9)
[2023-03-03T14:56:27.154Z] Finished!!
[2023-03-03T14:56:27.158Z] Executed 'Functions.orchestrator' (Succeeded, Id=2c1ee745-8aab-4761-bc09-c80afd0bc1d9, Duration=17ms)
まとめ
Durable Functions を使用することで、普通の TypeScript の関数を呼び出す感覚で別の Functions を呼び出すことができるし、実行フローの制御も簡単にできてしまうので、非常に便利だと思いました。
まだ実務では Durable Functions が必要となるような複雑なアーキテクチャは扱ったことがないですが、今のうちから色々試しながら理解を深めていきたいと思います!