5
Retry dan Backoff
Retry di heimdall dipecah menjadi dua kontrak terpisah. Retriable menentukan berapa lama menunggu sebelum percobaan berikutnya, dan Backoff menghitung besaran interval itu. Keduanya hanya punya satu method, sehingga strategi penundaan bisa diganti tanpa menyentuh loop retry di klien.
Dua interface dan komposisinya
Retriable mendefinisikan satu method, NextInterval(retry int) time.Duration retry.go:5-81. Backoff juga satu method, Next(retry int) time.Duration backoff.go:9-122.
NewRetrier membungkus sebuah Backoff menjadi Retriable. Struct retrier menyimpan field backoff, dan NextInterval meneruskan nilai retry ke r.backoff.Next(retry) retry.go:19-283 retry.go:35-384.
graph LR
CB["NewConstantBackoff"] --> BO["Backoff"]
EB["NewExponentialBackoff"] --> BO
BO --> NR["NewRetrier"]
NR --> RT["Retriable"]
RF["NewRetrierFunc"] --> RT
NN["NewNoRetrier"] --> RT
RT --> CL["Client.retrier.NextInterval"]
style BO fill:#f59e0b,color:#000
style RT fill:#f59e0b,color:#000
style CB fill:#8b5cf6,color:#fff
style EB fill:#8b5cf6,color:#fff
style NR fill:#8b5cf6,color:#fff
style RF fill:#8b5cf6,color:#fff
style NN fill:#8b5cf6,color:#fff
style CL fill:#2d7d46,color:#fff
%% fig: Komposisi Backoff dan Retriable
%% desc: Dua konstruktor backoff menghasilkan Backoff, NewRetrier membungkusnya menjadi Retriable, sedangkan NewRetrierFunc dan NewNoRetrier langsung menghasilkan Retriable.Tabel berikut merangkum lima konstruktor yang tersedia di paket heimdall.
| Konstruktor | Parameter | Nilai balik |
|---|---|---|
NewConstantBackoff | backoffInterval, maximumJitterInterval time.Duration | Backoff backoff.go:19-305 |
NewExponentialBackoff | initialTimeout, maxTimeout time.Duration, exponentFactor float64, maximumJitterInterval time.Duration | Backoff backoff.go:44-576 |
NewRetrier | backoff Backoff | Retriable retry.go:23-287 |
NewRetrierFunc | f RetriableFunc | Retriable retry.go:30-338 |
NewNoRetrier | tidak ada | Retriable retry.go:43-509 |
Perpindahan percobaan di klien HTTP
Loop di Client.Do memanggil c.retrier.NextInterval(i-1) sebelum percobaan ulang, lalu menunggu lewat internal.SleepInterruptible httpclient/client.go:149-15510. Argumen i-1 berarti percobaan ulang pertama memakai retry = 0. Klien baru memakai NewNoRetrier() sebagai nilai default retry.go:43-509 httpclient/client.go:3911, dan strategi diganti dengan option WithRetrier httpclient/options.go:30-3512. Urutan request secara utuh dijelaskan di Siklus Request dan Retry.
stateDiagram-v2
[*] --> Attempt
Attempt --> Success: "errs = nil"
Attempt --> Sleep: "error atau StatusCode >= 500"
Sleep --> Attempt: "SleepInterruptible(NextInterval(i-1))"
Sleep --> Aborted: "ctx.Done()"
Attempt --> Exhausted: "i == retryCount"
Success --> [*]
Exhausted --> [*]
Aborted --> [*]
style Success fill:#2d7d46,color:#fff
style Sleep fill:#f59e0b,color:#000
style Aborted fill:#dc2626,color:#fff
style Exhausted fill:#dc2626,color:#fff
%% fig: Perpindahan percobaan retry
%% desc: Percobaan yang gagal masuk ke state Sleep dengan durasi dari NextInterval(i-1); pembatalan context memindahkan loop ke Aborted tanpa menunggu lagi.SleepInterruptible memilih antara ctx.Done() dan time.After(d), jadi pembatalan context mengembalikan error dan loop retry dihentikan internal/sleep.go:9-1613.
Constant backoff
NewConstantBackoff menyimpan backoffInterval dan maximumJitterInterval dalam satuan milidetik, dan menetralkan jitter negatif menjadi 0 backoff.go:19-305. Next mengembalikan interval tetap ditambah jitter acak rand.Int64N(maximumJitterInterval+1) milidetik backoff.go:32-3514. Nilai retry tidak mempengaruhi hasil.
retry | Next(retry) dengan backoffInterval 100ms dan jitter 0 |
|---|---|
| 0 | 100ms |
| 1 | 100ms |
| 2 | 100ms |
| 3 | 100ms |
Angka di atas berasal dari TestConstantBackoffNextTime backoff_test.go:83-9215. Lewat NewRetrier, NewConstantBackoff(2ms, 1ms) menghasilkan NextInterval(1) minimal 2ms retry_test.go:19-2916.
Exponential backoff
NewExponentialBackoff menerima initialTimeout, maxTimeout, exponentFactor, dan maximumJitterInterval backoff.go:44-576. Next menormalkan retry < 0 menjadi 0, lalu memakai math.Min(initialTimeout*factor^retry, maxTimeout) dan menambahkan jitter acak backoff.go:59-6517.
retry | Next(retry), initialTimeout 100ms, maxTimeout 1000ms, exponentFactor 2.0, jitter 0 |
|---|---|
| 0 | 100ms |
| 1 | 200ms |
| 2 | 400ms |
| 3 | 800ms |
| 4 | 1000ms, dipotong maxTimeout |
Empat baris pertama diuji di TestExponentialBackoffNextTime backoff_test.go:10-1918, sedangkan pemotongan pada retry = 4 diuji di TestExponentialBackoffMaxTimeoutCrossed backoff_test.go:32-3819. Jika maxTimeout dinaikkan menjadi 1600ms, nilai Next(4) tepat 1600ms backoff_test.go:32-38. Nilai retry negatif menghasilkan interval percobaan pertama, yaitu 100ms backoff_test.go:48-54. Lewat NewRetrier dengan initialTimeout 2ms dan maxTimeout 10ms, NextInterval(1) minimal 4ms retry_test.go:10-17.
Jitter menambah rentang acak di atas nilai dasar.
maximumJitterInterval | Rentang Next(1) dengan initialTimeout 100ms dan exponentFactor 2.0 |
|---|---|
0 | tepat 200ms pada 10000 iterasi backoff_test.go:56-6322 |
1ms | 200ms sampai 201ms backoff_test.go:65-7223 |
50ms | 200ms sampai 250ms backoff_test.go:74-8124 |
⚠️
maximumJitterIntervalnegatif dinetralkan menjadi0di dalam konstruktor untuk menghindari panic saat membangkitkan jitter acak backoff.go:20-2425. Perilaku ini diuji dengan nilai-1msyang tetap menghasilkan 100ms, 200ms, 400ms, dan 800ms backoff_test.go:21-3026. Jangan mengandalkan jitter negatif sebagai pembatas interval.
Adapter fungsi dan null object
RetriableFunc adalah tipe fungsi func(retry int) time.Duration dengan method NextInterval yang hanya memanggil fungsi tersebut retry.go:10-1727. NewRetrierFunc mengembalikan f apa adanya sebagai Retriable, sehingga perhitungan interval bisa ditulis langsung tanpa struct backoff retry.go:30-338. Pola ini dipakai pada test retrier linear yang mengembalikan retry milidetik dan 0 untuk retry <= 0 retry_test.go:31-4228.
NewNoRetrier menyediakan null object: NextInterval selalu mengembalikan 0 * time.Millisecond retry.go:43-509, dan test memastikan NextInterval(1) bernilai nol retry_test.go:44-5029. Ini membuat klien tanpa konfigurasi tidak menunggu di antara percobaan.