9.4.8 الگو Retry Timeout

9.4.8 الگو Retry Timeout

9.4.8.1 توضیحات #

الگوی Retry Timeout یکی از رایج‌ترین و مهم‌ترین الگوها در توسعه سرویس‌های پایدار (resilient) و سیستم‌های توزیع‌شده است. این الگو زمانی کاربرد دارد که عملیاتی مانند تماس با سرویس خارجی (مثلاً API، پایگاه داده، یا هر نوع ارتباط شبکه‌ای) ممکن است به صورت موقت شکست بخورد و لازم باشد با رعایت یک فاصله زمانی معین (timeout) چند بار به طور خودکار تلاش مجدد (retry) صورت بگیرد تا شانس موفقیت افزایش یابد و تجربه کاربری بهبود پیدا کند.

در Go، برای پیاده‌سازی این الگو معمولاً از یک حلقه ساده (for) به همراه تابع time.After یا متدهایی مانند time.Sleep استفاده می‌شود. در هر تلاش، ابتدا عملیات مورد نظر (مثلاً ارسال درخواست) اجرا می‌شود. اگر عملیات موفقیت‌آمیز نبود، برنامه برای مدتی مشخص (مثلاً یک ثانیه یا بیشتر) صبر می‌کند و دوباره تلاش می‌کند. این روند تا زمانی ادامه می‌یابد که یا تعداد تلاش‌ها از حد تعیین‌شده عبور کند، یا عملیات با موفقیت انجام شود، یا یک سیگنال کنسل (مانند context) دریافت شود. با استفاده از time.After، به صورت idiomatic و بدون مسدودسازی غیرضروری، می‌توان در هر تکرار مدت زمان انتظار بین تلاش‌ها را پیاده‌سازی کرد.

مزیت الگوی Retry Timeout در Go این است که هم پیاده‌سازی آن ساده و خوانا است، هم قابلیت ترکیب با context برای پشتیبانی از لغو یا timeout کل عملیات وجود دارد و هم می‌توانید برای هر retry، لاگ، آمار، و حتی سیاست‌هایی مانند افزایش تدریجی تاخیر (exponential backoff) یا محدودیت کل زمان تلاش‌ها را به راحتی پیاده‌سازی کنید. این الگو یکی از ستون‌های اصلی resiliency در معماری میکروسرویس، ارتباطات شبکه‌ای و سامانه‌های distributed به شمار می‌رود و پیروی از آن، کیفیت و پایداری سرویس‌های شما را به طور چشمگیری افزایش می‌دهد.

9.4.8.2 دیاگرام #

flowchart TD Start([Start]) TryOp[Try Operation] Success{Success?} RetryMore{Retries Left?} Wait[Wait for timeout] DoneSuccess([Done: Success]) DoneFail([Done: Fail]) Start --> TryOp TryOp --> Success Success -- Yes --> DoneSuccess Success -- No --> RetryMore RetryMore -- Yes --> Wait Wait --> TryOp RetryMore -- No --> DoneFail

9.4.8.3 نمونه کد #

 1package main
 2
 3import (
 4	"context"
 5	"fmt"
 6	"io"
 7	"net/http"
 8	"time"
 9)
10
11func main() {
12	const (
13		url          = "http://example.com"
14		maxRetries   = 3
15		retryTimeout = 2 * time.Second
16	)
17	var resp *http.Response
18	var err error
19
20	// Timeout کلی روی کل تلاش‌ها (مثلاً 10 ثانیه)
21	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
22	defer cancel()
23
24	for i := 1; i <= maxRetries; i++ {
25		req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
26		resp, err = http.DefaultClient.Do(req)
27		if err == nil {
28			fmt.Printf("Success on attempt %d\n", i)
29			break
30		}
31		fmt.Printf("Attempt %d failed: %v\n", i, err)
32		if i < maxRetries {
33			select {
34			case <-ctx.Done():
35				fmt.Println("Global timeout reached, aborting retries.")
36				break
37			case <-time.After(retryTimeout):
38				fmt.Println("Retrying...")
39			}
40		}
41	}
42
43	if err != nil {
44		fmt.Println("Final Error:", err)
45		return
46	}
47	defer resp.Body.Close()
48
49	// خواندن بخشی از بدنه برای اطمینان از آزاد شدن connection
50	body, _ := io.ReadAll(io.LimitReader(resp.Body, 200))
51	fmt.Println("Status:", resp.Status)
52	fmt.Printf("Body sample: %q\n", body)
53}
1$ go run main.go
2Attempt 1 failed: Get "http://example.com": dial tcp: lookup example.com on 169.254.169.254:53: dial udp 169.254.169.254:53: connect: no route to host
3Retrying...
4Attempt 2 failed: Get "http://example.com": dial tcp: lookup example.com on 169.254.169.254:53: dial udp 169.254.169.254:53: connect: no route to host
5Retrying...
6Attempt 3 failed: Get "http://example.com": dial tcp: lookup example.com on 169.254.169.254:53: dial udp 169.254.169.254:53: connect: no route to host
7Final Error: Get "http://example.com": dial tcp: lookup example.com on 169.254.169.254:53: dial udp 169.254.169.254:53: connect: no route to host

در این مثال از الگوی Retry Timeout، ما یک عملیات HTTP GET به آدرس مورد نظر را با رویکردی حرفه‌ای و ایمن، همراه با تلاش مجدد (Retry) و کنترل Timeout پیاده‌سازی می‌کنیم. ابتدا تعداد تلاش مجدد (maxRetries)، مدت تأخیر بین هر تلاش (retryTimeout) و یک Timeout کلی برای کل عملیات (۱۰ ثانیه) تعریف شده است تا سیستم هم از نظر مدیریت زمان و هم از نظر مصرف منابع، رفتار پیش‌بینی‌پذیری داشته باشد.

در ابتدای حلقه، یک context با Timeout کلی ساخته می‌شود. برای هر تلاش (تا سقف مجاز)، یک درخواست HTTP با همین context ساخته می‌شود تا در صورت لغو کلی یا پایان مهلت، همه retryها به صورت امن و هماهنگ متوقف شوند. اگر درخواست موفقیت‌آمیز باشد، شماره تلاش موفق ثبت می‌شود و حلقه خاتمه پیدا می‌کند. اگر شکست بخورد، پیام مربوط به شماره تلاش و علت خطا چاپ می‌شود. پیش از هر retry (به جز آخرین تلاش)، یک وقفه زمانی برقرار می‌شود. این وقفه با select روی ctx.Done() نیز کنترل می‌شود تا اگر قبل از شروع تلاش بعدی، context لغو شده بود، عملیات فوراً متوقف گردد و پیام مناسبی نمایش داده شود.

پس از پایان حلقه، اگر همچنان خطا وجود داشته باشد، پیام خطا چاپ و برنامه خاتمه پیدا می‌کند. اگر درخواست موفقیت‌آمیز بوده، بدنه‌ی پاسخ حتماً بسته می‌شود تا از leak شدن منابع جلوگیری شود و connection قابل استفاده مجدد بماند. همچنین برای نمونه، بخشی از پاسخ خوانده و در خروجی چاپ می‌شود تا مطمئن شویم اتصال به سرور کامل برقرار شده است.

این معماری باعث می‌شود هم مدیریت خطا و retry کاملاً تحت کنترل باشد، هم منابع شبکه‌ای آزاد باقی بمانند و هم بتوان به سادگی رفتارهای حرفه‌ای‌تر مانند backoff تصاعدی، Circuit Breaker، یا لاگ پیشرفته‌تر را به آن اضافه کرد. چنین رویکردی در سامانه‌های توزیع‌شده و میکروسرویس‌ها استاندارد طلایی محسوب می‌شود و در پروژه‌های production-ready بسیار توصیه می‌شود.

9.4.8.4 کاربردها #

  • مقابله با خطاهای موقت شبکه و سرور: الگوی Retry Timeout اغلب زمانی به کار می‌رود که عملیات‌هایی مانند تماس با سرورهای HTTP، APIهای خارجی، پایگاه داده یا سایر سرویس‌های وابسته ممکن است به دلیل مشکلات موقتی (مانند قطعی لحظه‌ای شبکه، بار زیاد سرور، یا مشکلات DNS) شکست بخورند. با تکرار خودکار عملیات با فاصله زمانی کنترل‌شده، برنامه می‌تواند از گذرا بودن خطاها عبور کند و احتمال موفقیت را بدون دخالت کاربر بالا ببرد.
  • پایداری و تاب‌آوری (Resilience) سرویس‌ها: در معماری‌های میکروسرویس، توزیع‌شده و ابری، Retry Timeout یک ابزار کلیدی برای افزایش resiliency سامانه است. این الگو کمک می‌کند سامانه به طور خودکار در برابر اختلالات کوتاه‌مدت واکنش نشان دهد و از fail شدن کل عملیات به خاطر یک خطای زودگذر جلوگیری کند.
  • کاهش بار بر کاربر و تجربه کاربری بهتر: با استفاده از Retry Timeout، نیاز به تلاش مجدد دستی توسط کاربر حذف می‌شود و کاربران بدون آگاهی از خطاهای موقت، تجربه‌ای پیوسته و روان خواهند داشت. به‌ویژه در اپلیکیشن‌های موبایل یا تحت وب که اتصال شبکه متغیر است، این موضوع اهمیت بیشتری پیدا می‌کند.
  • پیاده‌سازی Backoff و Circuit Breaker: این الگو پایه‌ای برای پیاده‌سازی استراتژی‌های پیچیده‌تری مانند exponential backoff (افزایش تدریجی فاصله بین تلاش‌ها) و circuit breaker (جلوگیری از retry پیوسته هنگام شکست‌های دائم) نیز هست که بهبود پایداری و هوشمندی retry را به ارمغان می‌آورد.
  • اطمینان از اتمام موفق عملیات بحرانی: در کارهایی مانند ارسال تراکنش‌های بانکی، نوشتن در دیتابیس، یا ارسال پیام‌های حیاتی، Retry Timeout شانس اطمینان از موفقیت عملیات را افزایش می‌دهد و احتمال از دست رفتن داده‌ها را تا حد زیادی کاهش می‌دهد.