사용자 상호 작용 패턴은 계속하기 전에 사용자의 입력을 일시 중지하고 기다리는 워크플로를 설명합니다. 이 패턴은 승인 워크플로우, 다중 인증, 그리고 제한 시간 내에 응답하는 모든 상황에 유용합니다.
높은 수준에서 패턴은 다음과 같이 작동합니다.
- 오케스트레이터는 활동을 호출하여 담당자에게 알림을 보냅니다(SMS 코드 보내기, 승인자에게 이메일 보내기 등).
- 오케스트레이터는 지속성 타이머를 시작하고 동시에 사용자의 외부 이벤트를 기다립니다.
- 타이머가 실행되기 전에 사용자가 응답하는 경우 오케스트레이터는 응답을 처리합니다.
- 타이머가 먼저 실행되면 오케스트레이터는 시간 제한을 처리합니다(예: 요청을 거부).
문서 내용:
메모
Durable Functions 및 지속성 작업 SDK 피벗은 서로 다른 시나리오에서 동일한 패턴을 보여 줍니다. Durable Functions SMS 전화 확인 예제를 사용하는 반면 지속성 작업 SDK는 승인 워크플로 예제를 사용합니다.
이 샘플에서는 사용자 상호 작용을 포함하는 Durable Functions 오케스트레이션을 빌드하는 방법을 보여줍니다. 이 예제에서는 SMS 기반 전화 확인 시스템을 구현합니다. 전화 번호 확인 및 MFA(다단계 인증) 흐름에서 일반적입니다.
메모
전체 코드 샘플은 C#, JavaScript 및 Python 사용할 수 있습니다. PowerShell과 Java 샘플은 현재 제공되지 않습니다.
메모
Azure Functions에 대한 Node.js 프로그래밍 모델의 버전 4는 일반적으로 사용할 수 있습니다. v4 모델은 JavaScript 및 TypeScript 개발자에게 보다 유연하고 직관적인 환경을 제공하도록 설계되었습니다. v3과 v4의 차이점에 대한 자세한 내용은 마이그레이션 가이드를 참조하세요.
다음 코드 조각에서 JAVAScript(PM4)는 새로운 환경인 프로그래밍 모델 v4를 나타냅니다.
사전 요구 사항
이 문서에서는 지속성 작업 SDK를 사용하여 인간 상호 작용 패턴을 구현하는 방법을 보여 줍니다. 이 예제에서는 오케스트레이션이 요청을 승인하거나 거부할 때까지 대기하는 승인 워크플로를 구현합니다.
사용자 상호 작용 시나리오 개요
전화 확인은 앱을 사용하는 사용자가 스팸이 아니라는 것을 확인하고 사용자가 제공하는 전화 번호를 제어하는 데 도움이 됩니다. 다단계 인증은 계정을 보호하는 일반적인 방법입니다. 사용자 고유의 전화 확인을 빌드하려면 사용자와의 상태 저장 상호 작용이 필요합니다. 사용자는 일반적으로 코드(예: 4자리 숫자)를 가져오며 적절한 시간 내에 응답해야 합니다.
표준 Azure Functions는 무상태이므로 (다른 많은 클라우드 엔드포인트와 마찬가지로) 이러한 유형의 상호 작용을 수행하려면 데이터베이스 또는 다른 저장 저장소에 상태를 저장해야 합니다. 또한 상호 작용을 여러 함수 간에 분할하고 조정합니다. 예를 들어 한 함수는 코드를 생성하고 저장한 다음 사용자의 휴대폰으로 보냅니다. 다른 함수는 사용자의 응답을 수신하고 원래 요청에 매핑하여 코드의 유효성을 검사합니다. 보안을 보호하는 데 도움이 되는 시간 제한을 추가합니다. 이 워크플로는 빠르게 복잡해집니다.
Durable Functions 이 시나리오의 복잡성을 줄입니다. 이 샘플에서 오케스트레이터 함수는 외부 데이터 저장소 없이 상태 저장 상호 작용을 관리합니다. 오케스트레이터 함수는 내구성이 뛰어나므로 이러한 대화형 흐름은 매우 안정적입니다.
승인 워크플로는 계속하기 전에 사람이 요청을 검토해야 하는 비즈니스 애플리케이션에서 일반적입니다. 워크플로 요구 사항은 다음과 같습니다.
- 인간의 응답을 위해 무기한 대기하거나 시간 제한까지 기다립니다.
-
승인 및 거부 결과 모두 처리
- 응답이 수신되지 않은 경우 지원 시간 제한
- 요청자가 진행률을 확인할 수 있도록 상태 추적
지속성 작업 SDK는 다음을 사용하여 이 시나리오를 간소화합니다.
-
외부 이벤트: 오케스트레이션은 외부 시스템 또는 사용자가 발생한 이벤트를 일시 중지하고 기다릴 수 있습니다.
-
지속형 타이머: 응답을 받지 못한 경우 발생하는 시간 제한을 설정합니다.
-
사용자 지정 상태: 클라이언트에 현재 워크플로 상태 추적 및 노출
이 샘플에서는 Twilio 서비스를 사용하여 SMS 메시지를 휴대폰으로 보냅니다. Azure Functions 이미 Twilio 바인딩 통해 Twilio를 지원하고 있으며 샘플은 해당 기능을 사용합니다.
가장 먼저 필요한 것은 Twilio 계정입니다.
https://www.twilio.com/try-twilio에서 무료로 만들 수 있습니다. 계정이 있으면 다음 세 가지 앱 설정을 함수 앱에 추가합니다.
| 앱 설정 이름 |
값 설명 |
|
TwilioAccountSid |
Twilio 계정의 SID |
|
TwilioAuthToken |
Twilio 계정의 인증 토큰 |
|
TwilioPhoneNumber |
Twilio 계정과 연결되는 전화 번호이며, SMS 메시지를 보내는 데 사용됩니다. |
오케스트레이터 정의
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;
}
}
메모
처음에는 명확하지 않을 수도 있지만 이 오케스트레이터는 결정적 오케스트레이션 제약 조건을 위반하지 않습니다. 속성이 타이머 만료 시간을 계산하고 오케스트레이터 코드의 이 시점에서 재생할 때마다 동일한 값을 반환하기 때문에 CurrentUtcDateTime 결정적입니다. 반복해서 winner를 호출할 때마다 Task.WhenAny가 동일하게 유지됩니다.
V3 프로그래밍 모델
E4_SmsPhoneVerification 함수는 오케스트레이터 함수에 표준 function.json을 사용합니다.
{
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
],
"disabled": false
}
함수를 구현하는 코드는 다음과 같습니다.
const df = require("durable-functions");
const moment = require("moment");
module.exports = df.orchestrator(function* (context) {
const phoneNumber = context.df.getInput();
if (!phoneNumber) {
throw "A phone number input is required.";
}
const challengeCode = yield context.df.callActivity("E4_SendSmsChallenge", phoneNumber);
// The user has 90 seconds to respond with the code they received in the SMS message.
const expiration = moment.utc(context.df.currentUtcDateTime).add(90, "s");
const timeoutTask = context.df.createTimer(expiration.toDate());
let authorized = false;
for (let i = 0; i <= 3; i++) {
const challengeResponseTask = context.df.waitForExternalEvent("SmsChallengeResponse");
const winner = yield context.df.Task.any([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.
timeoutTask.cancel();
}
return authorized;
});
메모
처음에는 명확하지 않을 수도 있지만 이 오케스트레이터는 결정적 오케스트레이션 제약 조건을 위반하지 않습니다. 속성이 타이머 만료 시간을 계산하고 오케스트레이터 코드의 이 시점에서 재생할 때마다 동일한 값을 반환하기 때문에 currentUtcDateTime 결정적입니다. 반복해서 winner를 호출할 때마다 context.df.Task.any가 동일하게 유지됩니다.
V4 프로그래밍 모델
오케스트레이션 함수를 구현하는 smsPhoneVerification 코드는 다음과 같습니다.
const df = require("durable-functions");
const { DateTime } = require("luxon");
const sendSmsChallengeActivityName = "sendSmsChallenge";
df.app.orchestration("smsPhoneVerification", function* (context) {
const phoneNumber = context.df.getInput();
if (!phoneNumber) {
throw new Error("A phone number input is required.");
}
const challengeCode = yield context.df.callActivity(sendSmsChallengeActivityName, phoneNumber);
// The user has 90 seconds to respond with the code they received in the SMS message.
const expiration = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ seconds: 90 });
const timeoutTask = context.df.createTimer(expiration.toJSDate());
let authorized = false;
for (let i = 0; i <= 3; i++) {
const challengeResponseTask = context.df.waitForExternalEvent("SmsChallengeResponse");
const winner = yield context.df.Task.any([challengeResponseTask, timeoutTask]);
if (winner === timeoutTask) {
// Timeout expired
break;
}
// We got back a response! Compare it to the challenge code.
if (challengeResponseTask.result === challengeCode) {
authorized = true;
break;
}
}
if (!timeoutTask.isCompleted) {
// All pending timers must be complete or canceled before the function exits.
timeoutTask.cancel();
}
return authorized;
});
E4_SmsPhoneVerification 함수는 오케스트레이터 함수에 표준 function.json을 사용합니다.
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
]
}
함수를 구현하는 코드는 다음과 같습니다.
import azure.durable_functions as df
from datetime import timedelta
def is_valid_phone_number(phone_number: str):
has_area_code = phone_number[0] == "+"
is_positive_num = phone_number[1:].isdigit()
return has_area_code and is_positive_num
def orchestrator_function(context: df.DurableOrchestrationContext):
phone_number = context.get_input()
if (not phone_number) or (not is_valid_phone_number(phone_number)):
msg = "Please provide a phone number beginning with an international dialing prefix"+\
"(+) followed by the country code, and then rest of the phone number. Example:"\
"'+1425XXXXXXX'"
raise Exception(msg)
challenge_code = yield context.call_activity("SendSMSChallenge", phone_number)
expiration = context.current_utc_datetime + timedelta(seconds=180)
timeout_task = context.create_timer(expiration)
authorized = False
for _ in range(3):
challenge_response_task = context.wait_for_external_event("SmsChallengeResponse")
winner = yield context.task_any([challenge_response_task, timeout_task])
if (winner == challenge_response_task):
# We got back a response! Compare it to the challenge code
if (challenge_response_task.result == challenge_code):
authorized = True
break
else:
# Timeout expired
break
if not timeout_task.is_completed:
# All pending timers must be complete or canceled before the function exits.
timeout_task.cancel()
return authorized
main = df.Orchestrator.create(orchestrator_function)
메모
처음에는 명확하지 않을 수도 있지만 이 오케스트레이터는 결정적 오케스트레이션 제약 조건을 위반하지 않습니다. 결정론적인 이유는 currentUtcDateTime 속성이 타이머의 만료 시각을 계산하고 오케스트레이터 코드의 이 지점에서 리플레이할 때마다 동일한 값을 반환하기 때문입니다. 이 동작은 반복되는 모든 호출 context.df.Task.any 이 동일한 winner로 이어지도록 보장합니다.
오케스트레이터는 SMS 챌린지 코드를 보내고 사용자의 응답을 기다립니다. 90초 타임아웃 시간 내에 최대 세 번의 시도를 허용합니다.
param($Context)
$phoneNumber = $Context.Input
# Step 1: Send the SMS challenge code
$challengeCode = Invoke-DurableActivity -FunctionName 'E4_SendSmsChallenge' -Input $phoneNumber
# Step 2: Wait for the user to respond with the code
# Allow up to three attempts within a 90-second timeout window
$authorized = $false
$timeoutTask = Start-DurableTimer -Duration (New-TimeSpan -Seconds 90) -NoWait
for ($retryCount = 0; $retryCount -lt 3; $retryCount++) {
$challengeResponseTask = Start-DurableExternalEventListener -EventName 'SmsChallengeResponse' -NoWait
$winner = Wait-DurableTask -Task @($challengeResponseTask, $timeoutTask) -Any
if ($winner -eq $timeoutTask) {
break
}
$response = Get-DurableTaskResult -Task $challengeResponseTask
if ($response -eq $challengeCode) {
$authorized = $true
break
}
}
# Cancel the timeout timer if the user was verified before it fired
if ($authorized) {
Stop-DurableTimerTask -Task $timeoutTask
}
$authorized
@FunctionName("E4_SmsPhoneVerification")
public boolean smsPhoneVerification(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
String phoneNumber = ctx.getInput(String.class);
// Step 1: Send the SMS challenge code
int challengeCode = ctx.callActivity(
"E4_SendSmsChallenge", phoneNumber, Integer.class).await();
// Step 2: Create a 90-second timeout timer
Duration timeout = Duration.ofSeconds(90);
// Step 3: Wait for the user to respond with the code
// Allow up to three attempts within the timeout window
boolean authorized = false;
Task<Void> timeoutTask = ctx.createTimer(timeout);
for (int retryCount = 0; retryCount < 3; retryCount++) {
Task<String> challengeResponseTask = ctx.waitForExternalEvent(
"SmsChallengeResponse", String.class);
Task<?> winner = ctx.anyOf(challengeResponseTask, timeoutTask).await();
if (winner == challengeResponseTask) {
String response = challengeResponseTask.await();
if (Integer.parseInt(response) == challengeCode) {
authorized = true;
break;
}
} else {
// Timeout expired
break;
}
}
return authorized;
}
메모
이 오케스트레이터는 ctx.createTimer와 ctx.waitForExternalEvent가 재생할 때마다 동일한 결과를 생성하므로 결정론적입니다.
이 오케스트레이터 함수가 시작되면 다음 단계를 수행합니다:
- SMS 알림을 보낼 전화 번호를 가져옵니다.
-
E4_SendSmsChallenge 호출하여 사용자에게 SMS 메시지를 보내고 예상되는 4자리 챌린지 코드를 반환합니다.
- 현재 시간 이후 90초를 트리거하는 지속성 타이머를 만듭니다.
- 타이머와 병행하여 사용자로부터 SmsChallengeResponse 이벤트를 기다립니다.
사용자는 4자리 코드가 있는 SMS 메시지를 받습니다. 확인을 완료하기 위해 동일한 코드를 오케스트레이터 인스턴스로 보내는 데 90초가 걸립니다. 잘못된 코드를 제출하면 동일한 90초 창 내에서 세 번 더 시도합니다.
경고
더 이상 필요하지 않은 타이머를 취소합니다. 앞서 언급한 예에서는, 오케스트레이션이 챌린지 응답을 수락하면 타이머를 취소합니다.
오케스트레이터는 승인 요청을 제출한 다음, 사용자 응답 또는 시간 제한을 기다립니다.
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;
}
}
import {
OrchestrationContext,
TOrchestrator,
whenAny,
} from "@microsoft/durabletask-js";
const approvalOrchestrator: TOrchestrator = async function* (
ctx: OrchestrationContext,
amount: number
): any {
// Step 1: Submit the request
const requestId: string = yield ctx.callActivity(submitRequest, { amount });
ctx.setCustomStatus({ stage: "Awaiting approval", requestId });
// Step 2: Race external event vs timer
const approvalEvent = ctx.waitForExternalEvent<{ approved: boolean }>(
"approval"
);
const timeout = ctx.createTimer(5); // 5-second timeout
const winner = yield whenAny([approvalEvent, timeout]);
let result: string;
if (winner === approvalEvent) {
// Human responded in time
const decision = approvalEvent.getResult();
ctx.setCustomStatus({
stage: "Processing",
requestId,
approved: decision.approved,
});
result = yield ctx.callActivity(processApproval, {
requestId,
approved: decision.approved,
});
} else {
// Timer fired first — timed out
ctx.setCustomStatus({ stage: "Timed out", requestId });
result = yield ctx.callActivity(notifyTimeout, requestId);
}
ctx.setCustomStatus({ stage: "Completed", requestId });
return result;
};
import datetime
from durabletask import task
def human_interaction_orchestrator(ctx: task.OrchestrationContext, input_data: dict) -> dict:
"""
Orchestrator that demonstrates the human interaction pattern.
Submits an approval request, then waits for a human to approve or reject.
"""
request_id = input_data.get("request_id")
timeout_hours = input_data.get("timeout_hours", 24)
# Step 1: Submit the approval request (notify approver)
request_data = {
"request_id": request_id,
"requester": input_data.get("requester"),
"item": input_data.get("item")
}
submission_result = yield ctx.call_activity("submit_approval_request", input=request_data)
# Make the status available via custom status
ctx.set_custom_status(submission_result)
# Step 2: Create a durable timer for the timeout
timeout_deadline = ctx.current_utc_datetime + datetime.timedelta(hours=timeout_hours)
timeout_task = ctx.create_timer(timeout_deadline)
# Step 3: Wait for an external event (approval/rejection)
approval_task = ctx.wait_for_external_event("approval_response")
# Step 4: Wait for either the timeout or the approval response
winner_task = yield task.when_any([approval_task, timeout_task])
# Step 5: Process based on which task completed
if winner_task == approval_task:
# Human responded in time
approval_data = yield approval_task
# Process the approval
result = yield ctx.call_activity("process_approval", input={
"request_id": request_id,
"is_approved": approval_data.get("is_approved", False),
"approver": approval_data.get("approver", "Unknown")
})
else:
# Timeout occurred
result = {
"request_id": request_id,
"status": "Timeout",
"timed_out_at": ctx.current_utc_datetime.isoformat()
}
return result
이 샘플은 .NET, JavaScript, Java 및 Python 대해 표시됩니다.
import com.microsoft.durabletask.*;
import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions;
import java.time.Duration;
DurableTaskGrpcWorker worker = DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(connectionString)
.addOrchestration(new TaskOrchestrationFactory() {
@Override
public String getName() { return "ApprovalWorkflow"; }
@Override
public TaskOrchestration create() {
return ctx -> {
// Get the approval request
ApprovalRequest request = ctx.getInput(ApprovalRequest.class);
// Set initial status
ctx.setCustomStatus(new WorkflowStatus("Waiting for approval", null));
// Create a timeout timer
Task timeoutTask = ctx.createTimer(
Duration.ofHours((long) request.timeoutHours));
// Wait for external event (approval response)
Task<ApprovalResponse> approvalTask = ctx.waitForExternalEvent(
"approval_response", ApprovalResponse.class);
// Wait for either approval or timeout
Task<?> winner = ctx.anyOf(approvalTask, timeoutTask).await();
if (winner == approvalTask) {
// Human responded in time
ApprovalResponse response = approvalTask.await();
String status = response.isApproved ? "APPROVED" : "REJECTED";
ctx.setCustomStatus(new WorkflowStatus(status, response));
ctx.complete(new WorkflowResult(
status,
response.isApproved ? "Request approved" : "Request rejected"));
} else {
// Timeout occurred
ctx.setCustomStatus(new WorkflowStatus("Timed out", null));
ctx.complete(new WorkflowResult("TIMEOUT", "Request timed out"));
}
};
}
})
.build();
이 오케스트레이터는 다음 작업을 수행합니다.
- 승인자에게 알리기 위한 활동을 호출하여 승인 요청을 전송합니다.
- 클라이언트가 진행률을 추적할 수 있도록 사용자 지정 상태를 설정합니다.
- 시간 제한 기한에 대한 지속성 타이머를 만듭니다.
- 승인자가 촉발하는 외부 이벤트(
approval_response)를 기다립니다.
-
WhenAny, when_any, 또는 anyOf를 사용하여 승인 또는 시간 제한 중 먼저 완료되는 것을 기다립니다.
- 완료된 태스크에 따라 결과를 처리합니다.
경고
더 이상 필요하지 않은 타이머를 취소합니다. C# 예제에서 오케스트레이션은 승인을 받으면 시간 제한 타이머를 취소합니다.
활동 정의
E4_SendSmsChallenge 작업 함수
E4_SendSmsChallenge 함수는 Twilio 바인딩을 사용하여 사용자에게 4자리 코드가 포함된 SMS 메시지를 보냅니다.
[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;
}
메모
샘플을 실행하려면 Microsoft.Azure.WebJobs.Extensions.Twilio NuGet 패키지를 설치합니다. 기본 Twilio NuGet 패키지 는 버전 충돌 및 빌드 오류를 일으킬 수 있으므로 설치하지 마세요.
V3 프로그래밍 모델
function.json 파일을 다음과 같이 정의하세요:
{
"bindings": [
{
"name": "phoneNumber",
"type": "activityTrigger",
"direction": "in"
},
{
"type": "twilioSms",
"name": "message",
"from": "%TwilioPhoneNumber%",
"accountSidSetting": "TwilioAccountSid",
"authTokenSetting": "TwilioAuthToken",
"direction": "out"
}
],
"disabled": false
}
이 코드는 4자리 챌린지 코드를 생성하고 SMS 메시지를 보냅니다.
const seedrandom = require("seedrandom");
const uuidv1 = require("uuid/v1");
module.exports = function (context, phoneNumber) {
// Get a random number generator with a random seed (not time-based)
const rand = seedrandom(uuidv1());
const challengeCode = Math.floor(rand() * 10000);
context.log(`Sending verification code ${challengeCode} to ${phoneNumber}.`);
context.bindings.message = {
body: `Your verification code is ${challengeCode.toPrecision(4)}`,
to: phoneNumber,
};
context.done(null, challengeCode);
};
V4 프로그래밍 모델
다음은 4자리 챌린지 코드를 생성하고 SMS 메시지를 보내는 코드입니다.
const { output } = require("@azure/functions");
const df = require("durable-functions");
const sendSmsChallengeActivityName = "sendSmsChallenge";
const twilioOutput = output.generic({
type: "twilioSms",
from: "%TwilioPhoneNumber%",
accountSidSetting: "TwilioAccountSid",
authTokenSetting: "TwilioAuthToken",
});
df.app.activity(sendSmsChallengeActivityName, {
extraOutputs: [twilioOutput],
handler: function (phoneNumber, context) {
// Get a random challenge code
const challengeCode = Math.floor(Math.random() * 10000);
context.log(`Sending verification code ${challengeCode} to ${phoneNumber}.`);
context.extraOutputs.set(twilioOutput, {
body: `Your verification code is ${challengeCode.toPrecision(4)}`,
to: phoneNumber,
});
return challengeCode;
},
});
function.json 파일을 다음과 같이 정의하세요:
{
"bindings": [
{
"name": "phoneNumber",
"type": "activityTrigger",
"direction": "in"
},
{
"type": "twilioSms",
"name": "message",
"from": "%TwilioPhoneNumber%",
"accountSidSetting": "TwilioAccountSid",
"authTokenSetting": "TwilioAuthToken",
"direction": "out"
}
]
}
이 코드는 4자리 챌린지 코드를 생성하고 SMS 메시지를 보냅니다.
import json
import random
random.seed(10)
def main(phoneNumber, message):
code = random.randint(0, 10000)
payload = {
"body": f"Your verification code is {code}",
"to": phoneNumber
}
message.set(json.dumps(payload))
code_str = str(code)
return code_str
이 활동은 무작위로 4자리 도전 코드를 생성합니다. 실제 애플리케이션에서는 이 코드가 지정된 전화번호로 SMS 메시지도 전송합니다.
param($phoneNumber)
# Generate a random four-digit challenge code
$challengeCode = Get-Random -Minimum 1000 -Maximum 10000
Write-Host "Sending verification code $challengeCode to $phoneNumber."
# In a real app, send the SMS here using Twilio or another provider
$challengeCode
@FunctionName("E4_SendSmsChallenge")
public int sendSmsChallenge(
@DurableActivityTrigger(name = "phoneNumber") String phoneNumber) {
// Generate a random four-digit challenge code
int challengeCode = ThreadLocalRandom.current().nextInt(1000, 10000);
// In a real app, send the SMS here using Twilio or another provider
Logger logger = Logger.getLogger("E4_SendSmsChallenge");
logger.info(String.format("Sending verification code %d to %s.", challengeCode, phoneNumber));
return challengeCode;
}
활동은 승인 요청을 제출하고 응답을 처리합니다.
승인 요청 활동 제출
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);
}
}
import { ActivityContext } from "@microsoft/durabletask-js";
const submitRequest = async (
_ctx: ActivityContext,
request: { amount: number }
): Promise<string> => {
console.log(
`[submitRequest] Purchase request submitted: $${request.amount}`
);
return `REQ-${Date.now()}`;
};
import datetime
from durabletask import task
def submit_approval_request(ctx: task.ActivityContext, request_data: dict) -> dict:
"""
Activity that submits an approval request.
In a real application, this would notify a human approver via email, message, etc.
"""
request_id = request_data.get("request_id")
requester = request_data.get("requester")
item = request_data.get("item")
# In a real system, this would send an email, notification, or update a database
return {
"request_id": request_id,
"status": "Pending",
"submitted_at": datetime.datetime.now().isoformat(),
"approval_url": f"http://localhost:8000/api/approvals/{request_id}"
}
이 샘플은 .NET, JavaScript, Java 및 Python 대해 표시됩니다.
Java 샘플은 승인 요청을 인라인으로 제출합니다. 프로덕션 앱의 경우 별도의 작업을 만듭니다.
.addActivity(new TaskActivityFactory() {
@Override
public String getName() { return "SubmitApprovalRequest"; }
@Override
public TaskActivity create() {
return ctx -> {
ApprovalRequest request = ctx.getInput(ApprovalRequest.class);
// In a real system, send email/notification
logger.info("Submitting approval request for " + request.item);
return new SubmissionResult(
"Pending",
Instant.now().toString(),
"http://localhost:8000/api/approvals/" + request.requester);
};
}
})
프로세스 승인 활동
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; }
}
import { ActivityContext } from "@microsoft/durabletask-js";
const processApproval = async (
_ctx: ActivityContext,
data: { requestId: string; approved: boolean }
): Promise<string> => {
console.log(
`[processApproval] Request ${data.requestId}: ${
data.approved ? "APPROVED" : "REJECTED"
}`
);
return data.approved ? "Order placed" : "Order cancelled";
};
const notifyTimeout = async (
_ctx: ActivityContext,
requestId: string
): Promise<string> => {
console.log(
`[notifyTimeout] Request ${requestId} timed out — auto-rejected`
);
return "Timed out — auto-rejected";
};
import datetime
from durabletask import task
def process_approval(ctx: task.ActivityContext, approval_data: dict) -> dict:
"""
Activity that processes the approval once received.
"""
request_id = approval_data.get("request_id")
is_approved = approval_data.get("is_approved")
approver = approval_data.get("approver")
status = "Approved" if is_approved else "Rejected"
# In a real system, this would update a database, trigger workflows, etc.
return {
"request_id": request_id,
"status": status,
"processed_at": datetime.datetime.now().isoformat(),
"approver": approver
}
이 샘플은 .NET, JavaScript, Java 및 Python 대해 표시됩니다.
Java 샘플은 오케스트레이터에서 직접 승인을 처리합니다. 프로덕션 앱의 경우 별도의 작업을 만듭니다.
인간 상호 작용 샘플 실행
샘플에서 HTTP 트리거 함수를 사용하여 다음 HTTP POST 요청을 전송하여 오케스트레이션을 시작합니다.
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}"}
오케스트레이터 함수는 전화 번호를 수신하고 임의로 생성된 4자리 확인 코드를 사용하여 해당 번호로 SMS 메시지를 즉시 보냅니다. 예를 들면 2168다음과 같습니다. 그런 다음 이 함수는 90초 동안 응답을 기다립니다.
코드로 회신하려면 다른 함수에서 RaiseEventAsync(.NET) 또는 raiseEvent(JavaScript 및 TypeScript)를 사용하거나 202 응답에서 sendEventPostUri HTTP POST 엔드포인트를 호출합니다. 다음으로 대체 {eventName} 합니다.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
타이머가 만료되기 전에 이벤트를 보내면 오케스트레이션이 완료되고 output 필드가 성공적으로 확인되었음을 나타내는 필드로 true설정됩니다.
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"}
타이머가 만료되거나 잘못된 코드를 네 번 입력하면, 상태를 쿼리하여 output이(가) false으로 설정되어 전화 확인에 실패했음을 나타내는지 확인합니다.
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"}
샘플을 실행하려면 다음을 수행합니다.
로컬 개발을 위해 지속성 작업 스케줄러 에뮬레이터를 시작합니다.
Docker 를 설치해야 합니다.
docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
작업자를 시작하여 오케스트레이터 및 활동을 등록합니다.
클라이언트를 실행 하여 승인 워크플로를 예약하고 이벤트를 보냅니다.
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}");
import {
DurableTaskAzureManagedClientBuilder,
DurableTaskAzureManagedWorkerBuilder,
} from "@microsoft/durabletask-js-azuremanaged";
const client = new DurableTaskAzureManagedClientBuilder()
.connectionString(connectionString)
.build();
const worker = new DurableTaskAzureManagedWorkerBuilder()
.connectionString(connectionString)
.addOrchestrator(approvalOrchestrator)
.addActivity(submitRequest)
.addActivity(processApproval)
.addActivity(notifyTimeout)
.build();
await worker.start();
// Schedule the approval workflow
const approvalId = await client.scheduleNewOrchestration(
approvalOrchestrator,
500 // amount
);
console.log(`Orchestration started: ${approvalId}`);
// Wait for it to reach "Awaiting approval", then send approval
await new Promise((r) => setTimeout(r, 3000));
await client.raiseOrchestrationEvent(approvalId, "approval", {
approved: true,
});
console.log("Sent approval event");
const result = await client.waitForOrchestrationCompletion(
approvalId,
true,
60
);
console.log(`Result: ${result?.serializedOutput}`);
await worker.stop();
await client.stop();
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
import time
import uuid
client = DurableTaskSchedulerClient(
host_address=endpoint,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub,
token_credential=credential
)
# Schedule the approval workflow
input_data = {
"request_id": f"request-{uuid.uuid4()}",
"requester": "john.doe@example.com",
"item": "Vacation Request - 5 days",
"timeout_hours": 24
}
instance_id = client.schedule_new_orchestration(
human_interaction_orchestrator,
input=input_data
)
print(f"Started approval workflow: {instance_id}")
# Simulate human approving the request
print("Simulating approval...")
time.sleep(2)
# Raise the approval event
approval_response = {
"is_approved": True,
"approver": "manager@example.com"
}
client.raise_orchestration_event(instance_id, "approval_response", data=approval_response)
# 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.time.Instant;
import java.util.UUID;
DurableTaskClient client = DurableTaskSchedulerClientExtensions
.createClientBuilder(connectionString).build();
// Schedule the approval workflow
ApprovalRequest request = new ApprovalRequest(
"john.doe@example.com",
"Vacation Request - 5 days",
24 // timeout hours
);
String instanceId = client.scheduleNewOrchestrationInstance(
"ApprovalWorkflow",
new NewOrchestrationInstanceOptions()
.setInput(request)
.setInstanceId("request-" + UUID.randomUUID().toString()));
System.out.println("Started approval workflow: " + instanceId);
// Simulate human approving the request
System.out.println("Simulating approval...");
Thread.sleep(2000);
// Raise the approval event
ApprovalResponse response = new ApprovalResponse(
true, // isApproved
"manager@example.com",
"Approved",
Instant.now().toString()
);
client.raiseEvent(instanceId, "approval_response", response);
// Wait for completion
OrchestrationMetadata result = client.waitForInstanceCompletion(
instanceId, Duration.ofSeconds(60), true);
System.out.println("Result: " + result.readOutputAs(WorkflowResult.class).status);
다음 단계
이 샘플에서는 WaitForExternalEvent 및 CreateTimer API를 포함한 고급 Durable Functions 기능을 보여 줍니다.
Task.WhenAny(C#), context.df.Task.any(JavaScript 및 TypeScript) 또는 context.task_any(Python)를 결합하여 사용자가 응답할 때까지 기다리는 워크플로에 대해 신뢰할 수 있는 시간 제한 패턴을 구현하는 방법을 보여 줍니다.
이 샘플에서는 지속성 작업 SDK를 사용하여 구성 가능한 시간 제한과 함께 사람들이 응답할 때까지 기다리는 워크플로를 구현하는 방법을 보여 줍니다. 주요 개념:
-
외부 이벤트: 입력을 기다리는 데 사용
WaitForExternalEvent
-
지속성 타이머: 타임아웃을 구현하기 위한
CreateTimer 사용
-
경쟁 작업:
WhenAny, when_any 또는 anyOf을(를) 사용하여 먼저 완료되는 작업을 처리합니다.