Automasi developer di ekosistem GitHub makin mendorong tim untuk menghubungkan repository, pipeline build, deployment, dan status check ke banyak sistem internal. Pola ini terlihat jelas dari banyaknya proyek automasi dan workflow tooling yang muncul di komunitas GitHub, termasuk semangat eksperimen yang sering tampak pada peserta dan pemenang challenge komunitas seperti Finish-Up-A-Thon. Namun, begitu integrasi CI/CD mulai saling memanggil API, masalah klasik segera muncul: request ganda.
Jika endpoint trigger build atau deployment tidak dirancang dengan baik, satu retry karena timeout bisa memicu dua build, dua deployment, atau dua perubahan status. Solusinya bukan sekadar “jangan retry”, melainkan mendesain retry yang aman dengan idempotency key, deduplikasi di sisi server, kontrak status yang konsisten, dan observability yang memadai. Artikel ini fokus pada implementasi praktis untuk API integrasi CI/CD, bukan teori umum.
Mengapa API CI/CD rentan terhadap request ganda
Dalam integrasi CI/CD, duplikasi biasanya bukan bug tunggal, melainkan konsekuensi dari sistem terdistribusi:
- Client timeout: client tidak menerima response tepat waktu lalu mengirim ulang request, padahal server mungkin sudah memprosesnya.
- Retry otomatis: HTTP client, job runner, queue worker, atau gateway dapat melakukan retry.
- Race condition: dua actor berbeda memicu operasi yang sama hampir bersamaan, misalnya webhook dan manual retry dari UI.
- Webhook redelivery: platform seperti GitHub dapat mengirim ulang event jika endpoint sebelumnya gagal atau tidak merespons sesuai harapan.
- Kegagalan parsial: record database sudah tersimpan, tetapi enqueue job gagal; atau job berhasil dibuat, tetapi response ke client terputus.
Pada konteks CI/CD, efeknya lebih berbahaya daripada sekadar data dobel. Duplikasi bisa berarti image dibangun dua kali, environment ter-deploy ulang tanpa perlu, rollback terpicu keliru, atau status commit menjadi tidak konsisten.
Apa itu idempotency key dan kapan harus dipakai
Idempotency key adalah identifier unik yang dikirim client untuk menyatakan bahwa beberapa request identik seharusnya dianggap sebagai operasi logis yang sama. Jika request yang sama dikirim ulang dengan key yang sama, server tidak membuat operasi baru, tetapi mengembalikan hasil dari operasi pertama atau status operasi yang sedang berjalan.
Idempotency key sangat cocok untuk endpoint seperti:
POST /buildsuntuk memicu buildPOST /deploymentsuntuk memulai deploymentPOST /status-syncuntuk sinkronisasi status dari sistem eksternalPOST /webhook/githubjika Anda perlu melindungi downstream processing dari redelivery
Secara umum, idempotency key paling penting pada operasi yang:
- mengubah state,
- punya efek samping ke sistem lain,
- mahal untuk diulang, atau
- tidak aman jika dieksekusi lebih dari sekali.
Catatan: Idempotency key bukan pengganti validasi bisnis. Jika dua request berbeda kebetulan punya payload mirip, Anda tetap perlu aturan bisnis yang jelas untuk menentukan apakah keduanya memang operasi yang sama atau tidak.
Desain kontrak API: request, response, dan perilaku retry
1. Tentukan sumber idempotency key
Paling umum, client mengirim header seperti Idempotency-Key. Anda juga bisa menerima key di body, tetapi header lebih jelas untuk concern transport-level.
POST /deployments HTTP/1.1
Content-Type: application/json
Idempotency-Key: dep-20250706-8f3c2a
{
"repository": "acme/payment-service",
"commit_sha": "abc123def456",
"environment": "staging",
"triggered_by": "github-actions"
}Key sebaiknya:
- unik per operasi logis,
- cukup sulit bertabrakan,
- stabil di semua retry untuk request yang sama.
Praktiknya, key sering dibentuk dari UUID. Untuk webhook, jika platform pengirim memiliki event ID yang unik, gunakan itu sebagai bahan utama deduplikasi.
2. Simpan fingerprint request
Server sebaiknya tidak hanya menyimpan key, tetapi juga fingerprint payload, misalnya hash dari field-field penting. Tujuannya untuk mendeteksi kasus berbahaya: key sama, payload berbeda.
Contoh field penting untuk trigger deployment:
- repository
- commit SHA
- environment
- workflow atau pipeline identifier
Jika request datang dengan idempotency key yang sama tetapi fingerprint berbeda, server sebaiknya menolak dengan error yang jelas karena client menggunakan key secara tidak konsisten.
3. Gunakan response yang dapat diulang dengan aman
Untuk operasi async seperti build dan deployment, pola yang paling aman adalah:
- request pertama membuat operasi dan mengembalikan representasi job,
- retry dengan key yang sama mengembalikan representasi job yang sama, bukan membuat job baru.
Contoh response awal:
HTTP/1.1 202 Accepted
Content-Type: application/json
Location: /deployments/dep_789
{
"deployment_id": "dep_789",
"status": "queued",
"idempotency_key": "dep-20250706-8f3c2a"
}Jika client retry setelah timeout dan operasi yang sama sudah tercatat, server dapat mengembalikan:
HTTP/1.1 200 OK
Content-Type: application/json
Location: /deployments/dep_789
{
"deployment_id": "dep_789",
"status": "queued",
"idempotency_key": "dep-20250706-8f3c2a"
}Atau tetap 202 Accepted jika prosesnya masih berjalan. Yang penting adalah tidak menciptakan operasi baru dan response-nya konsisten.
4. Pilih status code dengan sengaja
Tidak ada satu-satunya jawaban mutlak, tetapi pola berikut umumnya masuk akal:
- 201 Created: operasi sinkron dan resource baru berhasil dibuat.
- 202 Accepted: operasi diterima untuk diproses async, umum untuk build/deploy.
- 200 OK: retry menemukan hasil yang sudah ada dan mengembalikan representasi yang sama.
- 409 Conflict: idempotency key sama tetapi payload berbeda, atau state bisnis tidak mengizinkan duplikasi tertentu.
- 422 Unprocessable Entity: payload valid secara format, tetapi tidak lolos aturan domain.
- 500/502/503/504: kegagalan server atau upstream; client boleh retry jika aman dan terdokumentasi.
Kesalahan umum adalah memakai 409 Conflict untuk semua duplikasi. Jika duplicate request dengan key yang sama memang bagian dari kontrak idempotent, biasanya lebih berguna mengembalikan hasil operasi sebelumnya daripada menjawab conflict.
Implementasi deduplikasi di sisi server
Pola data minimal
Anda membutuhkan penyimpanan yang bisa diakses atomik, misalnya database relasional, key-value store, atau kombinasi keduanya. Record idempotency minimal biasanya berisi:
idempotency_keyrequest_fingerprintoperation_typesepertideployment.createresource_idatau job ID hasil operasistatusmisalnyaprocessing,succeeded,failedresponse_snapshotopsional, jika ingin mengembalikan response identikexpires_atuntuk TTLcreated_atdanupdated_at
Hal pentingnya adalah ada unique constraint pada kombinasi yang relevan, biasanya:
(operation_type, idempotency_key)Ini mencegah dua proses paralel sama-sama “merasa” bahwa key belum ada.
Pseudocode alur aman
function createDeployment(request):
key = request.headers["Idempotency-Key"]
if key is missing:
return 400
fingerprint = hash(normalize(request.body))
begin transaction
existing = find idempotency record by (operation_type="deployment.create", key)
if existing exists:
if existing.request_fingerprint != fingerprint:
rollback
return 409 with error "idempotency key reused with different payload"
if existing.status in ["processing", "succeeded", "failed"]:
rollback
return replay(existing)
insert idempotency record with status="processing"
if unique constraint violation:
rollback
existing = re-read record
return replay(existing)
deployment = insert deployment row(status="queued")
enqueue deployment job(deployment.id)
update idempotency record:
resource_id = deployment.id
status = "succeeded"
response_snapshot = {
deployment_id: deployment.id,
status: "queued"
}
commit
return 202 with deployment infoPoin terpenting di atas:
- cek dan insert harus tahan race condition,
- penulisan idempotency record dan pembuatan resource idealnya berada dalam satu boundary konsisten,
- jika enqueue job tidak transactional terhadap database, Anda perlu strategi tambahan seperti outbox pattern.
Kapan memakai database, kapan memakai Redis
Database relasional cocok jika Anda butuh konsistensi kuat, unique constraint, dan keterkaitan dengan resource utama seperti deployment record.
Redis cocok untuk deduplikasi cepat dan TTL sederhana, terutama sebagai lapisan tambahan untuk webhook redelivery ber-volume tinggi. Namun, untuk operasi penting seperti deployment, hanya mengandalkan cache sementara sering tidak cukup karena data dedupe bisa hilang saat eviksi atau restart jika konfigurasi persistence tidak sesuai.
Pilihan praktis yang sering masuk akal:
- database sebagai source of truth untuk idempotency operasi bisnis,
- Redis opsional untuk throttle atau short-term dedupe di depan.
TTL penyimpanan idempotency key: berapa lama?
Tidak ada angka tunggal yang selalu benar. TTL harus mengikuti jendela retry realistis dan risiko bisnis duplikasi.
Pertimbangan utamanya:
- berapa lama client, worker, atau gateway dapat melakukan retry,
- berapa lama webhook redelivery mungkin terjadi,
- berapa mahal jika operasi sama terulang setelah beberapa jam atau hari,
- apakah user mungkin menekan tombol ulang secara manual dengan konteks yang sama.
Untuk trigger build/deploy, TTL sering dipilih cukup panjang agar aman melewati timeout, antrean, dan redelivery. Namun TTL terlalu lama juga punya trade-off:
- penyimpanan membesar,
- key lama bisa memblokir operasi baru jika client salah mendaur ulang key,
- debugging lebih rumit bila kebijakan retensi tidak jelas.
Pendekatan yang aman adalah mendokumentasikan TTL secara eksplisit dan memisahkan antara:
- TTL dedupe: berapa lama key dianggap operasi yang sama,
- retensi audit: berapa lama metadata disimpan untuk investigasi.
Praktik baik: jika TTL habis, request dengan key lama dapat diperlakukan sebagai operasi baru. Karena itu, client tidak boleh mendaur ulang idempotency key untuk aksi berbeda.
Menangani partial success dan kegagalan ambigu
Kasus tersulit bukan duplicate request biasa, melainkan partial success: sebagian proses berhasil, sebagian lagi gagal atau tidak pasti.
Skenario umum
- Record deployment berhasil dibuat, tetapi response ke client gagal terkirim.
- Idempotency record tersimpan, tetapi enqueue job ke broker gagal.
- Job sebenarnya sudah jalan di worker, tetapi status akhir belum tercatat karena database timeout.
- Webhook event sudah diproses ke downstream system, tetapi ack ke pengirim gagal sehingga event di-redeliver.
Cara merancang agar tetap aman
1. Simpan state proses secara eksplisit
Jangan hanya punya “ada/tidak ada” record idempotency. Simpan state seperti processing, succeeded, dan failed. Dengan begitu retry bisa memutuskan apakah harus mengembalikan hasil lama, menunggu, atau menjalankan recovery flow.
2. Pisahkan penerimaan request dari eksekusi efek samping
Untuk deployment async, endpoint sebaiknya hanya mencatat intent dan membuat job. Eksekusi ke cluster, cloud API, atau orchestrator dilakukan worker terpisah. Ini memudahkan retry karena unit idempotent-nya adalah intent atau job creation, bukan seluruh deployment langsung.
3. Gunakan outbox pattern bila ada message broker
Jika Anda menulis ke database lalu mengirim pesan ke queue/broker, ada risiko satu berhasil dan satu gagal. Outbox pattern membantu memastikan event untuk worker diterbitkan dari data yang sudah committed, bukan dikirim secara rentan di tengah transaksi.
4. Sediakan endpoint cek status
Alih-alih memaksa client menebak apakah retry aman, berikan endpoint seperti GET /deployments/{id} atau pencarian berdasarkan idempotency key. Ini penting saat response awal hilang di jaringan.
Observability: tanpa ini, idempotency sulit dioperasikan
Idempotency bukan hanya fitur logika, tetapi juga fitur operasional. Anda perlu bisa menjawab pertanyaan seperti:
- berapa banyak request yang berhasil dideduplicate,
- berapa banyak key reuse dengan payload berbeda,
- berapa lama operasi dengan status
processingtertahan, - apakah ada lonjakan redelivery dari webhook source tertentu.
Minimum yang perlu dicatat
- idempotency key di log terstruktur
- correlation/request ID
- operation type
- resource ID hasil operasi
- dedupe outcome: new, replayed, conflict, expired
- latency dan status code
Metric yang berguna
- jumlah request per endpoint
- rasio replay terhadap request baru
- jumlah conflict karena fingerprint mismatch
- jumlah key dengan status processing terlalu lama
- jumlah enqueue failure setelah record idempotency dibuat
Jika memakai tracing terdistribusi, masukkan idempotency key sebagai atribut span secara hati-hati. Jangan masukkan payload sensitif langsung ke log atau trace.
Contoh alur end-to-end untuk trigger deployment
Alur normal
- GitHub Actions atau sistem internal memanggil
POST /deploymentsdenganIdempotency-Key. - API memvalidasi payload dan menghitung fingerprint.
- API membuat record idempotency status
processing. - API membuat deployment record status
queued. - API menyimpan response snapshot dan mengembalikan
202 Accepted. - Worker mengambil job dan menjalankan deployment.
- Status deployment diperbarui menjadi
running, lalusucceededataufailed.
Alur saat timeout dan retry
- Client mengirim request, tetapi koneksi putus sebelum menerima response.
- Client retry dengan key yang sama.
- API menemukan record idempotency yang sudah ada.
- API mengembalikan deployment yang sama, bukan membuat deployment kedua.
Alur webhook redelivery
- GitHub mengirim event status atau deployment callback.
- Endpoint menerima event ID unik dari header atau body event.
- Sistem menyimpan event ID sebagai dedupe key untuk pemrosesan downstream.
- Jika event yang sama dikirim ulang, API mengabaikan efek samping kedua dan cukup mengembalikan ack yang sesuai.
Contoh pseudocode webhook dedupe
function handleWebhook(event):
deliveryId = event.headers["X-Delivery-ID"] or event.body.event_id
fingerprint = hash(raw_event_body)
if not acquireOrRead(deliveryId, fingerprint):
return 409
if alreadyProcessed(deliveryId):
return 200
processEventSafely(event)
markProcessed(deliveryId)
return 200Untuk webhook, strategi dedupe sering lebih sederhana daripada trigger deployment karena tujuan utamanya adalah mencegah pemrosesan event yang sama berulang. Namun jika event men-trigger operasi bisnis besar, tetap gunakan model status yang eksplisit.
Kesalahan umum dalam implementasi idempotency key
- Hanya memeriksa key di memory process. Ini gagal pada multi-instance deployment.
- Tidak menyimpan fingerprint request. Akibatnya key sama dengan payload berbeda bisa diam-diam diterima.
- Menganggap idempotency sama dengan “ignore duplicate”. Padahal client sering butuh response hasil operasi pertama.
- Tidak ada unique constraint atau operasi atomik. Race condition tetap bisa membuat duplikasi.
- TTL terlalu pendek. Retry sah dari client datang setelah key kedaluwarsa dan operasi terulang.
- Menandai sukses terlalu dini. Misalnya sebelum resource benar-benar tercipta atau job benar-benar tercatat.
- Tidak membedakan operasi sinkron dan async. Untuk CI/CD, banyak operasi lebih aman dimodelkan sebagai async job.
Checklist implementasi untuk API integrasi CI/CD yang lebih aman
- Tentukan endpoint mana yang wajib memakai idempotency key.
- Standarkan lokasi key, idealnya lewat header
Idempotency-Key. - Definisikan aturan pembentukan key di sisi client dan dokumentasikan bahwa key harus stabil saat retry.
- Simpan request fingerprint untuk mendeteksi key reuse dengan payload berbeda.
- Buat unique constraint pada kombinasi operation type dan idempotency key.
- Simpan status record idempotency:
processing,succeeded,failed. - Kembalikan response yang konsisten saat request di-retry.
- Pilih status code yang jelas:
202untuk async,200untuk replay hasil lama,409untuk mismatch. - Tetapkan TTL penyimpanan key berdasarkan jendela retry dan risiko bisnis.
- Sediakan endpoint lookup status untuk operasi async.
- Tambahkan log terstruktur, metric dedupe, dan alert untuk key yang macet di status
processing. - Uji race condition dan network failure, bukan hanya happy path.
Penutup
Dalam integrasi CI/CD, retry itu normal, timeout itu normal, dan webhook redelivery juga normal. Yang tidak normal adalah membiarkan semua itu membuat build, deployment, atau sinkronisasi status berjalan dua kali. Dengan desain API yang memanfaatkan idempotency key, deduplikasi atomik, kontrak response yang konsisten, dan observability yang baik, Anda bisa membuat retry menjadi aman tanpa mengorbankan keandalan automasi.
Jika Anda sedang membangun endpoint trigger build, deployment, atau status sync di ekosistem GitHub dan automasi developer, mulai dari satu prinsip ini: anggap duplicate request pasti akan terjadi. Dari sana, desain idempotency Anda akan jauh lebih realistis dan operasional.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!