Env Konsolidasi Antrean (asynq)
Registry environment variable untuk konsolidasi queue_jobs ke asynq — service mana menerima env apa, mana yang sudah diteruskan compose rilis dan mana yang belum, nilai default tiap flag, serta urutan aman menyalakannya tanpa menghentikan pengiriman notifikasi/webhook.
A. Ringkasan & Definition of Done
Konsolidasi antrean memindahkan public.queue_jobs dari tabel yang dibaca empat service dengan empat semantik berbeda menjadi outbox dengan satu pembaca: container relay (biner ./relay milik integration-service) yang meneruskan tiap baris ke asynq/Redis, lalu tiap service mengonsumsinya sebagai asynq.Handler.
queue_jobs tidak dihapus. Penulis terbesarnya adalah trigger PL/pgSQL di dalam Postgres dan Camunda sisi Java — keduanya tidak punya jalan ke Redis, jadi tabelnya tetap ada dan turun pangkat jadi outbox.
Halaman ini adalah registry env-nya: apa yang harus ada di .env, service mana yang membacanya, dan mana yang hari ini belum diteruskan docker-compose.yml rilis.
Semua flag mode di halaman ini default legacy/false. Selama tidak diubah, perilaku antrean sama persis seperti sebelum ada asynq — kecuali satu hal yang bukan flag: container relay ikut jalan otomatis begitu docker compose up. Baca §F sebelum menaikkan rilis ini.
DoD halaman ini dianggap terpenuhi bila:
- Setiap env di §D ada nilainya di
.envinstalasi (bukan mengandalkan default implisit di kode). REDIS_QUEUE_PASSWORDterisi dan berbeda dariREDIS_PASSWORD.- Baris-baris di §E sudah ditambahkan, atau ketiadaannya disadari dan diterima.
- Keputusan di §F diambil eksplisit — bukan terlewat.
B. Peta service: siapa menulis, siapa membaca
| Service | Perannya di antrean | Butuh env antrean? |
|---|---|---|
| relay (container terpisah, image integration-service) | Satu-satunya pembaca queue_jobs setelah cutover | ✅ paling banyak — §D.1 |
| integration (API) | Produsen + konsumen task.active.reminder, task.notification, webhook_delivery | ✅ §D.2 |
| report | Produsen (trigger + scheduler) + konsumen topik camunda.* | ✅ §D.3 |
| notification | Konsumen notification.app.reminder | ✅ §D.4 |
| bpm | Produsen saja (outbox privat discussion_outbox tidak lewat antrean ini) | ❌ tidak ada env antrean |
| camunda | Produsen (JPA + trigger) | ❌ tidak ada env antrean |
| generate-test, simulation-be | Tidak terlibat — server asynq mati yang dulu ada di sini sudah dicabut | ❌ lihat §D.6 |
redis-queue adalah instance Redis terpisah dari redis cache. Bukan sekadar REDIS_DB yang berbeda: maxmemory dan maxmemory-policy adalah setelan per-instance, jadi berbagi instance berarti begitu cache dibatasi memorinya dengan allkeys-lru, key antrean ikut dibuang — job hilang tanpa satu pun error atau baris log.
C. Env global (dipakai lintas service)
Semua sudah ada di .env.example rilis.
| Env | Default | Wajib diubah? | Keterangan |
|---|---|---|---|
REDIS_QUEUE_HOST | redis-queue | tidak | Nama container Redis antrean. |
REDIS_QUEUE_PORT | 6379 | tidak | |
REDIS_QUEUE_PASSWORD | placeholder | 🔴 YA | Kosong = --requirepass "" = Redis antrean tanpa auth. Harus berbeda dari REDIS_PASSWORD. |
REDIS_QUEUE_DB | 0 | tidak | Instance-nya sudah terpisah, jadi DB 0 aman di sini. |
REDIS_QUEUE_MAXMEMORY | 128mb | tergantung host | Turunan baseline: p95 payload 1.026 B × 121.354 baris pending ≈ 124,5 MB. asynq menyimpan seluruh payload di RAM. |
AK_PORT_ASYNQMON | 8106 | tidak | Port asynqmon; compose sengaja mem-bind ke 127.0.0.1 saja. |
Cek RAM host sebelum menyalakan antrean. REDIS_QUEUE_MAXMEMORY=128mb diturunkan dari backlog nyata, tapi RCA OOM #137661 mencatat host produksi dengan RAM tersisa hanya ~484 MB dari 15 GB — 128 MB sendirian sudah >26% dari sisa itu. Turunkan angkanya untuk host semacam itu, dan jangan deploy antrean sebelum RCA-nya selesai atau RAM ditambah.
asynqmon tidak punya autentikasi bawaan dan payload yang ditampilkannya berisi snapshot variabel proses Camunda (data bisnis pelanggan). Ia tidak didaftarkan di nginx dan port-nya di-bind ke 127.0.0.1 — akses lewat SSH tunnel:
ssh -L 8106:127.0.0.1:8106 <host> # lalu buka http://127.0.0.1:8106Menghapus binding 127.0.0.1 itu supaya "lebih gampang diakses" berarti siapa pun di jaringan yang sama bisa membaca payload dan menghapus job produksi.
D. Env per service
D.1. Relay (container relay)
Relay adalah container tersendiri yang menjalankan image integration-service dengan command: ["./relay"] — sengaja bukan goroutine di dalam proses API: goroutine akan ter-duplikasi tiap replika API dan ikut mati tiap deploy/OOM/restart API.
Koneksi (wajib): DB_* dan REDIS_QUEUE_*. Relay menolak start (exit 1) kalau REDIS_QUEUE_HOST kosong — sengaja, supaya tidak ada relay yang jalan ke Redis yang tidak dikonfigurasi.
| Env | Default | Fungsi |
|---|---|---|
RELAY_BATCH_SIZE | 200 | Baris per ronde poll. |
RELAY_INTERVAL_SECONDS | 10 | Jeda antar ronde. 200 × 6/menit = 72.000 job/jam, ±9× puncak historis terburuk (7.985/jam). |
RELAY_HEALTHCHECK_FILE | /tmp/relay-healthy | File heartbeat tiap ronde; healthcheck compose membacanya (ambang 60 s = 6× interval). |
RELAY_ASYNQ_TASK_RETENTION_SECONDS | 86400 (24 j) | asynq.Retention umum — job selesai tetap terlihat di asynqmon. 0 = hapus begitu selesai. |
RELAY_ASYNQ_TASK_RETENTION_REMINDER_SECONDS | 600 (10 mnt) | Retensi khusus task.active.reminder. Bagian dari jaminan dedup, bukan kerapian: TaskID deterministik bebas dipakai ulang begitu task dihapus dari Redis. |
QUEUE_JOB_RETENTION_DAYS | 30 | Sapuan retensi baris queue_jobs yang sudah completed. |
QUEUE_JOB_RETENTION_SWEEP_INTERVAL_SECONDS | 3600 | Frekuensi sapuan retensi. |
RELAY_STALE_REMINDER_THRESHOLD_SECONDS | 3600 (1 j) | Batas basi grup reminder — lebih tua dari ini ditandai skipped, tidak di-enqueue. |
RELAY_STALE_WEBHOOK_THRESHOLD_SECONDS | 86400 (24 j) | Batas basi grup webhook. |
RELAY_TIMEOUT_REPORT_SECONDS | 60 | asynq.Timeout untuk task yang masuk queue report. |
RELAY_TIMEOUT_INTEGRATION_SECONDS | 30 | asynq.Timeout untuk queue integration. |
Dua ambang staleness di atas adalah asumsi, bukan kebijakan yang sudah diputuskan. Angkanya bisa diubah lewat env, tapi topik mana yang punya kebijakan staleness sama sekali ditentukan di kode (queue/staleness.go) dan butuh MR. Memutar ulang reminder berumur 6 jam untuk tugas yang sudah selesai adalah spam, bukan pemulihan — sementara topik camunda.* justru wajib diputar seluruhnya seberapa pun basinya, karena itu rekonstruksi state.
Gate consumer. Relay juga membaca ENABLE_TASK_REMINDER_WORKER dan ENABLE_TASK_EVENT_NOTIFICATION_WORKER — env yang sama persis dengan yang dibaca container integration, supaya relay dan konsumennya tidak pernah berbeda pendapat soal apakah sebuah topik "hidup". Kedua container wajib menerima nilai identik. Gate ini hanya berlaku untuk dua topik itu; semua topik lain di-relay tanpa syarat.
D.2. integration-service (container integration)
| Env | Default | Di compose rilis? | Fungsi |
|---|---|---|---|
REDIS_QUEUE_HOST / PORT / PASSWORD / DB | lihat §C | ✅ | Dipakai hanya kalau salah satu *_CONSUMER di bawah = asynq. |
ENABLE_TASK_REMINDER_WORKER | false | ✅ | Worker task.active.reminder. Belum pernah hidup di rilis mana pun. |
ENABLE_TASK_EVENT_NOTIFICATION_WORKER | false | ✅ | Worker task.notification. Idem. |
ENABLE_WEBHOOK_DELIVERY_WORKER | true | ✅ | Kebalikan dua di atas — hidup hari ini, hanya nilai false yang mematikannya. Set false di semua node kecuali satu bila di-scale horizontal. |
WEBHOOK_DELIVERY_CONSUMER | legacy | ✅ | legacy = polling Postgres; asynq = handler asynq. |
TASK_REMINDER_CONSUMER | legacy | 🔴 belum | Idem, untuk task.active.reminder. |
TASK_EVENT_NOTIFICATION_CONSUMER | legacy | 🔴 belum | Idem, untuk task.notification. |
WEBHOOK_ASYNQ_CONCURRENCY | 3 | 🔴 belum | Hanya berlaku saat mode asynq. |
WEBHOOK_ASYNQ_RATE_PER_SECOND | 10 | 🔴 belum | Idem. |
ENABLE_* dan *_CONSUMER adalah dua keputusan terpisah. ENABLE_* menjawab "worker ini boleh jalan atau tidak", *_CONSUMER menjawab "kalau jalan, lewat polling Postgres atau lewat asynq". Menyalakan dua worker yang selama ini mati adalah keputusan produk tersendiri — bukan bagian dari migrasi transport ini, karena tidak ada baseline dual-run untuk membandingkannya.
D.3. report-service (container report)
| Env | Default | Di compose rilis? | Fungsi |
|---|---|---|---|
ENABLE_QUEUE_PROCESSOR | true | ✅ | Menyalakan QueueProcessorService (konsumen topik camunda.*). |
QUEUE_POLLING_INTERVAL | 30s | ✅ | Mode legacy saja. |
QUEUE_BATCH_SIZE | 10 | ✅ | Mode legacy saja. Plafon kerasnya 10 baris/ronde. |
QUEUE_WORKER_COUNT | 3 | ✅ | Mode legacy saja. |
ENABLE_TASK_REMINDER | false | ✅ | Scheduler task.active.reminder sisi report (produsen). |
QUEUE_PROCESSOR_CONSUMER | legacy | 🔴 belum | legacy atau asynq. |
QUEUE_PROCESSOR_ASYNQ_CONCURRENCY | 5 | 🔴 belum | Mode asynq saja. |
QUEUE_PROCESSOR_ASYNQ_RATE_PER_SECOND | 20 | 🔴 belum | Mode asynq saja. |
QUEUE_TENANT_ID | kosong | 🔴 belum | Opsional — batasi ke satu tenant (debugging). |
QUEUE_TOPICS | kosong | 🔴 belum | Opsional — override daftar topik, dipisah koma. |
REDIS_QUEUE_* | lihat §C | ⚠️ lewat env_file | Tidak ada di blok environment: report, tapi tetap sampai karena service ini memuat env_file: .env. |
Typo ENBALE_QUEUE_PROCESSOR yang sempat load-bearing. docker-compose.yml historis menyetel ENBALE_QUEUE_PROCESSOR (huruf tertukar), dan report-service/main.go menerima kedua ejaan sekaligus supaya instalasi lama tidak mati. Ejaan yang benar sudah diperbaiki di compose rilis. Kalau instalasi Anda masih membawa .env lama, pakai ENABLE_QUEUE_PROCESSOR — dan jangan hapus dukungan ejaan salah di kode tanpa memastikan tidak ada .env lama yang masih memakainya.
D.4. notification-service (container notification)
| Env | Default | Di compose rilis? | Fungsi |
|---|---|---|---|
REDIS_QUEUE_HOST / PORT / PASSWORD / DB | lihat §C | ✅ | Dipakai hanya saat mode asynq. |
APP_REMINDER_CONSUMER | legacy | ✅ | legacy = AppReminderWorker polling Postgres (perilaku hari ini); asynq = handler asynq. |
APP_REMINDER_ASYNQ_CONCURRENCY | 3 | 🔴 belum | Mode asynq saja. |
APP_REMINDER_ASYNQ_RATE_PER_SECOND | 10 | 🔴 belum | Idem. |
AppReminderWorker hidup hari ini tanpa flag enable sama sekali — jadi tidak ada ENABLE_... untuk service ini, hanya flag transport di atas.
D.5. bpm-service & camunda — tidak ada env antrean
Keduanya produsen queue_jobs dan tidak mengonsumsi apa pun dari asynq. Tidak ada REDIS_QUEUE_*, tidak ada flag consumer. Worker milik bpm (discussion_outbox, discussion_status_sync, sla) memakai outbox privatnya sendiri dan tidak tersentuh konsolidasi ini.
D.6. generate-test & simulation — tidak ada env antrean
Kedua service ini dulu menjalankan asynq.Server dengan ServeMux kosong — nol handler, nol enqueue — di DB Redis yang sama dengan cache aplikasi, dan dengan nama antrean yang identik satu sama lain. Server mati itu sudah dicabut.
CACHE_PROVIDER / CACHE_HOST / CACHE_PORT / CACHE_PASSWORD / CACHE_DB / CACHE_ENABLED tetap ada dan tetap dipakai — itu cache aplikasi sungguhan (UserResolver, DriveAccessMiddleware), bukan sisa antrean. Jangan hapus.
E. Yang belum diteruskan compose rilis
Semua env bertanda 🔴 di §D punya default yang benar di kode, jadi ketiadaannya tidak merusak apa pun hari ini. Yang hilang adalah kemampuan operator mengubahnya lewat .env — untuk mengubahnya sekarang, docker-compose.yml harus diedit langsung. Selama semuanya masih legacy, ini belum menghambat; ia menghambat tepat pada saat cutover, ketika angka-angka itu justru perlu diputar.
Tambahkan ke .env (dan ke blok environment: service yang bersangkutan bila instalasi Anda tidak memakai env_file: .env):
# --- integration-service (container `integration`) ---
TASK_REMINDER_CONSUMER=legacy
TASK_EVENT_NOTIFICATION_CONSUMER=legacy
WEBHOOK_ASYNQ_CONCURRENCY=3
WEBHOOK_ASYNQ_RATE_PER_SECOND=10
# --- report-service (container `report`) ---
QUEUE_PROCESSOR_CONSUMER=legacy
QUEUE_PROCESSOR_ASYNQ_CONCURRENCY=5
QUEUE_PROCESSOR_ASYNQ_RATE_PER_SECOND=20
# --- notification-service (container `notification`) ---
APP_REMINDER_ASYNQ_CONCURRENCY=3
APP_REMINDER_ASYNQ_RATE_PER_SECOND=10
# --- relay (container `relay`) ---
RELAY_ASYNQ_TASK_RETENTION_SECONDS=86400
RELAY_ASYNQ_TASK_RETENTION_REMINDER_SECONDS=600
QUEUE_JOB_RETENTION_DAYS=30
QUEUE_JOB_RETENTION_SWEEP_INTERVAL_SECONDS=3600
RELAY_STALE_REMINDER_THRESHOLD_SECONDS=3600
RELAY_STALE_WEBHOOK_THRESHOLD_SECONDS=86400
RELAY_TIMEOUT_REPORT_SECONDS=60
RELAY_TIMEOUT_INTEGRATION_SECONDS=30Menambahkan baris ke .env saja cukup untuk service yang memuat env_file: .env (integration, relay, report, notification semuanya memuatnya). Blok environment: hanya menimpa nilainya. Tapi perhatikan satu jebakan: Compose tidak menginterpolasi isi env_file, jadi nilai bernested ${...} di sana akan ter-pass mentah ke dalam container — tulis nilai literal.
F. Container relay jalan otomatis — baca sebelum deploy
Service relay di docker-compose.yml tidak punya flag enable dan tidak punya profiles: — ia ikut jalan pada docker compose up biasa, dan REDIS_QUEUE_HOST sudah di-hardcode redis-queue di bloknya. Begitu ia jalan, ia mulai menguras queue_jobs.
Masalahnya: relay bersifat table-wide, bukan per-topik. Gate ConsumerEnabledForTopic hanya menahan dua topik (task.active.reminder, task.notification). Semua topik lain — webhook_delivery, notification.app.reminder, seluruh camunda.*/report.* — akan di-enqueue ke Redis dan barisnya ditandai completed, sementara konsumen mereka masih legacy dan mencari baris pending yang tidak akan pernah ada lagi.
Gejalanya bukan error. Webhook berhenti terkirim, tabel pivot Camunda berhenti terisi, log bersih, queue_jobs terlihat sehat karena semuanya completed.
Sampai cutover benar-benar dijalankan, jalankan compose tanpa relay:
docker compose up -d --scale relay=0Perbaikan permanennya adalah menambahkan profiles: pada service relay di release-alurkerja-local sehingga ia hanya ikut naik saat profil antrean diaktifkan — layak jadi tiket tersendiri di repo itu.
G. Urutan aman menyalakan
Tiap langkah punya verifikasi. Jangan simpulkan "berhasil" dari "tidak ada yang rusak".
- Isi kredensial.
REDIS_QUEUE_PASSWORDterisi dan berbeda dariREDIS_PASSWORD;REDIS_QUEUE_MAXMEMORYdisesuaikan RAM host.docker compose up -d redis-queue docker compose exec redis-queue redis-cli -a "$REDIS_QUEUE_PASSWORD" config get maxmemory maxmemory-policy # harus: maxmemory sesuai .env, maxmemory-policy noeviction - Naikkan compose tanpa relay (
--scale relay=0). Semua*_CONSUMERtetaplegacy. Perilaku antrean belum berubah sama sekali. - Siapkan cutover: index
idx_queue_jobs_pending_relaydan penandaan backlog historis jadiskipped. Tanpa ini, relay pada boot pertama akan menyapu seluruh timbunanpendinghistoris — termasuk topik yang memang tidak pernah punya konsumen — langsung ke RAM Redis.SELECT topic, status, count(*) FROM public.queue_jobs GROUP BY 1,2 ORDER BY 3 DESC; - Nyalakan relay, konsumen masih
legacy— hanya sesudah langkah 3 dan hanya dalam jendela terjadwal, karena inilah titik di mana konsumen legacy mulai kelaparan (§F). - Pindahkan konsumen satu per satu, satu topik per deploy: ubah
*_CONSUMERkeasynquntuk satu topik, verifikasi lewat asynqmon bahwa antreannya benar-benar terkuras, baru lanjut ke topik berikutnya.
Kontrak lintas-repo tanpa pemeriksaan runtime. Tidak ada satu pun kode yang mendeteksi kalau relay dan konsumennya berbeda pendapat. Dua arah gagalnya sama-sama diam:
- relay mengirim ke asynq, konsumen masih
legacy→ baris ditandaicompleted, tidak ada yang mendengarkan Redis → event hilang. - konsumen
asynq, relay belum/tidak mengirim topik itu → konsumen menyala tapi Redis-nya kosong selamanya → idle tanpa error.
Murni disiplin deploy operator.
H. Troubleshooting
| Gejala | Penyebab yang paling mungkin | Cara memastikan |
|---|---|---|
Container relay restart terus, log REDIS_QUEUE_HOST is empty | REDIS_QUEUE_HOST tidak sampai ke container | docker compose exec relay printenv | grep REDIS_QUEUE |
Log ... but REDIS_QUEUE_HOST is empty -- refusing to start di report/notification | *_CONSUMER=asynq tapi Redis antrean belum dikonfigurasi. Service menolak start konsumen — sengaja, topiknya jadi tidak terkuras sama sekali | Kembalikan ke legacy, atau isi REDIS_QUEUE_* |
| Webhook/notifikasi berhenti terkirim, tidak ada error di log | Relay jalan sementara konsumennya masih legacy (§F) | SELECT status, count(*) FROM queue_jobs GROUP BY 1; — kalau completed melonjak dan pending nol, relay sudah menguras |
| Konsumen asynq menyala tapi tidak pernah memproses apa pun | Relay belum jalan, atau topiknya tertahan gate ENABLE_* | Buka asynqmon: antrean tujuan kosong = tidak ada yang masuk |
| Reminder terkirim dua kali | ENABLE_TASK_REMINDER_WORKER berbeda antara container integration dan relay | Bandingkan printenv di kedua container — nilainya wajib identik |
asynq Enqueue gagal, relay tidak menandai baris completed | Redis antrean penuh (maxmemory tercapai, noeviction menolak write) | redis-cli info memory; naikkan REDIS_QUEUE_MAXMEMORY hanya bila RAM host memungkinkan |
Job hilang tanpa jejak setelah menambah --maxmemory | Antrean digabung ke instance Redis cache dengan kebijakan allkeys-lru | Pastikan redis-queue adalah instance sendiri dan policy-nya noeviction |
| asynqmon tidak bisa dibuka dari LAN | Disengaja — port di-bind 127.0.0.1 | Akses lewat ssh -L 8106:127.0.0.1:8106 <host> |
I. Rujukan
Dokumen teknis mendalam ada di repo masing-masing:
| Dokumen | Repo | Isi |
|---|---|---|
docs/QUEUE_RELAY_USER_GUIDE.md | integration-service | Panduan lengkap relay + tabel status flag per topik |
docs/QUEUE_TOPIC_CONTRACT.md | integration-service | Kontrak topic → asynq queue/task type |
docs/CUTOVER_QUEUE_RELAY.md | integration-service | Runbook index + penandaan backlog historis |
docs/APP_REMINDER_ASYNQ_ROLLOUT.md | notification-service | Rollout APP_REMINDER_CONSUMER |
docs/redis-queue-terpisah.md | release-alurkerja-local | Alasan instance Redis terpisah |
docs/runbook-asynqmon.md | release-alurkerja-local | Operasi asynqmon, termasuk bahaya tombol delete |
Konsolidasi JWT via Proxy
Runbook aktivasi dan rollback RESOLVE_JWT_VIA_PROXY & PROXY_RESOLVE_JWT — dua mode verifikasi identitas, kontrak header X-User-*/X-Tenant-*, cara menyetel dan merotasi GATEWAY_SHARED_SECRET, serta troubleshooting gejala ke penyebab.
Health Monitoring
Implementasi health monitoring di Alurkerja
