On Premise

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 .env instalasi (bukan mengandalkan default implisit di kode).
  • REDIS_QUEUE_PASSWORD terisi dan berbeda dari REDIS_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

ServicePerannya di antreanButuh 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
reportProdusen (trigger + scheduler) + konsumen topik camunda.*§D.3
notificationKonsumen notification.app.reminder§D.4
bpmProdusen saja (outbox privat discussion_outbox tidak lewat antrean ini)❌ tidak ada env antrean
camundaProdusen (JPA + trigger)❌ tidak ada env antrean
generate-test, simulation-beTidak 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.

EnvDefaultWajib diubah?Keterangan
REDIS_QUEUE_HOSTredis-queuetidakNama container Redis antrean.
REDIS_QUEUE_PORT6379tidak
REDIS_QUEUE_PASSWORDplaceholder🔴 YAKosong = --requirepass "" = Redis antrean tanpa auth. Harus berbeda dari REDIS_PASSWORD.
REDIS_QUEUE_DB0tidakInstance-nya sudah terpisah, jadi DB 0 aman di sini.
REDIS_QUEUE_MAXMEMORY128mbtergantung hostTurunan baseline: p95 payload 1.026 B × 121.354 baris pending ≈ 124,5 MB. asynq menyimpan seluruh payload di RAM.
AK_PORT_ASYNQMON8106tidakPort 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:8106

Menghapus 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.

EnvDefaultFungsi
RELAY_BATCH_SIZE200Baris per ronde poll.
RELAY_INTERVAL_SECONDS10Jeda antar ronde. 200 × 6/menit = 72.000 job/jam, ±9× puncak historis terburuk (7.985/jam).
RELAY_HEALTHCHECK_FILE/tmp/relay-healthyFile heartbeat tiap ronde; healthcheck compose membacanya (ambang 60 s = 6× interval).
RELAY_ASYNQ_TASK_RETENTION_SECONDS86400 (24 j)asynq.Retention umum — job selesai tetap terlihat di asynqmon. 0 = hapus begitu selesai.
RELAY_ASYNQ_TASK_RETENTION_REMINDER_SECONDS600 (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_DAYS30Sapuan retensi baris queue_jobs yang sudah completed.
QUEUE_JOB_RETENTION_SWEEP_INTERVAL_SECONDS3600Frekuensi sapuan retensi.
RELAY_STALE_REMINDER_THRESHOLD_SECONDS3600 (1 j)Batas basi grup reminder — lebih tua dari ini ditandai skipped, tidak di-enqueue.
RELAY_STALE_WEBHOOK_THRESHOLD_SECONDS86400 (24 j)Batas basi grup webhook.
RELAY_TIMEOUT_REPORT_SECONDS60asynq.Timeout untuk task yang masuk queue report.
RELAY_TIMEOUT_INTEGRATION_SECONDS30asynq.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)

EnvDefaultDi compose rilis?Fungsi
REDIS_QUEUE_HOST / PORT / PASSWORD / DBlihat §CDipakai hanya kalau salah satu *_CONSUMER di bawah = asynq.
ENABLE_TASK_REMINDER_WORKERfalseWorker task.active.reminder. Belum pernah hidup di rilis mana pun.
ENABLE_TASK_EVENT_NOTIFICATION_WORKERfalseWorker task.notification. Idem.
ENABLE_WEBHOOK_DELIVERY_WORKERtrueKebalikan dua di atas — hidup hari ini, hanya nilai false yang mematikannya. Set false di semua node kecuali satu bila di-scale horizontal.
WEBHOOK_DELIVERY_CONSUMERlegacylegacy = polling Postgres; asynq = handler asynq.
TASK_REMINDER_CONSUMERlegacy🔴 belumIdem, untuk task.active.reminder.
TASK_EVENT_NOTIFICATION_CONSUMERlegacy🔴 belumIdem, untuk task.notification.
WEBHOOK_ASYNQ_CONCURRENCY3🔴 belumHanya berlaku saat mode asynq.
WEBHOOK_ASYNQ_RATE_PER_SECOND10🔴 belumIdem.

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)

EnvDefaultDi compose rilis?Fungsi
ENABLE_QUEUE_PROCESSORtrueMenyalakan QueueProcessorService (konsumen topik camunda.*).
QUEUE_POLLING_INTERVAL30sMode legacy saja.
QUEUE_BATCH_SIZE10Mode legacy saja. Plafon kerasnya 10 baris/ronde.
QUEUE_WORKER_COUNT3Mode legacy saja.
ENABLE_TASK_REMINDERfalseScheduler task.active.reminder sisi report (produsen).
QUEUE_PROCESSOR_CONSUMERlegacy🔴 belumlegacy atau asynq.
QUEUE_PROCESSOR_ASYNQ_CONCURRENCY5🔴 belumMode asynq saja.
QUEUE_PROCESSOR_ASYNQ_RATE_PER_SECOND20🔴 belumMode asynq saja.
QUEUE_TENANT_IDkosong🔴 belumOpsional — batasi ke satu tenant (debugging).
QUEUE_TOPICSkosong🔴 belumOpsional — override daftar topik, dipisah koma.
REDIS_QUEUE_*lihat §C⚠️ lewat env_fileTidak 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)

EnvDefaultDi compose rilis?Fungsi
REDIS_QUEUE_HOST / PORT / PASSWORD / DBlihat §CDipakai hanya saat mode asynq.
APP_REMINDER_CONSUMERlegacylegacy = AppReminderWorker polling Postgres (perilaku hari ini); asynq = handler asynq.
APP_REMINDER_ASYNQ_CONCURRENCY3🔴 belumMode asynq saja.
APP_REMINDER_ASYNQ_RATE_PER_SECOND10🔴 belumIdem.

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=30

Menambahkan 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=0

Perbaikan 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".

  1. Isi kredensial. REDIS_QUEUE_PASSWORD terisi dan berbeda dari REDIS_PASSWORD; REDIS_QUEUE_MAXMEMORY disesuaikan 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
  2. Naikkan compose tanpa relay (--scale relay=0). Semua *_CONSUMER tetap legacy. Perilaku antrean belum berubah sama sekali.
  3. Siapkan cutover: index idx_queue_jobs_pending_relay dan penandaan backlog historis jadi skipped. Tanpa ini, relay pada boot pertama akan menyapu seluruh timbunan pending historis — 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;
  4. Nyalakan relay, konsumen masih legacyhanya sesudah langkah 3 dan hanya dalam jendela terjadwal, karena inilah titik di mana konsumen legacy mulai kelaparan (§F).
  5. Pindahkan konsumen satu per satu, satu topik per deploy: ubah *_CONSUMER ke asynq untuk 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 ditandai completed, 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

GejalaPenyebab yang paling mungkinCara memastikan
Container relay restart terus, log REDIS_QUEUE_HOST is emptyREDIS_QUEUE_HOST tidak sampai ke containerdocker 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 sekaliKembalikan ke legacy, atau isi REDIS_QUEUE_*
Webhook/notifikasi berhenti terkirim, tidak ada error di logRelay 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 punRelay belum jalan, atau topiknya tertahan gate ENABLE_*Buka asynqmon: antrean tujuan kosong = tidak ada yang masuk
Reminder terkirim dua kaliENABLE_TASK_REMINDER_WORKER berbeda antara container integration dan relayBandingkan printenv di kedua container — nilainya wajib identik
asynq Enqueue gagal, relay tidak menandai baris completedRedis 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 --maxmemoryAntrean digabung ke instance Redis cache dengan kebijakan allkeys-lruPastikan redis-queue adalah instance sendiri dan policy-nya noeviction
asynqmon tidak bisa dibuka dari LANDisengaja — port di-bind 127.0.0.1Akses lewat ssh -L 8106:127.0.0.1:8106 <host>

I. Rujukan

Dokumen teknis mendalam ada di repo masing-masing:

DokumenRepoIsi
docs/QUEUE_RELAY_USER_GUIDE.mdintegration-servicePanduan lengkap relay + tabel status flag per topik
docs/QUEUE_TOPIC_CONTRACT.mdintegration-serviceKontrak topic → asynq queue/task type
docs/CUTOVER_QUEUE_RELAY.mdintegration-serviceRunbook index + penandaan backlog historis
docs/APP_REMINDER_ASYNQ_ROLLOUT.mdnotification-serviceRollout APP_REMINDER_CONSUMER
docs/redis-queue-terpisah.mdrelease-alurkerja-localAlasan instance Redis terpisah
docs/runbook-asynqmon.mdrelease-alurkerja-localOperasi asynqmon, termasuk bahaya tombol delete