Lompat ke konten
dokumentasi.id
Commit ecba1e8
v2
Star2,8 rb

4.2

Siklus Request dan Retry

Semua method pembantu pada httpclient.Client berakhir di satu pintu: Get, Post, Put, Patch, dan Delete membangun *http.Request lalu memanggil c.Do(request) tanpa logika retry sendiri (httpclient/client.go:57-67). Karena itu seluruh siklus percobaan, pengecekan status code, dan perakitan error hidup di dalam Client.Do. NewClient memulai dengan retryCount bernilai 0 dan heimdall.NewNoRetrier(), sehingga tanpa option hanya ada satu percobaan (httpclient/client.go:27-39). Cara memasang WithRetryCount, WithRetrier, dan option lain dibahas di Option pada httpclient.

TahapFungsiPerilaku
Penyiapan bodyinternal.SetRequestGetBodybody dibaca ke buffer dan GetBody dipasang; hanya dijalankan bila retryCount > 0 (internal/request_helper.go:11-31)
Reset bodyinternal.CloneRequestshallow clone lalu Body diganti dari reqGetBody (internal/request_helper.go:33-45)
Jedainternal.SleepInterruptiblemenunggu c.retrier.NextInterval(i-1) dan batal bila context selesai (internal/sleep.go:9-16)
Notifikasi pluginreportRequestStart, reportError, reportRequestEnddipanggil pada setiap percobaan (httpclient/client.go:204-220)
Keputusan berhentiskipRetrycontext selesai atau budget retry habis (httpclient/client.go:195-202)
Perakitan errorinternal.BuildMultiErrormenggabungkan errs menjadi satu error (internal/multi_error.go:5-14)

Penyiapan body request

Do langsung menutup body request asli lewat defer, karena SetRequestGetBody akan membungkus body itu dengan noop closer (httpclient/client.go:122-139). Penyiapan body hanya dijalankan bila c.retryCount > 0, sehingga request tanpa retry tidak membayar overhead pembacaan body. Hasilnya juga disalin ke variabel lokal reqGetBody, sebab request.GetBody bisa ditimpa plugin atau middleware di antara percobaan.

SetRequestGetBody melewati request yang nil, tidak punya body, atau sudah memiliki GetBody. Untuk http.NoBody ia memasang getter ringan tanpa alokasi buffer. Untuk body biasa ia membaca seluruh isi ke buf, mengganti r.Body dengan newBytesBody(buf), lalu memasang GetBody yang membuat reader baru dari buffer yang sama (internal/request_helper.go:11-31). Efeknya satu salinan body dapat diputar ulang sebanyak jumlah percobaan.

Siklus satu request

sequenceDiagram
    autonumber
    participant Caller
    participant Client as "Client.Do"
    participant Helper as "internal request helper"
    participant Retrier as "c.retrier"
    participant Sleeper as "internal.SleepInterruptible"
    participant Doer as "c.client.Do"
    participant Plugins as "c.plugins"
    participant Budget as "retryErrorBudget"
    participant Multi as "internal.BuildMultiError"

    Caller->>Client: Do request
    Note over Client: defer close body request asli
    alt retryCount lebih besar dari 0
        Client->>Helper: SetRequestGetBody
        Helper-->>Client: GetBody disalin ke reqGetBody
    end
    loop i = 0 sampai retryCount
        opt i lebih besar dari 0 dan response sebelumnya ada
            Client->>Client: drain body lama lalu Close
        end
        opt i lebih besar dari 0
            Client->>Retrier: NextInterval i-1
            Retrier-->>Client: durasi jeda
            Client->>Sleeper: tunggu sampai timer atau ctx selesai
            Sleeper-->>Client: ctx.Err bila context dibatalkan
            Client->>Helper: CloneRequest dengan reqGetBody
        end
        Client->>Plugins: OnRequestStart
        Client->>Doer: Do request
        Doer-->>Client: response dan err
        alt err tidak nil
            Client->>Plugins: OnError
            Client->>Budget: Failure
            Note over Client: skipRetry lalu lanjut percobaan berikutnya
        else status code masuk daftar retry
            Client->>Plugins: OnRequestEnd
            Client->>Budget: Failure
            Note over Client: skipRetry lalu lanjut percobaan berikutnya
        else status code lain
            Client->>Budget: Success
            Note over Client: errs dikosongkan lalu break
        end
    end
    Client->>Multi: BuildMultiError errs
    Multi-->>Caller: response dan error
%% fig: Siklus Client.Do dari penyiapan body sampai BuildMultiError
%% desc: Do menyiapkan body yang dapat diputar ulang, menjalankan loop percobaan dengan jeda dan clone request, lalu merakit error akhir.
Fig. 1: Siklus Client.Do dari penyiapan body sampai BuildMultiErrorDo menyiapkan body yang dapat diputar ulang, menjalankan loop percobaan dengan jeda dan clone request, lalu merakit error akhir.

Batas loop for i := 0; i <= c.retryCount; i++ berarti c.client.Do dipanggil paling banyak retryCount + 1 kali, dan body response percobaan sebelumnya dibuang lewat io.Copy(io.Discard, …) lalu ditutup sebelum percobaan baru dimulai (httpclient/client.go:144-163). Pada setiap iterasi selain yang pertama, Do menjeda dengan c.retrier.NextInterval(i-1) sehingga indeks retry yang diterima retrier dimulai dari 0 (retry.go:5-8). Jeda itu melewati internal.SleepInterruptible, yang mengembalikan ctx.Err() bila context selesai sebelum timer menyala; error tersebut masuk ke errs, dilaporkan ke plugin, lalu loop berhenti (internal/sleep.go:9-16). noRetrier.NextInterval selalu mengembalikan 0, jadi client tanpa retrier tidak pernah menunggu sama sekali (retry.go:49-51).

Setelah jeda, internal.CloneRequest membuat shallow clone lewat request.WithContext(request.Context()) dan mengganti Body dengan hasil reqGetBody(); body lama tidak di-clone dalam, sehingga memori tetap satu buffer (internal/request_helper.go:33-45). Strategi backoff yang mengisi NextInterval dijelaskan di Retry dan Backoff.

Pengecekan status code

Setelah c.client.Do mengembalikan response tanpa error, Do memanggil reportRequestEnd lalu memutuskan apakah percobaan diulang (httpclient/client.go:165-190). Ada dua pemicu: kode status ditemukan di c.retryableCodes, atau kode status >= http.StatusInternalServerError. Pencarian memakai slices.BinarySearch, jadi daftar wajib terurut; WithRetryableStatusCodes mengurutkan ulang daftar setiap kali option dipasang (httpclient/options.go:48-55).

Hasil c.client.DoIsi errsKeputusan
error transportditambah satu entryskipRetry lalu lanjut percobaan berikutnya
status code ada di retryableCodestidak diubahskipRetry lalu lanjut percobaan berikutnya
status code >= 500tidak diubahskipRetry lalu lanjut percobaan berikutnya
status code laindikosongkan dengan errs = nilSuccess() lalu break

Ketika tidak ada kondisi yang cocok, errs dikosongkan, error budget menerima Success(), dan loop berhenti; response itulah yang dikembalikan (httpclient/client.go:165-190).

⚠️ Jalur status code tidak pernah menambahkan entry ke errs. Bila semua percobaan berakhir dengan 5xx dan tidak ada error transport, Do mengembalikan response 5xx bersama nil sebagai error (httpclient/client.go:165-190). Pemeriksaan response.StatusCode tetap tanggung jawab pemanggil.

Berhenti lebih awal

Dua jalur di dalam loop memanggil c.skipRetry(request.Context()) sebelum lanjut ke iterasi berikutnya (httpclient/client.go:144-163). skipRetry mengembalikan true bila internal.IsCtxDone melihat context sudah selesai; pada kasus itu Success() dipanggil dan loop berhenti tanpa percobaan tambahan. IsCtxDone hanya melakukan pengecekan non-blocking pada ctx.Done() (internal/ctx.go:5-12). Bila context masih hidup, keputusan diserahkan ke retryErrorBudget.Failure() (httpclient/client.go:195-202).

Failure() mencatat satu token kegagalan dan melaporkan over budget ketika sisa token sudah menyentuh threshold (internal/error_budget.go:96-111). Bila error budget tidak dipasang, receiver nil membuat Success() dan Failure() sama-sama mengembalikan false, sehingga retry tidak pernah diblokir budget (internal/error_budget.go:77-92). Ambang dan rasio token diatur lewat option; rincian kebijakannya ada di Error Budget untuk Retry.

Perakitan error akhir

Baris terakhir Do mengembalikan internal.BuildMultiError(errs) bersama response terakhir (httpclient/client.go:192). Fungsi itu memetakan panjang errs ke tiga bentuk hasil: nil, error tunggal, atau *multiError (internal/multi_error.go:5-14).

graph TD
    A["errs []error"] --> B{"len errs"}
    B -->|"0"| C["return nil"]
    B -->|"1"| D["return errors[0]"]
    B -->|"lebih dari 1"| E["multiError"]
    E --> F["Error: gabung dengan koma"]
    E --> G["Unwrap []error"]
    style A fill:#8b5cf6,color:#fff
    style B fill:#f59e0b,color:#000
    style C fill:#2d7d46,color:#fff
    style D fill:#dc2626,color:#fff
    style E fill:#dc2626,color:#fff
    style F fill:#dc2626,color:#fff
    style G fill:#8b5cf6,color:#fff
%% fig: Pemetaan errs ke bentuk error akhir
%% desc: BuildMultiError memilih nil, error tunggal, atau multiError berdasarkan panjang errs.
Fig. 2: Pemetaan errs ke bentuk error akhirBuildMultiError memilih nil, error tunggal, atau multiError berdasarkan panjang errs.

multiError.Error() merangkai pesan setiap error dengan pemisah ", ", sedangkan Unwrap() []error mengembalikan seluruh isi slice sehingga errors.Is dan errors.As dapat menelusuri tiap error percobaan (internal/multi_error.go:20-34). Dengan begitu kegagalan berulang tetap terbaca satu per satu, tidak hanya sebagai pesan gabungan.