Użyj wzorca fan-out/fan-in , aby uruchomić wiele funkcji równolegle, a następnie zagregować wyniki. Ten wzorzec jest typowym podejściem do przetwarzania równoległego w przepływach pracy bezserwerowych platformy Azure. W tym samouczku zaimplementujesz wzorzec fan-out/fan-in z Durable Functions, aby wykonać kopię zapasową zawartości witryny aplikacji w usłudze Azure Storage.
Wymagania wstępne
Model programowania V3
Model programowania V4
Durable Functions nie obsługuje Go. Aby zobaczyć przykłady Go, wybierz Durable Task SDK na górze tego artykułu.
Użyj wzorca fan-out/fan-in do przetwarzania równoległego w orkiestracji przepływu pracy:
- Rozdzielanie pracy w wielu aktywnościach uruchomionych jednocześnie.
- Konsoliduj wyniki poprzez agregację.
W tym samouczku implementujesz wzorzec fan-out/fan-in za pomocą zestawów SDK Durable Task dla platform .NET, JavaScript, Python, Java i Go (beta).
Omówienie scenariusza
W tym przykładzie pokazano przetwarzanie równoległe, przekazując wszystkie pliki w katalogu (cyklicznie) do usługi Azure Blob Storage i zliczając przekazane łączne bajty.
Pojedyncza funkcja może obsłużyć przekazywanie, ale nie jest skalowana. Jedno wykonywanie funkcji jest uruchamiane na jednej maszynie wirtualnej, więc przepływność jest ograniczona do tej maszyny wirtualnej. Niezawodność jest kolejnym problemem: jeśli proces zakończy się niepowodzeniem w połowie lub trwa dłużej niż pięć minut, tworzenie kopii zapasowej kończy się częściowo ukończonym stanem i musi zostać ponownie uruchomione.
Podejście oparte na kolejce z dwiema operacjami zwiększa przepustowość i niezawodność, ale wprowadza złożoność zarządzania stanem i koordynacją, takich jak raportowanie łącznej liczby przesłanych bajtów.
Rozszerzenie Durable Functions zapewnia równoległe przetwarzanie, niezawodność i koordynację z minimalnym nakładem, bez konieczności zarządzania kolejkami.
W tym przykładzie menedżer przepływu pracy rozdziela zadania pomiędzy wiele działań do przetwarzania równoległego, a następnie scala wyniki. Używaj wzorca fan-out/fan-in, kiedy jest to konieczne:
- Przetwarzanie partii elementów, w których każdy element może być obsługiwany niezależnie
- Dystrybucja pracy między wieloma maszynami w celu uzyskania lepszej przepływności
- Agregowanie wyników ze wszystkich operacji równoległych
Bez tego wzorca elementy są przetwarzane sekwencyjnie (ograniczanie przepływności) lub tworzenie własnej logiki kolejkowania i koordynacji (dodawanie złożoności). Zestawy SDK Durable Task obsługują równoległość i koordynację, co sprawia, że wzorzec rozgałęzienia/zbierania jest prosty do zaimplementowania.
Składniki funkcji
W tym artykule opisano funkcje w przykładowej aplikacji:
-
E2_BackupSiteContent: Funkcja orkiestratora wywołuje E2_GetFileList w celu pobrania listy plików do kopii zapasowej, a następnie wywołuje E2_CopyFileToBlob dla każdego pliku.
-
E2_GetFileList: funkcja działania zwracająca listę plików w katalogu.
-
E2_CopyFileToBlob: Funkcja aktywności, która tworzy kopię zapasową pojedynczego pliku do Azure Blob Storage.
W tym artykule opisano składniki w przykładowym kodzie:
-
ParallelProcessingOrchestration, fanOutFanInOrchestrator, fan_out_fan_in_orchestrator, FanOutFanIn_WordCount, lub fanOutFanIn: Orkiestrator, który równolegle rozprasza pracę na wiele aktywności, czeka na zakończenie wszystkich działań, a następnie scala wyniki, agregując je.
-
ProcessWorkItemActivity, , processWorkItemprocess_work_itemlub CountWords: działanie, które przetwarza pojedynczy element roboczy.
-
AggregateResultsActivity, aggregateResultslub aggregate_results: Działanie agregujące wyniki ze wszystkich operacji równoległych.
Orchestrator
Ta funkcja orkiestratora wykonuje następujące zadania:
- Przyjmuje
rootDirectory jako dane wejściowe.
- Wywołuje funkcję , aby uzyskać cykliczną listę plików w obszarze
rootDirectory.
- Wykonuje wywołania funkcji równoległych w celu przekazania każdego pliku do Azure Blob Storage.
- Czeka na ukończenie wszystkich przesyłania.
- Zwraca łączną liczbę bajtów przekazanych do Azure Blob Storage.
Poniższy kod demonstruje implementację funkcji orkiestratora:
Model izolowany
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();
}
}
Zwróć uwagę na await Task.WhenAll(tasks); wiersz. Kod nie oczekuje na poszczególne wywołania metody E2_CopyFileToBlob, więc są one uruchamiane równolegle. Kiedy orchestrator przekazuje tablicę zadań do Task.WhenAll, zwraca zadanie, które nie zostanie ukończone, dopóki wszystkie operacje kopiowania się nie zakończą. Jeśli znasz bibliotekę równoległą zadań (TPL) w .NET, ten wzorzec jest znany. Dzięki rozszerzeniu Durable Functions te zadania są uruchamiane jednocześnie na wielu maszynach wirtualnych, a kompleksowe wykonywanie jest odporne na odtwarzanie procesów.
Po tym jak orkiestrator dokona operacji Task.WhenAll, wszystkie wywołania funkcji zostaną ukończone i zwrócą wartości. Każde wywołanie E2_CopyFileToBlob zwraca liczbę przekazanych bajtów. Oblicz sumę, dodając wartości zwracane.
Model w trakcie przetwarzania
[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;
}
Uwaga / Notatka
Przykład modelu w procesie używa przestarzałych pakietów procesowych. Powyższy kod przedstawia zalecany izolowany model pracownika w .NET.
Model programowania V3
Funkcja używa standardowego function.json dla funkcji orkiestratora.
{
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
],
"disabled": false
}
Poniższy kod demonstruje implementację funkcji orkiestratora:
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;
});
Zwróć uwagę na yield context.df.Task.all(tasks); wiersz. Kod nie zwraca poszczególnych wywołań do E2_CopyFileToBlob, więc są one uruchamiane równolegle. Kiedy orchestrator przekazuje tablicę zadań do context.df.Task.all, zwraca zadanie, które nie zostanie ukończone, dopóki wszystkie operacje kopiowania się nie zakończą. Jeśli znasz Promise.all język JavaScript, ta koncepcja nie jest dla Ciebie nowa. Dzięki rozszerzeniu Durable Functions te zadania są uruchamiane jednocześnie na wielu maszynach wirtualnych, a kompleksowe wykonywanie jest odporne na odtwarzanie procesów.
Uwaga / Notatka
Chociaż zadania są koncepcyjnie podobne do obietnic języka JavaScript, funkcje orkiestratora powinny używać context.df.Task.all i context.df.Task.any zamiast Promise.all i Promise.race do zarządzania równoległością zadań.
Po zakończeniu działania orkiestratora wszystkie wywołania funkcji są ukończone context.df.Task.all i wartości są zwrócone. Każde wywołanie metody zwraca E2_CopyFileToBlob liczbę przekazanych bajtów, więc obliczenie łącznej liczby bajtów jest kwestią dodania wszystkich wartości zwracanych razem.
Model programowania V4
Poniższy kod demonstruje implementację funkcji orkiestratora:
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();
--> Zwróć uwagę na linię yield context.df.Task.all(tasks); . Wszystkie pojedyncze wywołania funkcji copyFileToBlob nie zostały udostępnione, co umożliwia ich równoległe uruchamianie. Gdy przekazujesz tę tablicę zadań do context.df.Task.all, otrzymujesz zadanie, które nie kończy się, dopóki wszystkie operacje kopiowania nie zostaną zakończone. Jeśli znasz Promise.all język JavaScript, ta koncepcja nie jest dla Ciebie nowa. Dzięki rozszerzeniu Durable Functions te zadania są uruchamiane jednocześnie na wielu maszynach wirtualnych, a kompleksowe wykonywanie jest odporne na odtwarzanie procesów.
Uwaga / Notatka
Chociaż zadania są koncepcyjnie podobne do obietnic języka JavaScript, funkcje orkiestratora powinny używać context.df.Task.all i context.df.Task.any zamiast Promise.all i Promise.race do zarządzania równoległością zadań.
Po zwróceniu sterowania z context.df.Task.all wiesz, że wszystkie wywołania funkcji zostały zakończone i zwróciły ci wartości. Każde wywołanie metody zwraca copyFileToBlob liczbę przekazanych bajtów, więc obliczenie łącznej liczby bajtów jest kwestią dodania wszystkich wartości zwracanych razem.
Funkcja używa standardowego function.json dla funkcji orkiestratora.
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
]
}
Poniższy kod demonstruje implementację funkcji orkiestratora:
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)
Zwróć uwagę na yield context.task_all(tasks); wiersz. Kod nie zwraca poszczególnych wywołań do E2_CopyFileToBlob, więc są one uruchamiane równolegle. Kiedy orchestrator przekazuje tablicę zadań do context.task_all, zwraca zadanie, które nie zostanie ukończone, dopóki wszystkie operacje kopiowania się nie zakończą. Jeśli znasz asyncio.gather język Python, ta koncepcja nie jest dla Ciebie nowa. Dzięki rozszerzeniu Durable Functions te zadania są uruchamiane jednocześnie na wielu maszynach wirtualnych, a kompleksowe wykonywanie jest odporne na odtwarzanie procesów.
Uwaga / Notatka
Choć zadania są koncepcyjnie podobne do obiektów oczekiwalnych w języku Python, funkcje orkiestratora powinny używać interfejsów API yield, context.task_all i context.task_any do zarządzania równoczesnością zadań.
Gdy orkiestrator zwróci context.task_all, wszystkie wywołania funkcji zostaną zakończone i zwrócą wartości. Każde wywołanie zwraca E2_CopyFileToBlob liczbę przesłanych bajtów, więc możesz obliczyć sumę całkowitej liczby bajtów, sumując wszystkie wartości zwracane.
Orkiestrator otrzymuje listę plików, a następnie rozdziela zadanie kopiowania każdego pliku równolegle do magazynu obiektów blob przy użyciu -NoWait. Po zakończeniu wszystkich równoległych zadań wyniki są sumowane.
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;
}
Nie oczekuje się osobno na poszczególne wywołania do E2_CopyFileToBlob, więc są wykonywane równolegle. Gdy orkestrator przekazuje listę zadań do ctx.allOf(parallelTasks), zwraca zadanie, które nie kończy się, dopóki wszystkie operacje kopiowania nie zostaną zakończone. Po zakończeniu wszystkich zadań orkestrator sumuje wyniki, aby przesłać łączną liczbę bajtów.
Durable Functions nie obsługuje Go. Aby zobaczyć przykłady Go, wybierz Durable Task SDK na górze tego artykułu.
Aranżator wykonuje następujące zadania:
- Pobiera listę elementów roboczych jako dane wejściowe.
- Rozdziela zadania, tworząc zadanie dla każdego elementu roboczego i przetwarzając je równolegle.
- Czeka na ukończenie wszystkich zadań równoległych.
- Fani biorą udział w agregacji wyników.
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;
}
}
Użyj Task.WhenAll(), aby poczekać na ukończenie wszystkich zadań równoległych. Zestaw Durable Task SDK zapewnia, że zadania mogą być uruchamiane na wielu maszynach jednocześnie, a wykonanie jest odporne na ponowne uruchomienie procesu.
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;
};
Użyj whenAll(), aby poczekać na ukończenie wszystkich zadań równoległych. Zestaw Durable Task SDK zapewnia, że zadania mogą być uruchamiane na wielu maszynach jednocześnie, a wykonanie jest odporne na ponowne uruchomienie procesu.
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
Użyj task.when_all(), aby poczekać na ukończenie wszystkich zadań równoległych. Zestaw Durable Task SDK zapewnia, że zadania mogą być uruchamiane na wielu maszynach jednocześnie, a wykonanie jest odporne na ponowne uruchomienie procesu.
Ten przykład jest dostępny dla .NET, JavaScript, Java, Python i Go.
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();
Użyj ctx.allOf(tasks).await(), aby poczekać na ukończenie wszystkich zadań równoległych. Zestaw Durable Task SDK zapewnia, że zadania mogą być uruchamiane na wielu maszynach jednocześnie, a wykonanie jest odporne na ponowne uruchomienie procesu.
Zarejestruj ten orkestrator jako FanOutFanIn. Sekcja Aktywności definiuje limit , typy i oraz aktywności i . Rejestruj aktywności jako ProcessWorkItem i AggregateResults.
package main
import (
"fmt"
"github.com/microsoft/durabletask-go/task"
)
func fanOutFanIn(ctx *task.OrchestrationContext) (any, error) {
var items []int64
if err := ctx.GetInput(&items); err != nil {
return nil, err
}
if len(items) > maxItems {
return nil, fmt.Errorf("batch contains more than %d items", maxItems)
}
pending := make([]task.Task, len(items))
for i, item := range items {
pending[i] = ctx.CallActivity("ProcessWorkItem", task.WithActivityInput(item))
}
if err := ctx.WhenAll(pending...); err != nil {
return nil, fmt.Errorf("process batch: %w", err)
}
results := make([]workResult, len(pending))
for i, work := range pending {
if err := work.Await(&results[i]); err != nil {
return nil, err
}
}
var result summary
if err := ctx.CallActivity("AggregateResults", task.WithActivityInput(results)).Await(&result); err != nil {
return nil, err
}
return result, nil
}
Zaplanuj każdą aktywność, zanim zaczekasz.
WhenAll czeka na wszystkie zadania, w tym na zadania równorzędne, gdy jedno z nich zakończy się niepowodzeniem; Await dekoduje każdy wynik. Nie wdrażaj tego wzorca za pomocą natywnych goroutines wewnątrz orkiestratora.
Działania
Funkcje działania pomocnika to zwykłe funkcje, które używają activityTrigger powiązania.
funkcja działania E2_GetFileList
Model izolowany
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;
}
}
Model w trakcie przetwarzania
[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;
}
Model programowania V3
Plik function.json pliku E2_GetFileList wygląda podobnie do następującego przykładu:
{
"bindings": [
{
"name": "rootDirectory",
"type": "activityTrigger",
"direction": "in"
}
],
"disabled": false
}
Oto implementacja:
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);
}
);
};
Funkcja używa modułu readdirp w wersji 2.x, aby rekursywnie odczytywać strukturę katalogów.
Model programowania V4
Oto implementacja getFileList funkcji działania:
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,
Funkcja używa modułu readdirp (wersja 3.x) do cyklicznego odczytywania struktury katalogów.
Plik function.json pliku E2_GetFileList wygląda podobnie do następującego przykładu:
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "rootDirectory",
"type": "activityTrigger",
"direction": "in"
}
]
}
Oto implementacja:
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 Aktywność rekurencyjnie zbiera ścieżki plików z określonego katalogu:
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());
}
}
}
}
Durable Functions nie obsługuje Go. Aby zobaczyć przykłady Go, wybierz Durable Task SDK na górze tego artykułu.
Uwaga / Notatka
Nie umieszczaj tego kodu w funkcji orkiestratora. Funkcje programu Orchestrator nie powinny wykonywać operacji we/wy, w tym dostępu do lokalnego systemu plików. Aby uzyskać więcej informacji, zobacz Ograniczenia kodu funkcji programu Orchestrator.
funkcja działania E2_CopyFileToBlob
Model izolowany
Uwaga / Notatka
Aby uruchomić przykładowy kod, zainstaluj pakiet NuGet Azure.Storage.Blobs.
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;
}
}
Model w trakcie przetwarzania
[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;
}
Uwaga / Notatka
Przykład modelu w trakcie przetwarzania wymaga pakietu NuGet Microsoft.Azure.WebJobs.Extensions.Storage i korzysta z funkcji powiązania Azure Functions, takich jak parametr Binder.
Model programowania V3
Plik function.json dla E2_CopyFileToBlob jest podobnie prosty.
{
"bindings": [
{
"name": "filePath",
"type": "activityTrigger",
"direction": "in"
},
{
"name": "out",
"type": "blob",
"path": "",
"connection": "AzureWebJobsStorage",
"direction": "out"
}
],
"disabled": false
}
Implementacja języka JavaScript używa zestawu SDK Azure Storage dla środowiska Node aby przekazać pliki do 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);
}
);
});
});
};
Model programowania V4
Implementacja JavaScript copyFileToBlob używa powiązania wyjściowego Azure Storage do przesyłania plików do 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"
Plik function.json dla E2_CopyFileToBlob jest podobnie prosty.
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "filePath",
"type": "activityTrigger",
"direction": "in"
}
]
}
Implementacja Python używa zestawu SDK Azure Storage dla Python aby przekazać pliki do 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
Aktywność E2_CopyFileToBlob odczytuje plik i przesyła go do 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;
}
Durable Functions nie obsługuje Go. Aby zobaczyć przykłady Go, wybierz Durable Task SDK na górze tego artykułu.
Implementacja ładuje plik z dysku i asynchronicznie przesyła strumieniowo zawartość do obiektu blob o tej samej nazwie w kontenerze backups . Funkcja zwraca liczbę bajtów skopiowanych do pamięci. Orkiestrator używa tej wartości do obliczenia sumy zagregowanej.
Uwaga / Notatka
W tym przykładzie operacje we/wy są przenoszone do funkcji activityTrigger. Praca może być uruchamiana na wielu maszynach i wspiera zapisywanie postępu. Jeśli proces hosta się zakończy, wiesz, które przesyłania zostały zakończone.
Działania wykonują pracę. W przeciwieństwie do orkiestratorów, działania mogą wykonywać operacje wejścia/wyjścia i niedeterministyczną logikę.
Przetwarzanie działania elementu roboczego
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;
};
W przeciwieństwie do orkiestratorów działania mogą wykonywać operacje we/wy, takie jak wywołania HTTP, zapytania bazy danych i dostęp do plików.
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}
Ten przykład jest pokazany dla .NET, JavaScript, Java, Python i Go.
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();
};
}
})
Zapisz następujące aktywności i typy w osobnym pliku w tym samym pakiecie:
package main
import (
"fmt"
"github.com/microsoft/durabletask-go/task"
)
const (
maxItems = 100
maxMagnitude = 1_000_000
)
type workResult struct {
Item int64 `json:"item"`
Result int64 `json:"result"`
}
type summary struct {
TotalItems int `json:"total_items"`
Sum int64 `json:"sum"`
Average float64 `json:"average"`
}
func square(item int64) (workResult, error) {
if item < -maxMagnitude || item > maxMagnitude {
return workResult{}, fmt.Errorf("item %d exceeds the sample's safe arithmetic range", item)
}
return workResult{Item: item, Result: item * item}, nil
}
func processWorkItem(ctx task.ActivityContext) (any, error) {
var item int64
if err := ctx.GetInput(&item); err != nil {
return nil, err
}
return square(item)
}
Aktywność agregacji wyników
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,
};
};
W przeciwieństwie do orkiestratorów działania mogą wykonywać operacje we/wy, takie jak wywołania HTTP, zapytania bazy danych i dostęp do plików.
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
}
Ten przykład jest pokazany dla .NET, JavaScript, Java, Python i Go.
W przykładzie Java orkiestrator agreguje wyniki po tym, jak ctx.allOf(tasks).await() zakończy działanie.
Zapisz tę aktywność w innym pliku w tym samym pakiecie. Weryfikuje wyniki przed ich agregacją i obsługuje pustą partię bez dzielenia przez zero.
package main
import (
"fmt"
"github.com/microsoft/durabletask-go/task"
)
func aggregateResults(ctx task.ActivityContext) (any, error) {
var results []workResult
if err := ctx.GetInput(&results); err != nil {
return nil, err
}
if len(results) > maxItems {
return nil, fmt.Errorf("batch contains more than %d items", maxItems)
}
result := summary{TotalItems: len(results)}
for _, item := range results {
expected, err := square(item.Item)
if err != nil {
return nil, err
}
if item != expected {
return nil, fmt.Errorf("incorrect square for item %d: %d", item.Item, item.Result)
}
result.Sum += item.Result
}
if result.TotalItems != 0 {
result.Average = float64(result.Sum) / float64(result.TotalItems)
}
return result, nil
}
Aby uzyskać pełną aplikację z konfiguracją połączenia, zobacz szybki start Go.
Uruchom przykład fan-out/fan-in
Uruchom aranżację na Windows, wysyłając następujące żądanie HTTP POST:
POST http://{host}/orchestrators/E2_BackupSiteContent
Content-Type: application/json
Content-Length: 20
"D:\\home\\LogFiles"
Alternatywnie, w aplikacji funkcji działającej na systemie Linux, uruchom orkiestrację, wysyłając następujące żądanie HTTP POST. Python obecnie działa w systemie Linux dla usługi App Service:
POST http://{host}/orchestrators/E2_BackupSiteContent
Content-Type: application/json
Content-Length: 20
"/home/site/wwwroot"
Uwaga / Notatka
Funkcja HttpStart oczekuje formatu JSON.
Content-Type: application/json Dołącz nagłówek i zakoduj ścieżkę katalogu jako ciąg JSON. Fragment kodu HTTP zakłada, że host.json zawiera wpis, który usuwa domyślny prefiks api/ ze wszystkich adresów URL funkcji wyzwalacza HTTP. Znajdź znaczniki dla tej konfiguracji w przykładowym pliku host.json .
To żądanie HTTP wyzwala E2_BackupSiteContent orchestratora i przekazuje ciąg znaków D:\home\LogFiles jako parametr. Odpowiedź zawiera link umożliwiający sprawdzenie stanu operacji tworzenia kopii zapasowej:
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...)
W zależności od liczby plików dziennika w aplikacji funkcji wykonanie tej operacji może potrwać kilka minut. Pobierz najnowszy stan, wysyłając zapytanie o adres URL w Location nagłówku poprzedniej odpowiedzi HTTP 202:
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"}
W tym przypadku funkcja jest nadal uruchomiona. Odpowiedź zawiera dane wejściowe zapisane w stanie orkiestratora i czas ostatniej aktualizacji. Użyj wartości nagłówka Location, aby sprawdzać pod kątem ukończenia. Gdy stan to "Ukończono", odpowiedź przypomina następujący przykład:
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"}
Odpowiedź pokazuje, że aranżacja została ukończona i przybliżony czas zakończenia. Pole output wskazuje, że orkiestracja załadowała około 450 KB dzienników.
Aby uruchomić przykład:
Uruchom emulator harmonogramu trwałych zadań do rozwoju lokalnego.
docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
Uruchom pracownika, aby zarejestrować orkiestratora i działania.
Uruchom klienta, aby zaplanować aranżację z listą elementów roboczych:
// 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}`);
Utwórz DurableTaskAzureManagedClientBuilder przy użyciu ciągu połączenia do trwałego harmonogramu zadań. Użyj scheduleNewOrchestration, aby rozpocząć orkiestrację, i użyj waitForOrchestrationCompletion, aby poczekać na zakończenie.
# 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}")
Ten przykład jest pokazany dla .NET, JavaScript, Java, Python i Go.
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));
Aby uzyskać kompletny program, który uruchamia pracownika, planuje orkiestrację fan-out/fan-in i odczytuje wynik, zobacz quickstart Go. Więcej wzorców równoległych znajdziesz w Go SDK parallel sample.
Następne kroki
W tym przykładzie przedstawiono wzorzec fan-out/fan-in. W następnym przykładzie pokazano, jak zaimplementować wzorzec monitora z trwałymi czasomierzami.