Pola interaksi manusia

Pola interaksi manusia menjelaskan alur kerja yang menjeda dan menunggu input dari seseorang sebelum mereka melanjutkan. Pola ini berguna untuk alur kerja persetujuan, autentikasi multifaktor, dan skenario apa pun di mana seseorang merespons dalam batas waktu.

Pada tingkat tinggi, pola bekerja sebagai berikut:

  1. Orkestrator memanggil aktivitas untuk memberi tahu seseorang (mengirim kode SMS, mengirim email kepada pemberi izin, dll.).
  2. Orkestrator memulai timer tahan lama dan secara bersamaan menunggu peristiwa eksternal dari orang tersebut.
  3. Jika orang tersebut merespons sebelum timer diaktifkan, orkestrator memproses respons.
  4. Jika timer diaktifkan terlebih dahulu, orkestrator menangani batas waktu (misalnya, dengan menolak permintaan).

Dalam artikel ini:

Nota

Pivot Durable Functions dan Durable Task SDK menunjukkan pola yang sama dengan skenario yang berbeda: Durable Functions menggunakan contoh verifikasi telepon melalui SMS, sementara Durable Task SDKs menggunakan contoh alur kerja persetujuan.

Sampel ini menunjukkan cara membangun orkestrasi Durable Functions yang mencakup interaksi manusia. Contoh menerapkan sistem verifikasi telepon berbasis SMS. Ini umum dalam verifikasi nomor telepon dan alur autentikasi multifaktor (MFA).

Nota

Sampel kode lengkap tersedia untuk C#, JavaScript, dan Python. Sampel PowerShell dan Java saat ini tidak tersedia.

Nota

Model pemrograman Node.js versi 4 untuk Azure Functions umumnya tersedia. Model v4 dirancang untuk memberikan pengalaman yang lebih fleksibel dan intuitif bagi pengembang JavaScript dan TypeScript. Untuk informasi selengkapnya tentang perbedaan antara v3 dan v4, lihat panduan migrasi.

Dalam cuplikan kode berikut, JavaScript (PM4) menunjukkan model pemrograman v4, pengalaman baru.

Prasyarat

Artikel ini memperlihatkan cara menerapkan pola interaksi manusia dengan menggunakan SDK Tugas Tahan Lama. Contoh menerapkan alur kerja persetujuan di mana orkestrasi menunggu seseorang menyetujui atau menolak permintaan sebelum berlanjut.

Gambaran umum skenario interaksi manusia

Verifikasi telepon membantu mengonfirmasi bahwa orang yang menggunakan aplikasi Anda bukan spammer dan bahwa mereka mengontrol nomor telepon yang mereka berikan. Autentikasi multifaktor adalah cara umum untuk melindungi akun. Membangun verifikasi ponsel milik Anda sendiri memerlukan interaksi berbasis status dengan seseorang. Pengguna biasanya mendapatkan kode (misalnya, angka empat digit) dan harus merespons dalam jumlah waktu yang wajar.

Azure Functions standar tidak menyimpan status (seperti banyak titik akhir cloud lainnya), sehingga jenis interaksi ini memerlukan Anda untuk menyimpan status dalam database atau penyimpanan persisten lainnya. Anda juga membagi interaksi di beberapa fungsi dan mengoordinasikannya. Misalnya, satu fungsi menghasilkan kode, menyimpannya, dan mengirimkannya ke ponsel pengguna. Fungsi lain menerima respons pengguna dan memetakannya ke permintaan asli untuk memvalidasi kode. Tambahkan batas waktu untuk membantu melindungi keamanan. Alur kerja ini menjadi rumit dengan cepat.

Durable Functions mengurangi kompleksitas skenario ini. Dalam sampel ini, fungsi orkestrator mengelola interaksi stateful tanpa penyimpanan data eksternal. Karena fungsi orkestrator tahan lama, alur interaktif ini sangat dapat diandalkan.

Alur kerja persetujuan umum dalam aplikasi bisnis di mana permintaan harus ditinjau oleh manusia sebelum melanjutkan. Persyaratan alur kerja adalah:

  • Tunggu selama mungkin untuk respons manusia, atau hingga waktu habis
  • Menangani hasil persetujuan dan penolakan
  • Batas waktu dukungan saat tidak ada respons yang diterima
  • Melacak status sehingga pemohon dapat memeriksa kemajuan

SDK Durable Task menyederhanakan skenario ini dengan:

  • Peristiwa eksternal: Orkestrasi dapat menjeda dan menunggu peristiwa yang diangkat oleh sistem atau pengguna eksternal
  • Timer tahan lama: Atur batas waktu yang diaktifkan jika tidak ada respons yang diterima
  • Status kustom: Melacak dan mengekspos status alur kerja saat ini ke klien

Mengonfigurasi integrasi Twilio

Sampel ini melibatkan penggunaan layanan Twilio untuk mengirim pesan SMS ke ponsel. Azure Functions sudah memiliki dukungan untuk Twilio melalui pengikatan Twilio, dan sampel menggunakan fitur tersebut.

Hal pertama yang Anda butuhkan adalah akun Twilio. Anda dapat membuat akun gratis di https://www.twilio.com/try-twilio. Setelah memiliki akun, tambahkan tiga pengaturan aplikasi berikut ke aplikasi fungsi Anda.

Nama pengaturan aplikasi Deskripsi nilai
TwilioAccountSid SID untuk akun Twilio Anda
TwilioAuthToken Token Auth untuk akun Twilio Anda
TwilioPhoneNumber Nomor telepon yang terkait dengan akun Twilio Anda. Ini digunakan untuk mengirim pesan SMS.

Tentukan orkestrator

Fungsi orkestrator E4_SmsPhoneVerification

[FunctionName("E4_SmsPhoneVerification")]
public static async Task<bool> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    string phoneNumber = context.GetInput<string>();
    if (string.IsNullOrEmpty(phoneNumber))
    {
        throw new ArgumentNullException(
            nameof(phoneNumber),
            "A phone number input is required.");
    }

    int challengeCode = await context.CallActivityAsync<int>(
        "E4_SendSmsChallenge",
        phoneNumber);

    using (var timeoutCts = new CancellationTokenSource())
    {
        // The user has 90 seconds to respond with the code they received in the SMS message.
        DateTime expiration = context.CurrentUtcDateTime.AddSeconds(90);
        Task timeoutTask = context.CreateTimer(expiration, timeoutCts.Token);

        bool authorized = false;
        for (int retryCount = 0; retryCount <= 3; retryCount++)
        {
            Task<int> challengeResponseTask =
                context.WaitForExternalEvent<int>("SmsChallengeResponse");

            Task winner = await Task.WhenAny(challengeResponseTask, timeoutTask);
            if (winner == challengeResponseTask)
            {
                // We got back a response! Compare it to the challenge code.
                if (challengeResponseTask.Result == challengeCode)
                {
                    authorized = true;
                    break;
                }
            }
            else
            {
                // Timeout expired
                break;
            }
        }

        if (!timeoutTask.IsCompleted)
        {
            // All pending timers must be complete or canceled before the function exits.
            timeoutCts.Cancel();
        }

        return authorized;
    }
}

Nota

Awalnya mungkin tidak jelas, tetapi orkestrator ini tidak melanggar batasan orkestrasi deterministik. Ini deterministik karena CurrentUtcDateTime properti menghitung waktu kedaluwarsa timer, dan mengembalikan nilai yang sama pada setiap pemutaran ulang pada titik ini dalam kode orkestrator. Perilaku ini memastikan bahwa winner sama untuk setiap panggilan berulang ke Task.WhenAny.

Setelah dimulai, fungsi orkestrator ini melakukan hal berikut:

  1. Mendapatkan nomor telepon untuk mengirim pemberitahuan SMS.
  2. Memanggil E4_SendSmsChallenge untuk mengirim pesan SMS kepada pengguna dan mengembalikan kode tantangan empat digit yang diharapkan.
  3. Membuat timer tahan lama yang memicu 90 detik setelah waktu saat ini.
  4. Sejalan dengan timer, menunggu peristiwa SmsChallengeResponse dari pengguna.

Pengguna menerima pesan SMS dengan kode empat digit. Mereka memiliki 90 detik untuk mengirim kode yang sama ke instans orkestrator untuk menyelesaikan verifikasi. Jika mereka mengirimkan kode yang salah, mereka mendapatkan tiga percobaan lagi dalam jendela 90 detik yang sama.

Peringatan

Batalkan timer yang tidak lagi Anda butuhkan. Dalam contoh di atas, orkestrasi membatalkan timer ketika menerima respons tantangan.

Orkestrator mengirimkan permintaan persetujuan, lalu menunggu respons manusia atau batas waktu.

using Microsoft.DurableTask;
using System;
using System.Threading;
using System.Threading.Tasks;

[DurableTask(nameof(ApprovalOrchestration))]
public class ApprovalOrchestration : TaskOrchestrator<ApprovalRequestData, ApprovalResult>
{
    public override async Task<ApprovalResult> RunAsync(
        TaskOrchestrationContext context, ApprovalRequestData input)
    {
        string requestId = input.RequestId;
        double timeoutHours = input.TimeoutHours;

        // Step 1: Submit the approval request (notify approver)
        SubmissionResult submissionResult = await context.CallActivityAsync<SubmissionResult>(
            nameof(SubmitApprovalRequestActivity), input);

        // Make the status available via custom status
        context.SetCustomStatus(submissionResult);

        // Step 2: Create a durable timer for the timeout
        DateTime timeoutDeadline = context.CurrentUtcDateTime.AddHours(timeoutHours);

        using var timeoutCts = new CancellationTokenSource();
        Task timeoutTask = context.CreateTimer(timeoutDeadline, timeoutCts.Token);

        // Step 3: Wait for an external event (approval/rejection)
        Task<ApprovalResponseData> approvalTask = context.WaitForExternalEvent<ApprovalResponseData>(
            "approval_response");

        // Step 4: Wait for either the timeout or the approval response
        Task completedTask = await Task.WhenAny(approvalTask, timeoutTask);

        // Step 5: Process based on which task completed
        ApprovalResult result;

        if (completedTask == approvalTask)
        {
            // Human responded in time - cancel the timeout timer
            timeoutCts.Cancel();

            ApprovalResponseData approvalData = approvalTask.Result;

            // Process the approval
            result = await context.CallActivityAsync<ApprovalResult>(
                nameof(ProcessApprovalActivity),
                new ProcessApprovalInput
                {
                    RequestId = requestId,
                    IsApproved = approvalData.IsApproved,
                    Approver = approvalData.Approver
                });
        }
        else
        {
            // Timeout occurred
            result = new ApprovalResult
            {
                RequestId = requestId,
                Status = "Timeout",
                ProcessedAt = context.CurrentUtcDateTime.ToString("o")
            };
        }

        return result;
    }
}

Orkestrator ini melakukan tindakan berikut:

  1. Mengajukan permintaan persetujuan dengan memanggil fungsi yang memberi tahu penyetuju.
  2. Mengatur status kustom sehingga klien dapat melacak kemajuan.
  3. Membuat timer berdaya tahan untuk batas waktu kedaluwarsa.
  4. Menunggu peristiwa eksternal (approval_response) yang dipicu oleh pemberi persetujuan.
  5. Menggunakan WhenAny, when_any, atau anyOf untuk menunggu mana yang selesai terlebih dahulu: persetujuan atau batas waktu.
  6. Memproses hasil berdasarkan tugas mana yang selesai.

Peringatan

Batalkan timer yang tidak lagi Anda butuhkan. Dalam contoh C#, orkestrasi membatalkan timer batas waktu saat menerima persetujuan.

Menentukan aktivitas

Fungsi aktivitas E4_SendSmsChallenge

Fungsi E4_SendSmsChallenge menggunakan pengikatan Twilio untuk mengirim pesan SMS yang menyertakan kode empat digit kepada pengguna.

[FunctionName("E4_SendSmsChallenge")]
public static int SendSmsChallenge(
    [ActivityTrigger] string phoneNumber,
    ILogger log,
    [TwilioSms(AccountSidSetting = "TwilioAccountSid", AuthTokenSetting = "TwilioAuthToken", From = "%TwilioPhoneNumber%")]
        out CreateMessageOptions message)
{
    // Get a random number generator with a random seed (not time-based)
    var rand = new Random(Guid.NewGuid().GetHashCode());
    int challengeCode = rand.Next(10000);

    log.LogInformation($"Sending verification code {challengeCode} to {phoneNumber}.");

    message = new CreateMessageOptions(new PhoneNumber(phoneNumber));
    message.Body = $"Your verification code is {challengeCode:0000}";

    return challengeCode;
}

Nota

Untuk menjalankan sampel, instal paket NuGet Microsoft.Azure.WebJobs.Extensions.Twilio. Jangan instal paket Twilio NuGet utama karena dapat menyebabkan konflik versi dan kesalahan build.

Aktivitas mengirimkan permintaan persetujuan dan memproses respons.

Mengajukan aktivitas permintaan persetujuan

using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

[DurableTask(nameof(SubmitApprovalRequestActivity))]
public class SubmitApprovalRequestActivity : TaskActivity<ApprovalRequestData, SubmissionResult>
{
    private readonly ILogger<SubmitApprovalRequestActivity> _logger;

    public SubmitApprovalRequestActivity(ILogger<SubmitApprovalRequestActivity> logger)
    {
        _logger = logger;
    }

    public override Task<SubmissionResult> RunAsync(
        TaskActivityContext context, ApprovalRequestData input)
    {
        _logger.LogInformation(
            "Submitting approval request {RequestId} from {Requester} for {Item}",
            input.RequestId, input.Requester, input.Item);

        // In a real system, this would send an email, notification, or update a database
        var result = new SubmissionResult
        {
            RequestId = input.RequestId,
            Status = "Pending",
            SubmittedAt = DateTime.UtcNow.ToString("o"),
            ApprovalUrl = $"http://localhost:8000/api/approvals/{input.RequestId}"
        };

        return Task.FromResult(result);
    }
}

Aktivitas persetujuan proses

using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

[DurableTask(nameof(ProcessApprovalActivity))]
public class ProcessApprovalActivity : TaskActivity<ProcessApprovalInput, ApprovalResult>
{
    private readonly ILogger<ProcessApprovalActivity> _logger;

    public ProcessApprovalActivity(ILogger<ProcessApprovalActivity> logger)
    {
        _logger = logger;
    }

    public override Task<ApprovalResult> RunAsync(
        TaskActivityContext context, ProcessApprovalInput input)
    {
        string status = input.IsApproved ? "Approved" : "Rejected";
        _logger.LogInformation(
            "Processing {Status} request {RequestId} by {Approver}",
            status, input.RequestId, input.Approver);

        // In a real system, this would update a database, trigger workflows, etc.
        var result = new ApprovalResult
        {
            RequestId = input.RequestId,
            Status = status,
            ProcessedAt = DateTime.UtcNow.ToString("o"),
            Approver = input.Approver
        };

        return Task.FromResult(result);
    }
}

// Data classes
public class ApprovalRequestData
{
    public string RequestId { get; set; } = string.Empty;
    public string Requester { get; set; } = string.Empty;
    public string Item { get; set; } = string.Empty;
    public double TimeoutHours { get; set; } = 24.0;
}

public class ApprovalResponseData
{
    public bool IsApproved { get; set; }
    public string Approver { get; set; } = string.Empty;
}

public class SubmissionResult
{
    public string RequestId { get; set; } = string.Empty;
    public string Status { get; set; } = string.Empty;
    public string SubmittedAt { get; set; } = string.Empty;
    public string ApprovalUrl { get; set; } = string.Empty;
}

public class ProcessApprovalInput
{
    public string RequestId { get; set; } = string.Empty;
    public bool IsApproved { get; set; }
    public string Approver { get; set; } = string.Empty;
}

public class ApprovalResult
{
    public string RequestId { get; set; } = string.Empty;
    public string Status { get; set; } = string.Empty;
    public string ProcessedAt { get; set; } = string.Empty;
    public string? Approver { get; set; }
}

Menjalankan sampel interaksi manusia

Gunakan fungsi yang dipicu HTTP dalam sampel untuk memulai orkestrasi dengan mengirim permintaan HTTP POST berikut:

POST http://{host}/orchestrators/E4_SmsPhoneVerification
Content-Length: 14
Content-Type: application/json

"+1425XXXXXXX"
HTTP/1.1 202 Accepted
Content-Type: application/json; charset=utf-8

{"id":"741c65651d4c40cea29acdd5bb47baf1",
 "sendEventPostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}",
 "statusQueryGetUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=...&code={systemKey}",
 "terminatePostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/terminate?reason={text}&taskHub=...&code={systemKey}"}

Fungsi orkestrator menerima nomor telepon dan segera mengirim pesan SMS ke nomor tersebut dengan kode verifikasi 4 digit yang dihasilkan secara acak—misalnya, 2168. Lalu, Fungsi tersebut menunggu 90 detik untuk merespons.

Untuk membalas dengan kode, gunakan RaiseEventAsync (.NET) atau raiseEvent (JavaScript dan TypeScript) di fungsi lain, atau panggil titik akhir sendEventPostUri HTTP POST dalam respons 202. Ganti {eventName} dengan SmsChallengeResponse:

POST http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/SmsChallengeResponse?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
Content-Length: 4
Content-Type: application/json

2168

Jika Anda mengirim peristiwa sebelum timer kedaluwarsa, orkestrasi selesai, dan output bidang diatur ke true, yang menunjukkan verifikasi yang berhasil.

GET http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
HTTP/1.1 200 OK
Content-Length: 144
Content-Type: application/json; charset=utf-8

{"runtimeStatus":"Completed","input":"+1425XXXXXXX","output":true,"createdTime":"2026-04-23T19:10:49Z","lastUpdatedTime":"2026-04-23T19:12:23Z"}

Jika timer kedaluwarsa atau Anda memasukkan kode yang salah empat kali, periksa status untuk melihat output ditetapkan menjadi false, yang menunjukkan bahwa verifikasi telepon gagal.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 145

{"runtimeStatus":"Completed","input":"+1425XXXXXXX","output":false,"createdTime":"2026-04-23T19:20:49Z","lastUpdatedTime":"2026-04-23T19:22:23Z"}

Untuk menjalankan sampel:

  1. Mulai emulator Durable Task Scheduler untuk pengembangan lokal. Docker harus diinstal.

    docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
    
  2. Mulai pekerja untuk mendaftarkan orkestrator dan aktivitas.

  3. Jalankan klien untuk menjadwalkan alur kerja persetujuan dan mengirim peristiwa.

using System;
using System.Threading.Tasks;

var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build();

// Schedule the approval workflow
var input = new ApprovalRequestData
{
    RequestId = "request-" + Guid.NewGuid().ToString(),
    Requester = "john.doe@example.com",
    Item = "Vacation Request - 5 days",
    TimeoutHours = 24
};

string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
    nameof(ApprovalOrchestration), input);

Console.WriteLine($"Started approval workflow: {instanceId}");

// Simulate human approving the request
Console.WriteLine("Simulating approval...");
await Task.Delay(2000);

// Raise the approval event
var approvalResponse = new ApprovalResponseData
{
    IsApproved = true,
    Approver = "manager@example.com"
};

await client.RaiseEventAsync(instanceId, "approval_response", approvalResponse);

// Wait for completion
var result = await client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true);
Console.WriteLine($"Result: {result.ReadOutputAs<ApprovalResult>().Status}");

Langkah berikutnya

Sampel ini menunjukkan kemampuan Durable Functions tingkat lanjut, termasuk API WaitForExternalEvent dan CreateTimer. Ini menunjukkan cara menggabungkan Task.WhenAny (C#), context.df.Task.any (JavaScript dan TypeScript), atau context.task_any (Python) untuk menerapkan pola waktu habis yang andal untuk alur kerja yang menunggu orang merespons.

Sampel ini menunjukkan cara menggunakan SDK Tugas Tahan Lama untuk menerapkan alur kerja yang menunggu orang merespons, dengan batas waktu yang dapat dikonfigurasi. Konsep utama:

  • Peristiwa eksternal: Menggunakan WaitForExternalEvent untuk menunggu input
  • Timer tahan lama: Menggunakan CreateTimer untuk mengimplementasikan batas waktu
  • Tugas balapan: Menggunakan WhenAny, when_any, atau anyOf untuk menangani tugas mana pun yang selesai terlebih dahulu