타임아웃 패턴
개요
주문 화면이 재고 서비스에 수량을 묻는다고 하자. 재고 서비스가 응답하지 않으면 await inventory.GetStockAsync("A-1")은 돌아오지 않는다. 그동안 이 요청이 쓰는 연결과 메모리가 묶이고, 동기 호출이라면 요청을 처리하던 스레드도 함께 묶인다. 얼마나 기다릴지는 클라이언트 기본값이 정하는데, .NET HttpClient.Timeout의 기본값은 100초이고1 gRPC는 기본적으로 기한을 설정하지 않아 클라이언트가 끝없이 기다릴 수 있다.2 이런 호출이 쌓이면 재고 서비스 하나의 지연이 주문 화면 전체의 지연이 된다.
타임아웃 패턴(Timeout pattern)은 호출마다 기다릴 최대 시간을 정하고, 그 시간이 지나면 기다림을 끝내고 실패로 처리한다. 역할은 셋이다. 호출자(caller)는 얼마나 기다릴지 정하고, 시간이 지나면 결과를 "timed out"으로 다룬다. 취소 신호는 시간이 다 됐다는 사실을 작업에 전한다. .NET에서는 CancellationTokenSource가 정해진 시간 뒤에 취소를 알리고, 그 소스가 만든 CancellationToken을 작업에 넘긴다. JavaScript에서는 AbortController와 AbortSignal이 같은 일을 한다. 작업(operation)은 받은 신호를 확인하고, 하던 일을 정리한 뒤 멈춘다.
세 번째 역할이 빠지면 패턴은 반만 동작한다. 호출자는 기다리기를 그만두지만, 신호를 받지 못한 작업은 끝까지 실행되며 자원을 계속 쓴다. .NET의 취소는 협조적 취소(cooperative cancellation)여서, 취소를 요청하는 쪽은 신호만 보내고 언제 어떻게 멈출지는 신호를 받은 작업이 스스로 정한다.3
구조
caller는 CancelAfter(50 ms)로 취소를 예약하고 같은 소스의 토큰을 GetStockAsync에 넘긴다. 50 ms가 지나면 토큰이 취소되고, SlowInventory는 1000 ms짜리 대기를 그만둔 뒤 OperationCanceledException으로 끝난다. 호출자는 이 취소가 자기가 건 타임아웃 때문인지 확인한 뒤 "timed out"으로 처리한다.
역사
Michael Nygard는 운영 중인 소프트웨어가 무너지는 방식과 이를 막는 설계를 Release It!에서 다뤘다. 이 책의 초판은 2007년 Pragmatic Bookshelf에서 나왔다.4 초판의 5장 Stability Patterns는 5.1 Use Timeouts로 시작하고, 5.2 Circuit Breaker와 5.3 Bulkheads가 뒤를 잇는다.5 2018년에 나온 2판의 목차에서도 안정성 패턴(Stability Patterns) 부분은 Timeouts로 시작한다.6
2판의 안정성 안티패턴 장은 통합 지점(Integration Points), 곧 소켓이나 원격 호출로 다른 시스템과 이어지는 모든 지점을 위험 요소로 본다. 원격 서버가 연결 요청을 따라잡지 못해 연결이 반쯤 열린 채 대기열에 남으면, 연결을 연 스레드는 운영체제 커널 안에서 막히고 연결 타임아웃은 대개 분 단위다. 응답을 읽는 read() 호출도 기본값이 무한정 기다리는 경우가 많아서, 막힌 호출에서 빠져나오려면 소켓 타임아웃을 직접 설정해야 한다. 막힌 스레드는 다른 요청을 처리하지 못하므로, 책은 느린 응답이 응답하지 않는 것보다 훨씬 나쁘다고 정리한다.7
.NET 쪽 자료도 기다림을 멈추는 일과 작업을 멈추는 일을 구분한다. Stephen Toub는 2012년 글에서 취소를 지원하지 않는 비동기 작업을 취소할 수 있느냐고 묻고 "No"라고 답했다. 그가 보인 방법은 작업과 취소 요청 중 먼저 오는 쪽을 기다리는 것인데, 그는 이 방법이 기다림만 끝낼 뿐 작업 자체는 한동안 더 실행될 수 있다고 적었다.8 .NET 6부터 제공되는 Task.WaitAsync(TimeSpan)도 같은 성격이다. 문서에는 반환되는 Task가 원래 Task가 끝나거나 제한 시간이 지나면 완료되고, 시간이 지난 경우에는 TimeoutException으로 끝난다는 설명만 있고 원래 Task를 멈춘다는 내용은 없다.9
왜 사용하는가?
느린 의존성이 호출자의 자원을 붙잡는 시간에 상한이 생긴다. 작업이 취소에 응답해 정리되면 연결과 메모리(동기 호출이라면 스레드도)가 풀린다. 이때 호출자는 기본값을 돌려줄지, 오류로 응답할지, 나중에 다시 시도할지를 고를 수 있다. 타임아웃이 난 횟수는 의존성에 문제가 있다는 신호이기도 하다. Microsoft의 표준 HTTP 회복성 처리기에서 회로 차단기는 직접적인 실패나 타임아웃이 너무 많으면 실행을 막는다.10 이 판단은 서킷 브레이커 패턴에서 다룬다.
작업이 취소 신호를 받지 않거나 받고도 확인하지 않으면, 타임아웃이 나도 호출자만 떠나고 작업은 남는다. Polly의 타임아웃 전략 문서도 콜백이 취소 토큰을 따르지 않으면 취소 요청 뒤에도 계속 실행된다고 경고한다.11 이렇게 버려진 작업은 연결과 메모리를 계속 쓰고, 나중에 돌려준 객체를 정리할 쪽이 없거나 그 사이 발생한 예외가 아무도 모르게 묻힐 수 있다.8
타임아웃은 결과를 모르는 상태를 남긴다. 호출자가 포기한 시점에 서버는 아직 요청을 처리하고 있었을 수도, 이미 끝냈을 수도 있다. 결제나 주문 생성처럼 외부에 효과를 내는 호출이라면, 타임아웃 뒤에 같은 요청을 다시 보내기 전에 그 작업이 멱등인지 확인해야 한다. 멱등이란 같은 요청을 여러 번 보내도 한 번 보낸 것과 결과가 같다는 뜻이다. 재시도와 묶을 때의 조건은 재시도 패턴에서 다룬다.
설정한 타임아웃이 호출의 어느 단계까지 덮는지는 구현마다 다르다. AWS Builders' Library 글은 DNS 조회나 TLS 핸드셰이크 같은 원격 호출을 타임아웃이 덮지 않는 구현도 있다고 적는다.12
값과 배치 위치
값은 의존성의 지연 분포를 보고 고른다. AWS 글은 먼저 받아들일 수 있는 거짓 타임아웃 비율(예: 0.1%)을 정하고, 하위 서비스 지연에서 그에 해당하는 백분위수를 타임아웃으로 삼는다고 설명한다. 이 예에서는 p99.9, 곧 요청의 99.9%가 그 시간 안에 끝나는 지연값이다. 거짓 타임아웃은 조금만 더 기다렸으면 성공했을 호출을 끊는 경우다. 같은 글은 타임아웃이 너무 길면 기다리는 동안 자원이 계속 쓰여 쓸모가 줄고, 너무 짧으면 재시도가 늘어 백엔드 트래픽과 지연이 커지며, 지연이 조금만 늘어도 모든 요청이 재시도되어 전체 장애로 번질 수 있다고 경고한다.12
호출이 여러 단계로 이어지면 단계마다 따로 정한 타임아웃만으로는 부족하다. 바깥 요청에 2초를 주고 안쪽 호출 셋에 각각 2초를 주면, 바깥 호출자가 이미 포기한 뒤에도 안쪽 호출이 계속 돈다. 그래서 요청 전체가 끝나야 하는 시각, 곧 기한(deadline)을 하나 정해 하위 호출에 넘긴다. 타임아웃이 "얼마 동안"이라면 기한은 "언제까지"다. gRPC는 기한을 클라이언트가 그 뒤로는 응답을 기다리지 않을 시점으로 정의하고, 기한을 넘긴 호출을 DEADLINE_EXCEEDED 상태로 실패시킨다. 서버가 받은 기한은 이미 지난 시간을 뺀 타임아웃으로 바뀌며, 일부 언어 구현은 이를 하위 호출에 자동으로 전파한다.2
.NET 프로세스 안에서는 같은 CancellationToken을 하위 호출에 그대로 넘기는 것이 기한 전파에 해당한다. 토큰은 프로세스 경계를 넘지 못하므로, 다른 서비스를 부를 때는 gRPC 기한처럼 남은 시간을 요청에 실어 보내야 한다. 하위 호출이 자기 제한 시간을 더하고 싶으면 연결된 토큰(linked token)을 만든다. 연결된 토큰은 원래 토큰 여럿을 하나로 묶은 것으로, 그중 하나라도 취소되면 함께 취소된다.13 이렇게 하면 실제 제한은 남은 기한과 자기 제한 중 짧은 쪽이 된다. HttpClient 문서도 클라이언트 전체의 Timeout과 요청별 CancellationTokenSource를 함께 쓰면 둘 중 짧은 쪽만 적용된다고 적는다.1
재시도와 함께 쓸 때는 타임아웃이 두 개 필요하다. 시도 한 번에 거는 시도별 타임아웃과, 재시도와 그 사이 대기까지 모두 합친 전체 타임아웃이다. 시도별 타임아웃만 있으면 전체 시간이 시도 수만큼 늘어 호출자가 기대한 시간을 넘을 수 있고, 전체 타임아웃만 있으면 느린 시도 하나가 예산을 다 써서 재시도할 기회가 사라진다. Microsoft.Extensions.Http.Resilience의 표준 회복성 처리기는 바깥에서 안쪽 순서로 속도 제한, 전체 타임아웃(기본 30초), 재시도, 회로 차단기, 시도별 타임아웃(기본 10초)을 쌓는다. 전체 타임아웃이 재시도를 감싸고, 시도별 타임아웃은 재시도 안쪽에서 시도 하나를 제한한다.10 여러 계층이 각자 재시도하면 부하는 곱으로 늘어난다. AWS 글의 예에서는 고객의 호출 하나가 다섯 단계의 서비스 호출로 이어져 데이터베이스 조회로 끝나고 계층마다 세 번씩 시도하는데, 계층마다 독립적으로 재시도하면 데이터베이스 부하가 243배가 된다.14
예제
1000 ms가 걸리는 가짜 재고 서비스에 50 ms 타임아웃을 건다. 작업은 토큰을 Task.Delay에 넘겨 취소 요청이 오면 대기를 바로 끝내고, 상태를 기록한 뒤 ThrowIfCancellationRequested로 취소로 끝났음을 호출자에게 알린다. Microsoft 문서가 취소에 응답한 사실을 호출한 쪽에 알릴 때 권하는 방법이다.15 ConfigureAwaitOptions.SuppressThrowing(.NET 8부터)은 취소된 대기에서 예외를 던지지 않게 해서, 작업이 토큰을 직접 확인하고 정리할 자리를 만든다.16 이 옵션은 결과가 없는 Task에만 쓸 수 있고, Task<T>에 쓰면 ArgumentOutOfRangeException이 난다.17
using System;
using System.Threading;
using System.Threading.Tasks;
var inventory = new SlowInventory(TimeSpan.FromMilliseconds(1000));
// 호출자가 기다릴 시간을 정한다. 50 ms가 지나면 이 소스의 토큰이 취소된다.
using var timeout = new CancellationTokenSource();
timeout.CancelAfter(TimeSpan.FromMilliseconds(50));
try
{
int quantity = await inventory.GetStockAsync("A-1", timeout.Token);
Console.WriteLine($"stock: {quantity}");
}
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
{
Console.WriteLine("timed out");
}
Console.WriteLine($"work: {inventory.State}");
// 느린 재고 서비스를 흉내 낸다. 응답에 1000 ms가 걸리지만 취소 토큰을 따른다.
sealed class SlowInventory(TimeSpan delay)
{
public string State { get; private set; } = "not started";
public async Task<int> GetStockAsync(string sku, CancellationToken ct)
{
State = "running";
// 취소 요청이 오면 Delay가 곧바로 끝난다. 예외 대신 토큰을 직접 확인하려고 SuppressThrowing을 쓴다.
await Task.Delay(delay, ct).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
if (ct.IsCancellationRequested)
{
// 열린 연결이나 잠금을 정리하는 자리다. 정리한 뒤 취소로 끝났음을 알린다.
State = "stopped by cancellation";
ct.ThrowIfCancellationRequested();
}
State = "completed";
return 7;
}
}timed out
work: stopped by cancellation호출자는 when (timeout.IsCancellationRequested) 필터로 자기가 건 타임아웃 때문에 취소된 경우만 "timed out"으로 처리했고, 작업은 1000 ms를 채우지 않고 신호를 받은 시점에 멈췄다.
범위 넓힌 예제
세 경우를 비교한다. 첫째는 WaitAsync로 기다림만 끊는 경우로, 작업에는 CancellationToken.None을 넘겨 신호가 가지 않게 했다. 둘째는 CreateLinkedTokenSource로 상위 토큰과 호출별 제한 시간을 묶어 먼저 오는 쪽이 작업을 멈추게 한다. 상위 토큰은 요청을 보낸 사용자가 취소하거나 연결을 끊을 때 취소되는 토큰이다. 셋째는 요청 전체에 500 ms 기한을 주고 그 기한 토큰 하나를 세 호출이 나눠 쓰는 경우다. CallAsync는 상위 토큰과 기한 토큰을 따로 받아, 호출이 어떻게 끝났는지를 CallStatus 값으로 돌려준다. 사용자 취소는 UpstreamCanceled, 공유 기한 만료는 DeadlineExceeded, 이 호출의 제한 초과는 TimedOut이다. 사용자가 취소한 호출을 의존성의 실패로 세면 서킷 브레이커가 멀쩡한 의존성을 차단할 수 있어서 이렇게 나눈다. 기한 만료는 앞선 호출이 예산을 얼마나 썼는지에 따라서도 생기므로, 차단기에 셀지는 따로 정한다.
using System;
using System.Threading;
using System.Threading.Tasks;
TimeSpan Ms(int value) => TimeSpan.FromMilliseconds(value);
// 1) WaitAsync: 호출자만 기다림을 멈춘다. 작업에는 취소 신호가 가지 않는다.
var abandoned = new FakeService("stock", Ms(1000));
Task<int> running = abandoned.GetAsync(CancellationToken.None);
try
{
await running.WaitAsync(Ms(50));
}
catch (TimeoutException)
{
Console.WriteLine($"WaitAsync: caller stopped waiting, work: {abandoned.State}");
}
int late = await running;
Console.WriteLine($"WaitAsync: abandoned work finished later with {late}, work: {abandoned.State}");
// 2) 연결된 토큰: 상위 취소와 이 호출의 제한 시간 중 먼저 오는 쪽이 작업을 멈춘다.
using (var request = new CancellationTokenSource())
{
Console.WriteLine(await CallAsync(new FakeService("stock", Ms(1000)), request.Token, CancellationToken.None, Ms(50)));
}
using (var request = new CancellationTokenSource())
{
request.CancelAfter(Ms(50)); // 예: 요청을 보낸 클라이언트가 연결을 끊었다.
Console.WriteLine(await CallAsync(new FakeService("stock", Ms(1000)), request.Token, CancellationToken.None, Ms(5000)));
}
// 3) 공유 기한: 요청 전체에 500 ms를 주고, 기한 토큰 하나를 모든 하위 호출에 넘긴다.
using (var request = new CancellationTokenSource())
using (var deadline = new CancellationTokenSource(Ms(500)))
{
Console.WriteLine(await CallAsync(new FakeService("price", Ms(30)), request.Token, deadline.Token, Ms(5000)));
Console.WriteLine(await CallAsync(new FakeService("stock", Ms(10000)), request.Token, deadline.Token, Ms(5000)));
Console.WriteLine(await CallAsync(new FakeService("ship", Ms(30)), request.Token, deadline.Token, Ms(5000)));
}
// 호출 하나에 자기 제한 시간을 걸고, 어떤 이유로 끝났는지 값으로 돌려준다.
// 상위 취소와 공유 기한은 따로 받아야 끝난 이유를 구분할 수 있다.
static async Task<CallResult> CallAsync(FakeService service, CancellationToken upstream, CancellationToken deadline, TimeSpan limit)
{
// 이미 취소됐거나 기한이 지났으면 시작하지 않는다.
if (upstream.IsCancellationRequested || deadline.IsCancellationRequested)
return new CallResult(service.Name, CallStatus.NotStarted, service.State);
using var linked = CancellationTokenSource.CreateLinkedTokenSource(upstream, deadline);
linked.CancelAfter(limit);
try
{
await service.GetAsync(linked.Token);
return new CallResult(service.Name, CallStatus.Ok, service.State);
}
catch (OperationCanceledException) when (upstream.IsCancellationRequested)
{
// 사용자가 취소했다. 의존성의 실패가 아니다.
return new CallResult(service.Name, CallStatus.UpstreamCanceled, service.State);
}
catch (OperationCanceledException) when (deadline.IsCancellationRequested)
{
// 요청 전체의 기한이 다 됐다.
return new CallResult(service.Name, CallStatus.DeadlineExceeded, service.State);
}
catch (OperationCanceledException) when (linked.IsCancellationRequested)
{
// 이 호출의 제한 시간이 지났다.
return new CallResult(service.Name, CallStatus.TimedOut, service.State);
}
}
enum CallStatus { Ok, TimedOut, DeadlineExceeded, UpstreamCanceled, NotStarted }
sealed record CallResult(string Name, CallStatus Status, string Work)
{
public override string ToString() => $"{Name}: {Status}, work: {Work}";
}
// 느린 의존성을 흉내 낸다. 정해진 시간이 걸리지만 취소 토큰을 따른다.
sealed class FakeService(string name, TimeSpan delay)
{
public string Name => name;
public string State { get; private set; } = "not started";
public async Task<int> GetAsync(CancellationToken ct)
{
State = "running";
// 취소 요청이 오면 Delay가 곧바로 끝난다. 예외 대신 토큰을 직접 확인하려고 SuppressThrowing을 쓴다.
await Task.Delay(delay, ct).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
if (ct.IsCancellationRequested)
{
// 열린 연결이나 잠금을 정리하는 자리다. 정리한 뒤 취소로 끝났음을 알린다.
State = "stopped by cancellation";
ct.ThrowIfCancellationRequested();
}
State = "completed";
return 7;
}
}WaitAsync: caller stopped waiting, work: running
WaitAsync: abandoned work finished later with 7, work: completed
stock: TimedOut, work: stopped by cancellation
stock: UpstreamCanceled, work: stopped by cancellation
price: Ok, work: completed
stock: DeadlineExceeded, work: stopped by cancellation
ship: NotStarted, work: not started처음 두 줄은 WaitAsync가 TimeoutException을 낸 뒤에도 작업이 running으로 남았다가 끝까지 실행됐음을 보여 준다. 실제 서비스라면 그동안 연결과 메모리를 계속 썼을 것이다. 공유 기한에서 stock 호출은 자기 제한 5000 ms가 아니라 남은 기한 때문에 멈춰 DeadlineExceeded로 보고됐고, 기한이 지난 뒤의 ship 호출은 시작하지 않았다. 이 예제의 취소는 작업이 토큰을 확인하는 지점에서만 효과가 있다. 토큰을 받지 않는 라이브러리 호출이나 토큰을 확인하지 않는 계산 루프는 중간에 멈추지 않으므로, 토큰을 받는 API를 고르거나 루프 안에서 토큰을 확인해야 한다. 또 토큰은 같은 프로세스 안의 작업만 멈춘다. 이미 네트워크로 나간 요청은 서버가 끝까지 처리했을 수 있으므로, 결과를 모르는 호출은 상태를 다시 조회하거나 멱등성을 보장한 뒤에 재시도한다.
TypeScript 예제
Node.js와 브라우저에서는 AbortSignal이 취소 신호 역할을 한다. AbortSignal.timeout(ms)는 지정한 시간이 지나면 스스로 중단되는 신호를 돌려주고, 이때 중단 이유(reason)는 이름이 TimeoutError인 DOMException이다. 브라우저 사양에서 이 제한은 활성 시간 기준이다. 워커가 일시 중단되거나 페이지가 bfcache에 머무는 동안에는 카운트도 멈춘다.18 반면 AbortController.abort()를 이유 없이 부르면 이유는 AbortError가 된다.19 AbortSignal.any는 여러 신호 중 하나라도 중단되면 중단되는 신호를 만들어, C#의 연결된 토큰과 같은 일을 한다. Node.js에는 AbortSignal.timeout이 v17.3.0과 v16.14.0에, AbortSignal.any가 v20.3.0과 v18.17.0에 들어왔다.20 두 번째 경우의 Promise.race는 WaitAsync처럼 기다림만 끊는다.
// 느린 의존성을 흉내 낸다. delayMs가 걸리지만 AbortSignal을 따른다.
class FakeService {
state = "not started";
constructor(readonly name: string, private readonly delayMs: number) {}
get(signal: AbortSignal): Promise<number> {
return new Promise((resolve, reject) => {
if (signal.aborted) {
reject(signal.reason);
return;
}
this.state = "running";
const onAbort = () => {
// 타이머를 지우는 것이 실제로 작업을 멈추는 부분이다.
clearTimeout(timer);
this.state = "stopped by abort";
reject(signal.reason);
};
const timer = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
this.state = "completed";
resolve(7);
}, this.delayMs);
signal.addEventListener("abort", onAbort, { once: true });
});
}
}
// 중단 이유를 값으로 바꾼다. 타임아웃은 TimeoutError, abort()는 AbortError다.
function reasonOf(e: unknown): string {
if (e instanceof DOMException && e.name === "TimeoutError") return "timed out";
if (e instanceof DOMException && e.name === "AbortError") return "canceled by caller";
return "failed";
}
async function call(service: FakeService, signal: AbortSignal): Promise<string> {
try {
await service.get(signal);
return `${service.name}: ok, work: ${service.state}`;
} catch (e) {
return `${service.name}: ${reasonOf(e)}, work: ${service.state}`;
}
}
async function main(): Promise<void> {
// 1) AbortSignal.timeout: 50 ms 뒤 신호가 중단되고 작업이 그 신호를 받아 멈춘다.
console.log(await call(new FakeService("stock", 1000), AbortSignal.timeout(50)));
// 2) 기다림만 끊기: 작업에 신호를 주지 않으면 작업은 계속 돈다.
const abandoned = new FakeService("stock", 1000);
const running = abandoned.get(new AbortController().signal);
const timer = new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 50));
const first = await Promise.race([running, timer]);
console.log(`race: ${first}, work: ${abandoned.state}`);
console.log(`race: abandoned work finished later with ${await running}, work: ${abandoned.state}`);
// 3) 신호 합치기: 호출자의 취소와 타임아웃 중 먼저 오는 쪽이 작업을 멈춘다.
const caller = new AbortController();
setTimeout(() => caller.abort(), 50);
const combined = AbortSignal.any([caller.signal, AbortSignal.timeout(5000)]);
console.log(await call(new FakeService("stock", 1000), combined));
}
main();stock: timed out, work: stopped by abort
race: timeout, work: running
race: abandoned work finished later with 7, work: completed
stock: canceled by caller, work: stopped by abortFakeService는 abort 이벤트를 받으면 타이머를 지우고 거부(reject)한다. 이 처리가 없으면 신호가 중단되어도 작업은 끝까지 돈다. fetch처럼 signal 옵션을 받는 API에는 fetch(url, { signal: AbortSignal.timeout(5000) })처럼 신호를 넘기면 된다.21
같이 보기
C# 예제는 각각 독립된 .NET 10 콘솔 프로그램이다.
TypeScript 예제는 ES2022 이상으로 컴파일한 JavaScript를 Node.js에서 실행한다.
각주
- Microsoft Learn. HttpClient.Timeout Property. Remarks. ↩
- gRPC. Deadlines. ↩
- Microsoft Learn. Cancellation in Managed Threads. 도입부. ↩
- Michael T. Nygard. Release It! Design and Deploy Production-Ready Software. Pragmatic Bookshelf, 2007. Internet Archive 서지. ↩
- Michael T. Nygard. Release It! Pragmatic Bookshelf, 2007. 출판사 목차 발췌 PDF. 5장 Stability Patterns. 이 발췌는 2009년 2월 P3.0 인쇄본에서 뽑은 것이다. ↩
- Michael T. Nygard. Release It! Second Edition. Pragmatic Bookshelf, 2018. 출판사 목차. Create Stability, Stability Patterns. ↩
- Michael T. Nygard. Release It! Second Edition (2018). 출판사 발췌 PDF. 4장 Stability Antipatterns, Integration Points, Socket-Based Protocols. ↩
- Stephen Toub. How do I cancel non-cancelable async operations? .NET Blog, 2012. ↩
- Microsoft Learn. Task.WaitAsync Method. WaitAsync(TimeSpan), Applies to. ↩
- Microsoft Learn. Build resilient HTTP apps. Standard resilience handler defaults. ↩
- Polly. Timeout resilience strategy. ↩
- AWS Builders' Library. Timeouts, retries, and backoff with jitter (2019). Timeouts. ↩
- Microsoft Learn. Cancellation in Managed Threads. Listen to multiple tokens simultaneously. ↩
- AWS Builders' Library. Timeouts, retries, and backoff with jitter (2019). Retries and backoff. ↩
- Microsoft Learn. Cancellation in Managed Threads. Listening and Responding to Cancellation Requests. ↩
- Microsoft Learn. ConfigureAwaitOptions Enum. Fields, Applies to. ↩
- Microsoft Learn. CA2261: Do not use ConfigureAwaitOptions.SuppressThrowing with Task<TResult>. ↩
- MDN Web Docs. AbortSignal: timeout() static method. ↩
- MDN Web Docs. AbortController: abort() method. ↩
- Node.js. Global objects: AbortSignal. AbortSignal.timeout(delay), AbortSignal.any(signals). ↩
- MDN Web Docs. AbortSignal: timeout() static method. Examples. ↩