ファンアウト/ファンイン パターンを使用して、複数の関数を並列で実行し、結果を集計します。 このパターンは、Azure サーバーレス ワークフローでの並列処理の一般的なアプローチです。 このチュートリアルでは、 Durable Functions を使用してファンアウト/ファンイン パターンを実装し、アプリのサイト コンテンツを Azure Storage にバックアップします。
前提条件
V3 プログラミング モデル
V4 プログラミング モデル
ワークフロー オーケストレーションで並列処理を行うには、 ファンアウト/ファンイン パターンを使用します。
- 複数のアクティビティが同時に実行されている間に、作業を分散させます。
- 結果を集計してファン インします。
このチュートリアルでは、.NET、JavaScript、Python、Java 用の Durable Task SDK を使用してファンアウト/ファンイン パターンを実装します。
シナリオの概要
このサンプルでは、ディレクトリの下にあるすべてのファイルを (再帰的に) Azure Blob Storage にアップロードし、アップロードされた合計バイト数をカウントする並列処理を示します。
1 つの関数でアップロードを処理できますが、スケーリングは行われません。 1 つの関数の実行は 1 つの仮想マシン (VM) で実行されるため、スループットはその VM に制限されます。 信頼性はもう 1 つの懸念事項です。プロセスが途中で失敗した場合、または 5 分を超える時間がかかる場合、バックアップは部分的に完了した状態で終了し、再起動する必要があります。
2 つの関数を使用したキューベースのアプローチでは、スループットと信頼性が向上しますが、アップロードされた合計バイト数の報告など、状態管理と調整の複雑さが生じます。
Durable Functions を使用すると、最小限のオーバーヘッドで並列処理、信頼性、調整が可能になり、キュー管理は必要ありません。
この例では、ワークフローオーケストレーターが、並列処理のために作業を複数のアクティビティに分散し、結果を集計して統合します。 次の必要がある場合は、ファンアウト/ファンイン パターンを使用します。
- 各項目を個別に処理できる項目のバッチを処理する
- スループットを向上させるために複数のマシンに作業を分散する
- すべての並列操作の結果を集計する
このパターンがない場合は、項目を順番に処理 (スループットの制限) するか、独自のキューと調整ロジックを構築します (複雑さが増します)。 Durable Task SDK は並列化と調整を処理するため、ファンアウト/ファンイン パターンを簡単に実装できます。
関数コンポーネント
この記事では、サンプル アプリの関数について説明します。
-
E2_BackupSiteContent: を呼び出してバックアップするファイルの一覧を取得し、各ファイルのE2_GetFileListを呼び出すE2_CopyFileToBlob。
-
E2_GetFileList:ディレクトリ内のファイルのリストを返すアクティビティ関数。
-
E2_CopyFileToBlob: 1 つのファイルをAzure Blob Storageにバックアップするアクティビティ関数。
この記事では、コード例のコンポーネントについて説明します。
-
ParallelProcessingOrchestration, fanOutFanInOrchestrator, fan_out_fan_in_orchestrator, または FanOutFanIn_WordCount: 複数のアクティビティに対して同時に作業を行い、すべてのアクティビティが完了するまで待機し、結果を集計するオーケストレーター。
-
ProcessWorkItemActivity、 processWorkItem、 process_work_item、または CountWords: 1 つの作業項目を処理するアクティビティ。
-
AggregateResultsActivity、 aggregateResults、または aggregate_results: すべての並列操作の結果を集計するアクティビティ。
Orchestrator
このオーケストレーター関数は以下の作業を実行します:
-
rootDirectoryを入力として受け取ります。
-
rootDirectory の下のファイルの再帰リストを取得する関数を呼び出します。
- 各ファイルをAzure Blob Storageにアップロードする並列関数呼び出しを行います。
- すべてのアップロードが完了するまで待機します。
- Azure Blob Storageにアップロードされた合計バイト数を返します。
次のコードは、オーケストレーター関数の実装を示しています。
分離モデル
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
namespace SampleApp;
public static class BackupSiteContent
{
[Function("E2_BackupSiteContent")]
public static async Task<long> Run(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
string rootDirectory = context.GetInput<string>()?.Trim();
if (string.IsNullOrEmpty(rootDirectory))
{
rootDirectory = Directory.GetParent(typeof(BackupSiteContent).Assembly.Location)!.FullName;
}
string[] files = await context.CallActivityAsync<string[]>("E2_GetFileList", rootDirectory);
Task<long>[] tasks = files
.Select(file => context.CallActivityAsync<long>("E2_CopyFileToBlob", file))
.ToArray();
long[] results = await Task.WhenAll(tasks);
return results.Sum();
}
}
await Task.WhenAll(tasks); 行に注目してください。 コードは、 E2_CopyFileToBlobへの個々の呼び出しを待機しないため、並列で実行されます。 オーケストレーターは、タスク配列を Task.WhenAllに渡すと、すべてのコピー操作が完了するまで完了しないタスクを返します。 .NETのタスク並列ライブラリ (TPL) に慣れている場合、このパターンは使い慣れたパターンです。 Durable Functions 拡張機能を使用すると、これらのタスクは複数の仮想マシンで同時に実行され、エンドツーエンドの実行はプロセスのリサイクルに対する回復性があります。
オーケストレーターが Task.WhenAllを待機すると、すべての関数呼び出しが完了し、値が返されます。
E2_CopyFileToBlobを呼び出すたびに、アップロードされたバイト数が返されます。 戻り値を追加して合計を計算します。
プロセス内モデル
[FunctionName("E2_BackupSiteContent")]
public static async Task<long> Run(
[OrchestrationTrigger] IDurableOrchestrationContext backupContext)
{
string rootDirectory = backupContext.GetInput<string>()?.Trim();
if (string.IsNullOrEmpty(rootDirectory))
{
rootDirectory = Directory.GetParent(typeof(BackupSiteContent).Assembly.Location).FullName;
}
string[] files = await backupContext.CallActivityAsync<string[]>(
"E2_GetFileList",
rootDirectory);
var tasks = new Task<long>[files.Length];
for (int i = 0; i < files.Length; i++)
{
tasks[i] = backupContext.CallActivityAsync<long>(
"E2_CopyFileToBlob",
files[i]);
}
await Task.WhenAll(tasks);
long totalBytes = tasks.Sum(t => t.Result);
return totalBytes;
}
注
インプロセス モデルのサンプルでは、非推奨のインプロセス パッケージが使用されています。 上記のコードは、推奨される .NET 分離ワーカー モデルを示しています。
V3 プログラミング モデル
この関数では、オーケストレーター関数の標準的な function.json が使用されます。
{
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
],
"disabled": false
}
次のコードは、オーケストレーター関数の実装を示しています。
const df = require("durable-functions");
module.exports = df.orchestrator(function* (context) {
const rootDirectory = context.df.getInput();
if (!rootDirectory) {
throw new Error("A directory path is required as an input.");
}
const files = yield context.df.callActivity("E2_GetFileList", rootDirectory);
// Backup Files and save Promises into array
const tasks = [];
for (const file of files) {
tasks.push(context.df.callActivity("E2_CopyFileToBlob", file));
}
// wait for all the Backup Files Activities to complete, sum total bytes
const results = yield context.df.Task.all(tasks);
const totalBytes = results.reduce((prev, curr) => prev + curr, 0);
// return results;
return totalBytes;
});
yield context.df.Task.all(tasks); 行に注目してください。 このコードでは、 E2_CopyFileToBlobに対する個々の呼び出しは生成されないため、並列で実行されます。 オーケストレーターは、タスク配列を context.df.Task.allに渡すと、すべてのコピー操作が完了するまで完了しないタスクを返します。 JavaScript の Promise.all に慣れている場合、この概念は初めてではありません。 Durable Functions 拡張機能を使用すると、これらのタスクは複数の仮想マシンで同時に実行され、エンドツーエンドの実行はプロセスのリサイクルに対する回復性があります。
注
タスクは概念的には JavaScript の Promise に似ていますが、タスクの並列化を管理するために、オーケストレーター関数は context.df.Task.all と context.df.Task.any の代わりに Promise.all と Promise.race を使用する必要があります。
オーケストレーターが context.df.Task.allを生成すると、すべての関数呼び出しが完了し、値が返されます。
E2_CopyFileToBlobを呼び出すたびに、アップロードされたバイト数が返されるため、合計バイト数を計算することは、すべての戻り値をまとめて追加することです。
V4 プログラミング モデル
次のコードは、オーケストレーター関数の実装を示しています。
const df = require("durable-functions");
const path = require("path");
const getFileListActivityName = "getFileList";
const copyFileToBlobActivityName = "copyFileToBlob";
const backupRootDirectorySettingName = "BACKUP_ROOT_DIRECTORY";
df.app.orchestration("backupSiteContent", function* (context) {
const rootDir = context.df.getInput();
if (typeof rootDir !== "string" || !rootDir.trim()) {
throw new Error("A directory path is required as an input.");
}
const files = yield context.df.callActivity(getFileListActivityName, rootDir);
// Backup Files and save Tasks into array
const tasks = [];
for (const file of files) {
tasks.push(context.df.callActivity(copyFileToBlobActivityName, file));
}
// wait for all the Backup Files Activities to complete, sum total bytes
const results = yield context.df.Task.all(tasks);
const totalBytes = results ? results.reduce((prev, curr) => prev + curr, 0) : 0;
// return results;
return totalBytes;
});
df.app.activity(getFileListActivityName, {
handler: async function (requestedRootDirectory, context) {
const backupRootDirectory = await getBackupRootDirectory();
――>yield context.df.Task.all(tasks); の線に注目してください。
copyFileToBlob関数への個々の呼び出しはすべて生成されませんでした。これにより、並列で実行できます。 このタスクの配列を context.df.Task.allに渡すと、 すべてのコピー操作が完了しないとタスクが完了しません。 JavaScript の Promise.all に慣れている場合、この概念は初めてではありません。 Durable Functions 拡張機能を使用すると、これらのタスクは複数の仮想マシンで同時に実行され、エンドツーエンドの実行はプロセスのリサイクルに対する回復性があります。
注
タスクは概念的には JavaScript の Promise に似ていますが、タスクの並列化を管理するために、オーケストレーター関数は context.df.Task.all と context.df.Task.any の代わりに Promise.all と Promise.race を使用する必要があります。
context.df.Task.allから降伏した後、すべての関数呼び出しが完了し、値が返されることがわかります。
copyFileToBlobを呼び出すたびに、アップロードされたバイト数が返されるため、合計バイト数を計算することは、すべての戻り値をまとめて追加することです。
この関数では、オーケストレーター関数の標準的な function.json が使用されます。
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
]
}
次のコードは、オーケストレーター関数の実装を示しています。
import azure.functions as func
import azure.durable_functions as df
def orchestrator_function(context: df.DurableOrchestrationContext):
root_directory: str = context.get_input()
if not root_directory:
raise Exception("A directory path is required as input")
files = yield context.call_activity("E2_GetFileList", root_directory)
tasks = []
for file in files:
tasks.append(context.call_activity("E2_CopyFileToBlob", file))
results = yield context.task_all(tasks)
total_bytes = sum(results)
return total_bytes
main = df.Orchestrator.create(orchestrator_function)
yield context.task_all(tasks); 行に注目してください。 このコードでは、 E2_CopyFileToBlobに対する個々の呼び出しは生成されないため、並列で実行されます。 オーケストレーターは、タスク配列を context.task_allに渡すと、すべてのコピー操作が完了するまで完了しないタスクを返します。 Python の asyncio.gather に慣れている場合、この概念は初めてではありません。 Durable Functions 拡張機能を使用すると、これらのタスクは複数の仮想マシンで同時に実行され、エンドツーエンドの実行はプロセスのリサイクルに対する回復性があります。
注
タスクは概念的には Python await-ables に似ていますが、オーケストレーター関数では、タスクの並列化を管理するために、 yield API と context.task_all API と context.task_any API を使用する必要があります。
オーケストレーターが context.task_allを返した後、すべての関数呼び出しは完了し、値を返します。 各 E2_CopyFileToBlob 呼び出しはアップロードされたバイト数を返すため、すべての返還値を合計して合計バイト数を計算できます。
オーケストレーターはファイルの一覧を取得し、その後 -NoWait を使用して各ファイルを Blob Storage に並列でコピーします。 すべての並列作業が完了した後、結果が合計されます。
param($Context)
$rootDirectory = $Context.Input
# Get all files in the directory
$files = Invoke-DurableActivity -FunctionName 'E2_GetFileList' -Input $rootDirectory
# Fan-out: schedule parallel uploads for each file
$parallelTasks = @()
foreach ($file in $files) {
$parallelTasks += Invoke-DurableActivity -FunctionName 'E2_CopyFileToBlob' -Input $file -NoWait
}
# Fan-in: wait for all uploads and sum the results
$results = Wait-DurableTask -Task $parallelTasks
$totalBytes = ($results | Measure-Object -Sum).Sum
$totalBytes
@FunctionName("E2_BackupSiteContent")
public long backupSiteContent(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
String rootDirectory = ctx.getInput(String.class);
// Get all files in the directory
List<String> files = ctx.callActivity("E2_GetFileList", rootDirectory, List.class).await();
// Fan-out: schedule parallel uploads for each file
List<Task<Long>> parallelTasks = new ArrayList<>();
for (String file : files) {
parallelTasks.add(ctx.callActivity("E2_CopyFileToBlob", file, Long.class));
}
// Fan-in: wait for all uploads and sum the results
List<Long> results = ctx.allOf(parallelTasks).await();
long totalBytes = 0;
for (Long bytes : results) {
totalBytes += bytes;
}
return totalBytes;
}
E2_CopyFileToBlobへの個別の電話は個別に待たされるわけではなく、並行して行われます。 オーケストレーターがタスクリストを ctx.allOf(parallelTasks)に渡すと、すべてのコピー操作が完了するまで完了しないタスクが返されます。 すべてのタスクが完了すると、オーケストレーターは結果を合計してアップロードされたバイト数を得ます。
オーケストレーターは以下の作業を行います。
- 作業項目の一覧を入力として受け取ります。
- 作業項目ごとにタスクを作成し、それらを並列処理する方法でファンアウトします。
- すべての並列タスクが完了するまで待機します。
- 結果を集計してファンインします。
using Microsoft.DurableTask;
using System.Collections.Generic;
using System.Threading.Tasks;
[DurableTask]
public class ParallelProcessingOrchestration : TaskOrchestrator<List<string>, Dictionary<string, int>>
{
public override async Task<Dictionary<string, int>> RunAsync(
TaskOrchestrationContext context, List<string> workItems)
{
// Step 1: Fan-out by creating a task for each work item in parallel
var processingTasks = new List<Task<Dictionary<string, int>>>();
foreach (string workItem in workItems)
{
// Create a task for each work item (fan-out)
Task<Dictionary<string, int>> task = context.CallActivityAsync<Dictionary<string, int>>(
nameof(ProcessWorkItemActivity), workItem);
processingTasks.Add(task);
}
// Step 2: Wait for all parallel tasks to complete
Dictionary<string, int>[] results = await Task.WhenAll(processingTasks);
// Step 3: Fan-in by aggregating all results
Dictionary<string, int> aggregatedResults = await context.CallActivityAsync<Dictionary<string, int>>(
nameof(AggregateResultsActivity), results);
return aggregatedResults;
}
}
Task.WhenAll()を使用して、すべての並列タスクが完了するまで待機します。 Durable Task SDK は、タスクを複数のマシンで同時に実行でき、実行が再起動を処理する回復性を確保します。
import {
OrchestrationContext,
TOrchestrator,
whenAll,
} from "@microsoft/durabletask-js";
const fanOutFanInOrchestrator: TOrchestrator = async function* (
ctx: OrchestrationContext,
workItems: string[]
): any {
// Fan-out: create a task for each work item in parallel
const tasks = workItems.map((item) => ctx.callActivity(processWorkItem, item));
// Wait for all parallel tasks to complete
const results: number[] = yield whenAll(tasks);
// Fan-in: aggregate all results
const aggregatedResult = yield ctx.callActivity(aggregateResults, results);
return aggregatedResult;
};
whenAll()を使用して、すべての並列タスクが完了するまで待機します。 Durable Task SDK は、タスクを複数のマシンで同時に実行でき、実行が再起動を処理する回復性を確保します。
from durabletask import task
def fan_out_fan_in_orchestrator(ctx: task.OrchestrationContext, work_items: list) -> dict:
"""Orchestrator demonstrating fan-out/fan-in pattern."""
# Fan-out: Create a task for each work item
parallel_tasks = []
for item in work_items:
parallel_tasks.append(ctx.call_activity(process_work_item, input=item))
# Wait for all tasks to complete
results = yield task.when_all(parallel_tasks)
# Fan-in: Aggregate all the results
final_result = yield ctx.call_activity(aggregate_results, input=results)
return final_result
task.when_all()を使用して、すべての並列タスクが完了するまで待機します。 Durable Task SDK は、タスクを複数のマシンで同時に実行でき、実行が再起動を処理する回復性を確保します。
このサンプルは、.NET、JavaScript、Java、およびPythonで使用できます。
import com.microsoft.durabletask.*;
import java.util.List;
import java.util.stream.Collectors;
DurableTaskGrpcWorker worker = DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(connectionString)
.addOrchestration(new TaskOrchestrationFactory() {
@Override
public String getName() { return "FanOutFanIn_WordCount"; }
@Override
public TaskOrchestration create() {
return ctx -> {
List<?> inputs = ctx.getInput(List.class);
// Fan-out: Create a task for each input item
List<Task<Integer>> tasks = inputs.stream()
.map(input -> ctx.callActivity("CountWords", input.toString(), Integer.class))
.collect(Collectors.toList());
// Wait for all parallel tasks to complete
List<Integer> allResults = ctx.allOf(tasks).await();
// Fan-in: Aggregate results
int totalCount = allResults.stream().mapToInt(Integer::intValue).sum();
ctx.complete(totalCount);
};
}
})
.build();
ctx.allOf(tasks).await()を使用して、すべての並列タスクが完了するまで待機します。 Durable Task SDK は、タスクを複数のマシンで同時に実行でき、実行が再起動を処理する回復性を確保します。
活動
ヘルパー アクティビティ関数は、 activityTrigger バインディングを使用する通常の関数です。
E2_GetFileList アクティビティ関数
分離モデル
using System.IO;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace SampleApp;
public static class BackupSiteContent
{
[Function("E2_GetFileList")]
public static string[] GetFileList(
[ActivityTrigger] string rootDirectory,
FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger("E2_GetFileList");
logger.LogInformation("Searching for files under '{RootDirectory}'...", rootDirectory);
string[] files = Directory.GetFiles(rootDirectory, "*", SearchOption.AllDirectories);
logger.LogInformation("Found {FileCount} file(s) under {RootDirectory}.", files.Length, rootDirectory);
return files;
}
}
プロセス内モデル
[FunctionName("E2_GetFileList")]
public static string[] GetFileList(
[ActivityTrigger] string rootDirectory,
ILogger log)
{
log.LogInformation($"Searching for files under '{rootDirectory}'...");
string[] files = Directory.GetFiles(rootDirectory, "*", SearchOption.AllDirectories);
log.LogInformation($"Found {files.Length} file(s) under {rootDirectory}.");
return files;
}
V3 プログラミング モデル
ファイルは、次の例のようになります。
{
"bindings": [
{
"name": "rootDirectory",
"type": "activityTrigger",
"direction": "in"
}
],
"disabled": false
}
実装を次に示します。
const readdirp = require("readdirp");
module.exports = function (context, rootDirectory) {
context.log(`Searching for files under '${rootDirectory}'...`);
const allFilePaths = [];
readdirp(
{ root: rootDirectory, entryType: "all" },
function (fileInfo) {
if (!fileInfo.stat.isDirectory()) {
allFilePaths.push(fileInfo.fullPath);
}
},
function (err, res) {
if (err) {
throw err;
}
context.log(`Found ${allFilePaths.length} under ${rootDirectory}.`);
context.done(null, allFilePaths);
}
);
};
この関数は、 readdirp モジュールのバージョン 2.xを使用して、ディレクトリ構造を再帰的に読み取ります。
V4 プログラミング モデル
getFileList アクティビティ関数の実装を次に示します。
const df = require("durable-functions");
const readdirp = require("readdirp");
const getFileListActivityName = "getFileList";
const rootDirectory = await resolvePathWithinRoot(
backupRootDirectory,
requestedRootDirectory
);
context.log(`Searching for files under '${rootDirectory}'...`);
const allFiles = [];
for await (const entry of readdirp(rootDirectory, { type: "files" })) {
const filePath = await resolvePathWithinRoot(rootDirectory, entry.fullPath);
allFiles.push({
backupPath: path.relative(rootDirectory, filePath).replace(/\\/g, "/"),
filePath,
rootDirectory,
この関数は、readdirp モジュール (バージョン 3.x) を使用してディレクトリ構造を再帰的に読み取ります。
ファイルは、次の例のようになります。
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "rootDirectory",
"type": "activityTrigger",
"direction": "in"
}
]
}
実装を次に示します。
import os
from os.path import dirname
from typing import List
def main(rootDirectory: str) -> List[str]:
all_file_paths = []
# We walk the file system
for path, _, files in os.walk(rootDirectory):
# We copy the code for activities and orchestrators
if "E2_" in path:
# For each file, we add their full-path to the list
for name in files:
if name == "__init__.py" or name == "function.json":
file_path = os.path.join(path, name)
all_file_paths.append(file_path)
return all_file_paths
E2_GetFileListアクティビティは指定されたディレクトリからファイルパスを再帰的に収集します:
param($rootDirectory)
Get-ChildItem -Path $rootDirectory -Recurse -File | Select-Object -ExpandProperty FullName
@FunctionName("E2_GetFileList")
public List<String> getFileList(
@DurableActivityTrigger(name = "rootDirectory") String rootDirectory) {
File root = new File(rootDirectory);
List<String> files = new ArrayList<>();
collectFiles(root, files);
return files;
}
private void collectFiles(File directory, List<String> files) {
File[] entries = directory.listFiles();
if (entries != null) {
for (File entry : entries) {
if (entry.isDirectory()) {
collectFiles(entry, files);
} else {
files.add(entry.getAbsolutePath());
}
}
}
}
注
オーケストレーター関数にこのコードを配置しないでください。 オーケストレーター関数では、ローカル ファイル システム アクセスを含め、I/O を実行しないでください。 詳細については、「オーケストレーター関数コードの制約」を参照してください。
E2_CopyFileToBlob アクティビティ関数
分離モデル
注
サンプル コードを実行するには、Azure.Storage.Blobs NuGet パッケージをインストールします。
using System;
using System.IO;
using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace SampleApp;
public static class BackupSiteContent
{
[Function("E2_CopyFileToBlob")]
public static async Task<long> CopyFileToBlob(
[ActivityTrigger] string filePath,
FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger("E2_CopyFileToBlob");
long byteCount = new FileInfo(filePath).Length;
string blobPath = filePath
.Substring(Path.GetPathRoot(filePath)!.Length)
.Replace('\\', '/');
string outputLocation = $"backups/{blobPath}";
string? connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException("AzureWebJobsStorage is not configured.");
}
BlobContainerClient containerClient = new(connectionString, "backups");
await containerClient.CreateIfNotExistsAsync();
BlobClient blobClient = containerClient.GetBlobClient(blobPath);
logger.LogInformation("Copying '{FilePath}' to '{OutputLocation}'. Total bytes = {ByteCount}.", filePath, outputLocation, byteCount);
await using Stream source = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
await blobClient.UploadAsync(source, overwrite: true);
return byteCount;
}
}
プロセス内モデル
[FunctionName("E2_CopyFileToBlob")]
public static async Task<long> CopyFileToBlob(
[ActivityTrigger] string filePath,
Binder binder,
ILogger log)
{
long byteCount = new FileInfo(filePath).Length;
// strip the drive letter prefix and convert to forward slashes
string blobPath = filePath
.Substring(Path.GetPathRoot(filePath).Length)
.Replace('\\', '/');
string outputLocation = $"backups/{blobPath}";
log.LogInformation($"Copying '{filePath}' to '{outputLocation}'. Total bytes = {byteCount}.");
// copy the file contents into a blob
using (Stream source = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (Stream destination = await binder.BindAsync<CloudBlobStream>(
new BlobAttribute(outputLocation, FileAccess.Write)))
{
await source.CopyToAsync(destination);
}
return byteCount;
}
注
インプロセス モデル サンプルでは、 Microsoft.Azure.WebJobs.Extensions.Storage NuGet パッケージが必要であり、 Binder パラメーターなどの Azure Functions バインド機能を使用します。
V3 プログラミング モデル
function.json ファイルは E2_CopyFileToBlob と同様に単純です。
{
"bindings": [
{
"name": "filePath",
"type": "activityTrigger",
"direction": "in"
},
{
"name": "out",
"type": "blob",
"path": "",
"connection": "AzureWebJobsStorage",
"direction": "out"
}
],
"disabled": false
}
JavaScript の実装では、Azure Storage SDK for Node を使用してAzure Blob Storageにファイルをアップロードします。
const fs = require("fs");
const path = require("path");
const storage = require("azure-storage");
module.exports = function (context, filePath) {
const container = "backups";
const root = path.parse(filePath).root;
const blobPath = filePath.substring(root.length).replace("\\", "/");
const outputLocation = `backups/${blobPath}`;
const blobService = storage.createBlobService();
blobService.createContainerIfNotExists(container, (error) => {
if (error) {
throw error;
}
fs.stat(filePath, function (error, stats) {
if (error) {
throw error;
}
context.log(
`Copying '${filePath}' to '${outputLocation}'. Total bytes = ${stats.size}.`
);
const readStream = fs.createReadStream(filePath);
blobService.createBlockBlobFromStream(
container,
blobPath,
readStream,
stats.size,
function (error) {
if (error) {
throw error;
}
context.done(null, stats.size);
}
);
});
});
};
V4 プログラミング モデル
copyFileToBlob の JavaScript 実装では、Azure Storage出力バインドを使用してファイルをAzure Blob Storageにアップロードします。
const df = require("durable-functions");
const fs = require("fs/promises");
const { output } = require("@azure/functions");
const copyFileToBlobActivityName = "copyFileToBlob";
const backupRootDirectorySettingName = "BACKUP_ROOT_DIRECTORY";
}
context.log(`Found ${allFiles.length} under ${rootDirectory}.`);
return allFiles;
},
});
const blobOutput = output.storageBlob({
path: "backups/{backupPath}",
connection: "StorageConnString",
});
df.app.activity(copyFileToBlobActivityName, {
extraOutputs: [blobOutput],
handler: async function (input, context) {
if (
!input ||
typeof input.backupPath !== "string" ||
typeof input.filePath !== "string" ||
typeof input.rootDirectory !== "string"
function.json ファイルは E2_CopyFileToBlob と同様に単純です。
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "filePath",
"type": "activityTrigger",
"direction": "in"
}
]
}
Python実装では、Python Azure Storage SDK を使用して、ファイルをAzure Blob Storageにアップロードします。
import os
import pathlib
from azure.storage.blob import BlobServiceClient
from azure.core.exceptions import ResourceExistsError
connect_str = os.getenv('AzureWebJobsStorage')
def main(filePath: str) -> str:
# Create the BlobServiceClient object which will be used to create a container client
blob_service_client = BlobServiceClient.from_connection_string(connect_str)
# Create a unique name for the container
container_name = "backups"
# Create the container if it does not exist
try:
blob_service_client.create_container(container_name)
except ResourceExistsError:
pass
# Create a blob client using the local file name as the name for the blob
parent_dir, fname = pathlib.Path(filePath).parts[-2:] # Get last two path components
blob_name = parent_dir + "_" + fname
blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_name)
# Count bytes in file
byte_count = os.path.getsize(filePath)
# Upload the created file
with open(filePath, "rb") as data:
blob_client.upload_blob(data)
return byte_count
E2_CopyFileToBlobアクティビティはファイルを読み取り、Azure Blob Storageにアップロードします:
param($filePath)
$storageContext = New-AzStorageContext -ConnectionString $env:AzureWebJobsStorage
$container = "backups"
# Create the container if it doesn't exist
New-AzStorageContainer -Name $container -Context $storageContext -ErrorAction SilentlyContinue
$blobName = $filePath.Substring([System.IO.Path]::GetPathRoot($filePath).Length).Replace('\', '/')
Set-AzStorageBlobContent -File $filePath -Container $container -Blob $blobName -Context $storageContext -Force
(Get-Item $filePath).Length
@FunctionName("E2_CopyFileToBlob")
public long copyFileToBlob(
@DurableActivityTrigger(name = "filePath") String filePath) throws Exception {
File file = new File(filePath);
long byteCount = file.length();
String blobPath = filePath
.substring(File.listRoots()[0].getPath().length())
.replace('\\', '/');
String connectionString = System.getenv("AzureWebJobsStorage");
BlobContainerClient containerClient = new BlobContainerClientBuilder()
.connectionString(connectionString)
.containerName("backups")
.buildClient();
containerClient.createIfNotExists();
BlobClient blobClient = containerClient.getBlobClient(blobPath);
blobClient.uploadFromFile(filePath, true);
return byteCount;
}
実装では、ディスクからファイルを読み込み、 backups コンテナー内の同じ名前の BLOB にコンテンツを非同期的にストリーミングします。 この関数は、ストレージにコピーされたバイト数を返します。 オーケストレーターはその値を使用して集計合計を計算します。
注
次の使用例は、I/O 操作を activityTrigger 関数に移動します。 この作業は複数のマシンで実行でき、進行状況のチェックポイント処理をサポートします。 ホスト プロセスが終了すると、どのアップロードが完了しているかがわかります。
アクティビティが作業を行います。 オーケストレーターとは異なり、アクティビティは I/O 操作と非決定的ロジックを実行できます。
作業項目アクティビティの処理
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
[DurableTask]
public class ProcessWorkItemActivity : TaskActivity<string, Dictionary<string, int>>
{
private readonly ILogger<ProcessWorkItemActivity> _logger;
public ProcessWorkItemActivity(ILogger<ProcessWorkItemActivity> logger)
{
_logger = logger;
}
public override Task<Dictionary<string, int>> RunAsync(TaskActivityContext context, string workItem)
{
_logger.LogInformation("Processing work item: {WorkItem}", workItem);
// Process the work item (where you do the actual work)
var result = new Dictionary<string, int>
{
{ workItem, workItem.Length }
};
return Task.FromResult(result);
}
}
import { ActivityContext } from "@microsoft/durabletask-js";
const processWorkItem = async (
_ctx: ActivityContext,
item: string
): Promise<number> => {
console.log(`Processing work item: "${item}"`);
return item.length;
};
オーケストレーターとは異なり、アクティビティは HTTP 呼び出し、データベース クエリ、ファイル アクセスなどの I/O 操作を実行できます。
from durabletask import task
def process_work_item(ctx: task.ActivityContext, item: int) -> dict:
"""Activity processing a single work item."""
# Process the work item (where you do the actual work)
result = item * item
return {"item": item, "result": result}
このサンプルは、.NET、JavaScript、Java、およびPythonについて示されています。
import java.util.StringTokenizer;
// Activity registration
.addActivity(new TaskActivityFactory() {
@Override
public String getName() { return "CountWords"; }
@Override
public TaskActivity create() {
return ctx -> {
String input = ctx.getInput(String.class);
StringTokenizer tokenizer = new StringTokenizer(input);
return tokenizer.countTokens();
};
}
})
集計結果アクティビティ
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
[DurableTask]
public class AggregateResultsActivity : TaskActivity<Dictionary<string, int>[], Dictionary<string, int>>
{
private readonly ILogger<AggregateResultsActivity> _logger;
public AggregateResultsActivity(ILogger<AggregateResultsActivity> logger)
{
_logger = logger;
}
public override Task<Dictionary<string, int>> RunAsync(
TaskActivityContext context, Dictionary<string, int>[] results)
{
_logger.LogInformation("Aggregating {Count} results", results.Length);
// Combine all results into one aggregated result
var aggregatedResult = new Dictionary<string, int>();
foreach (var result in results)
{
foreach (var kvp in result)
{
aggregatedResult[kvp.Key] = kvp.Value;
}
}
return Task.FromResult(aggregatedResult);
}
}
import { ActivityContext } from "@microsoft/durabletask-js";
const aggregateResults = async (
_ctx: ActivityContext,
results: number[]
): Promise<object> => {
const total = results.reduce((sum, val) => sum + val, 0);
return {
totalItems: results.length,
sum: total,
average: results.length > 0 ? total / results.length : 0,
};
};
オーケストレーターとは異なり、アクティビティは HTTP 呼び出し、データベース クエリ、ファイル アクセスなどの I/O 操作を実行できます。
from durabletask import task
def aggregate_results(ctx: task.ActivityContext, results: list) -> dict:
"""Activity aggregating results from multiple work items."""
sum_result = sum(item["result"] for item in results)
return {
"total_items": len(results),
"sum": sum_result,
"average": sum_result / len(results) if results else 0
}
このサンプルは、.NET、JavaScript、Java、およびPythonについて示されています。
Javaサンプルでは、オーケストレーターは、ctx.allOf(tasks).await() が返された後に結果を集計します。
ファンアウト/ファンイン サンプルを実行する
次の HTTP POST 要求を送信して、Windowsでオーケストレーションを開始します。
POST http://{host}/orchestrators/E2_BackupSiteContent
Content-Type: application/json
Content-Length: 20
"D:\\home\\LogFiles"
または、Linux 関数アプリで、次の HTTP POST 要求を送信してオーケストレーションを開始します。 現在、Pythonは Linux for App Service で実行されています。
POST http://{host}/orchestrators/E2_BackupSiteContent
Content-Type: application/json
Content-Length: 20
"/home/site/wwwroot"
注
HttpStart関数には JSON が必要です。
Content-Type: application/json ヘッダーを含め、ディレクトリ パスを JSON 文字列としてエンコードします。 HTTP スニペットは 、host.json に、すべての HTTP トリガー関数 URL から既定の api/ プレフィックスを削除するエントリがあることを前提としています。 サンプルのhost.jsonファイルで、この構成のマークアップ を 見つけます。
この HTTP 要求で E2_BackupSiteContent オーケストレーターがトリガーされ、文字列 D:\home\LogFiles がパラメーターとして渡されます。 応答には、バックアップ操作の状態を確認するためのリンクがあります。
HTTP/1.1 202 Accepted
Content-Length: 719
Content-Type: application/json; charset=utf-8
Location: http://{host}/runtime/webhooks/durabletask/instances/b4e9bdcc435d460f8dc008115ff0a8a9?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
(...trimmed...)
関数アプリのログ ファイルの数によっては、この操作が完了するまでに数分かかることがあります。 前の HTTP 202 応答の Location ヘッダーの URL に対してクエリを実行して、最新の状態を取得します。
GET http://{host}/runtime/webhooks/durabletask/instances/b4e9bdcc435d460f8dc008115ff0a8a9?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
HTTP/1.1 202 Accepted
Content-Length: 148
Content-Type: application/json; charset=utf-8
Location: http://{host}/runtime/webhooks/durabletask/instances/b4e9bdcc435d460f8dc008115ff0a8a9?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
{"runtimeStatus":"Running","input":"D:\\home\\LogFiles","output":null,"createdTime":"2019-06-29T18:50:55Z","lastUpdatedTime":"2019-06-29T18:51:16Z"}
ここでは、関数はまだ実行中です。 応答には、オーケストレーター状態で保存された入力と、最後に更新された時刻が表示されます。
Location ヘッダー値を使用して、完了を確認します。 状態が "Completed" の場合、応答は次の例のようになります。
HTTP/1.1 200 OK
Content-Length: 152
Content-Type: application/json; charset=utf-8
{"runtimeStatus":"Completed","input":"D:\\home\\LogFiles","output":452071,"createdTime":"2019-06-29T18:50:55Z","lastUpdatedTime":"2019-06-29T18:51:26Z"}
応答には、オーケストレーションが完了し、完了までのおおよその時間が示されます。
output フィールドは、オーケストレーションが約 450 KB のログをアップロードしたことを示します。
例を実行するには:
ローカル開発用の Durable Task Scheduler エミュレーターを起動します。
docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
ワーカーを起動 してオーケストレーターとアクティビティを登録します。
クライアントを実行 して、作業項目の一覧を含むオーケストレーションをスケジュールします。
// Schedule the orchestration with a list of work items
var workItems = new List<string> { "item1", "item2", "item3", "item4", "item5" };
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
nameof(ParallelProcessingOrchestration), workItems);
// Wait for completion
var result = await client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true);
Console.WriteLine($"Result: {result.ReadOutputAs<Dictionary<string, int>>().Count} items processed");
import {
DurableTaskAzureManagedClientBuilder,
} from "@microsoft/durabletask-js-azuremanaged";
const connectionString =
process.env.DURABLE_TASK_SCHEDULER_CONNECTION_STRING ||
"Endpoint=http://localhost:8080;Authentication=None;TaskHub=default";
const client = new DurableTaskAzureManagedClientBuilder()
.connectionString(connectionString)
.build();
const workItems = ["item1", "item2", "item3", "item4", "item5"];
const instanceId = await client.scheduleNewOrchestration(fanOutFanInOrchestrator, workItems);
const state = await client.waitForOrchestrationCompletion(instanceId, true, 30);
console.log(`Result: ${state?.serializedOutput}`);
Durable Task Scheduler への接続文字列を使用して、DurableTaskAzureManagedClientBuilderを作成します。
scheduleNewOrchestrationを使用してオーケストレーションを開始し、waitForOrchestrationCompletionを使用して完了を待機します。
# Schedule the orchestration with a list of work items
work_items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
instance_id = client.schedule_new_orchestration(fan_out_fan_in_orchestrator, input=work_items)
# Wait for completion
result = client.wait_for_orchestration_completion(instance_id, timeout=60)
print(f"Result: {result.serialized_output}")
このサンプルは、.NET、JavaScript、Java、およびPythonについて示されています。
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
// Schedule the orchestration with a list of strings
List<String> sentences = Arrays.asList(
"Hello, world!",
"The quick brown fox jumps over the lazy dog.",
"Always remember you are absolutely unique.");
String instanceId = client.scheduleNewOrchestrationInstance(
"FanOutFanIn_WordCount",
new NewOrchestrationInstanceOptions().setInput(sentences));
// Wait for completion
OrchestrationMetadata result = client.waitForInstanceCompletion(instanceId, Duration.ofSeconds(30), true);
System.out.println("Total word count: " + result.readOutputAs(int.class));
次のステップ
このサンプルは、ファンアウト/ファンイン パターンを示しています。 次のサンプルでは、 永続的タイマーを使用してモニター パターンを実装する方法を示します。