[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-29-semaphore-async-rust-deadlock-fire-and-forget":3},{"article":4,"author":53},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":16,"meta_description":17,"focus_keyword":18,"og_image":19,"canonical_url":19,"robots_meta":20,"created_at":15,"updated_at":15,"tags":21,"category_name":31,"related_articles":32},"d2000000-0000-0000-0000-000000000301","a0000000-0000-0000-0000-000000000026","Deep EVM #29: Semaphore di Async Rust — Berburu Deadlock dan Pola Fire-and-Forget","deep-evm-29-semaphore-async-rust-deadlock-fire-and-forget","Pembahasan mendalam tokio::sync::Semaphore untuk kontrol backpressure, pola write fire-and-forget, diagnosis deadlock dengan tracing dan tokio-console, serta solusi produksi menggunakan RAII permit dan acquire timeout.","## Mengapa Semaphore di Async Rust\n\nKetika Anda menjalankan pipeline throughput tinggi — bot MEV yang memproses 180.000 rantai arbitrase per block, server API yang menangani 10.000 request bersamaan, atau job ETL yang menulis jutaan baris — Anda pasti akan menabrak batas sumber daya. Connection pool database habis. Provider RPC membatasi rate Anda. Memory membengkak karena Anda men-spawn 50.000 task tokio, masing-masing memegang data sebesar satu rantai.\n\nPendekatan naif adalah concurrency tanpa batas: `tokio::spawn` untuk setiap unit kerja dan berharap runtime mengurusnya. Tidak demikian. Dalam produksi, kami mengamati penggunaan memory 15,4 GB dari 2,7 juta task yang di-spawn, masing-masing memegang `Vec\u003CHop>` plus konteks simulasi. Perbaikannya adalah concurrency berbatch dengan backpressure berbasis semaphore, yang menurunkan memory ke 0,8 GB.\n\nSemaphore adalah primitif yang tepat ketika Anda perlu membatasi jumlah operasi bersamaan tanpa men-serialize mereka sepenuhnya. Tidak seperti mutex (yang mengizinkan tepat satu), semaphore mengizinkan N aksesor bersamaan. Ini membuatnya sempurna untuk:\n\n- **Concurrency write database**: batasi ke ukuran connection pool (misalnya 20 write bersamaan)\n- **Rate limiting RPC**: batasi request keluar untuk menghindari respons 429 dari provider\n- **Backpressure memory**: cegah spawning task tanpa batas dengan gating pada permit yang tersedia\n- **Pemrosesan batch**: kontrol berapa banyak batch simulasi yang berjalan paralel terhadap shared node\n\n## Dasar-Dasar tokio::sync::Semaphore\n\n`tokio::sync::Semaphore` adalah counting semaphore yang dirancang untuk kode async. Ia mempertahankan counter internal dari permit yang tersedia. Task mengakuisisi permit sebelum melanjutkan dan melepasnya ketika selesai.\n\n```rust\nuse std::sync::Arc;\nuse tokio::sync::Semaphore;\n\n\u002F\u002F Izinkan hingga 20 write database bersamaan\nlet semaphore = Arc::new(Semaphore::new(20));\n\nfor chain in chains_to_persist {\n    let sem = semaphore.clone();\n    let db_pool = db_pool.clone();\n\n    tokio::spawn(async move {\n        \u002F\u002F Akuisisi permit — memblokir jika semua 20 sedang digunakan\n        let _permit = sem.acquire().await.unwrap();\n\n        \u002F\u002F Lakukan write — permit dipegang untuk scope ini\n        sqlx::query(\"UPDATE chains SET profit = $1 WHERE id = $2\")\n            .bind(chain.profit)\n            .bind(chain.id)\n            .execute(&db_pool)\n            .await\n            .ok();\n\n        \u002F\u002F _permit di-drop di sini — otomatis dilepas\n    });\n}\n```\n\nAPI surface kunci:\n\n- `Semaphore::new(permits)` — buat dengan N permit\n- `acquire(&self)` — async, menunggu sampai permit tersedia, mengembalikan `SemaphorePermit` (guard RAII)\n- `try_acquire(&self)` — non-async, mengembalikan `Err(TryAcquireError)` langsung jika tidak ada permit\n- `acquire_owned(self: Arc\u003CSelf>)` — mengembalikan `OwnedSemaphorePermit` yang memiliki `Arc`, berguna ketika permit harus outlive borrow\n- `add_permits(&self, n)` — tingkatkan jumlah permit secara dinamis\n- `close(&self)` — tutup semaphore; semua panggilan `acquire` yang menunggu mengembalikan `Err(AcquireError)`\n- `available_permits(&self)` — inspeksi jumlah saat ini (berguna untuk metrik)\n\nPilihan desain kritis: `acquire()` mengembalikan guard RAII. Ketika guard di-drop, permit dilepas. Ini berarti Anda mendapat cleanup otomatis bahkan pada panic, early return, dan bailout operator `?` — selama guard hidup di stack.\n\n## Pola Write Fire-and-Forget\n\nDalam sistem throughput tinggi, Anda sering ingin mempersist data tanpa memblokir hot path. Polanya: spawn task background yang mengakuisisi permit semaphore, melakukan write, dan melepas. Pemanggil tidak menunggu hasilnya.\n\nDalam sistem yang dirancang dengan baik, ini diimplementasikan sebagai decorator yang membungkus service storage inner:\n\n```rust\nuse std::sync::Arc;\nuse tokio::sync::Semaphore;\n\npub struct AsyncChainStore\u003CS: ChainStore> {\n    inner: Arc\u003CS>,\n    semaphore: Arc\u003CSemaphore>,\n}\n\nimpl\u003CS: ChainStore + Send + Sync + 'static> AsyncChainStore\u003CS> {\n    pub fn new(inner: Arc\u003CS>, max_concurrent_writes: usize) -> Self {\n        Self {\n            inner,\n            semaphore: Arc::new(Semaphore::new(max_concurrent_writes)),\n        }\n    }\n\n    \u002F\u002F\u002F Fire-and-forget: persist profit rantai tanpa memblokir pemanggil.\n    \u002F\u002F\u002F Backpressure ditegakkan oleh semaphore — jika semua permit\n    \u002F\u002F\u002F terpakai, task yang di-spawn menunggu, tetapi pemanggil kembali segera.\n    pub fn save_profits_async(&self, chains: Vec\u003CChainProfit>) {\n        let inner = self.inner.clone();\n        let sem = self.semaphore.clone();\n\n        tokio::spawn(async move {\n            \u002F\u002F Akuisisi permit — di sinilah backpressure terjadi\n            let _permit = match sem.acquire().await {\n                Ok(p) => p,\n                Err(_) => {\n                    tracing::warn!(\"semaphore ditutup, membuang write\");\n                    return;\n                }\n            };\n\n            if let Err(e) = inner.batch_update_profits(&chains).await {\n                tracing::error!(\n                    error = %e,\n                    count = chains.len(),\n                    \"write fire-and-forget gagal\"\n                );\n            }\n            \u002F\u002F _permit di-drop di sini — task berikutnya yang mengantre melanjutkan\n        });\n    }\n}\n```\n\nPola decorator ini menjaga `ChainStore` inner tetap murni — ia tidak tahu apa-apa tentang kontrol concurrency. Decorator menangani manajemen semaphore, logging error, dan spawning task. ServiceLocator menghubungkan mereka:\n\n```rust\n\u002F\u002F Di locator\u002Fmod.rs\nlet chain_store = Arc::new(PgChainStore::new(db_pool.clone()));\nlet async_chain_store = AsyncChainStore::new(chain_store, 20);\n```\n\nMengapa tidak cukup menggunakan batas koneksi bawaan database pool? Karena semaphore memberikan knob terpisah. Pool Anda mungkin memiliki 50 koneksi, tetapi Anda ingin write fire-and-forget menggunakan paling banyak 20, menyisakan 30 untuk read yang sensitif latensi. Semaphore menegakkan partisi ini di tingkat aplikasi.\n\n## Skenario Deadlock\n\nDeadlock semaphore berbahaya karena tidak menyebabkan panic atau error — program hanya berhenti membuat progress. Berikut pola yang kami temui di produksi.\n\n### Skenario 1: Permit Tidak Dilepas pada Early Return\n\n```rust\nasync fn process_batch(sem: &Semaphore, batch: &[Chain]) -> Result\u003C()> {\n    let permit = sem.acquire().await?;\n\n    \u002F\u002F Early return jika batch kosong — tetapi permit masih dipegang!\n    if batch.is_empty() {\n        return Ok(()); \u002F\u002F permit di-drop di sini — sebenarnya tidak masalah\n    }\n\n    \u002F\u002F Bahaya sebenarnya: menyimpan permit di struct yang outlive scope\n    let ctx = ProcessingContext {\n        permit: Some(permit), \u002F\u002F permit dipindahkan ke struct\n        batch,\n    };\n\n    \u002F\u002F Jika process() menyimpan ctx di tempat yang berumur panjang, permit bocor\n    process(ctx).await?;\n    Ok(())\n}\n```\n\nGuard RAII melindungi Anda dari kebanyakan early return. Bahaya datang ketika Anda memindahkan permit ke struct yang keluar dari lifetime yang diharapkan — disimpan di `HashMap`, dikirim melalui channel, atau dipegang di `static`.\n\n### Skenario 2: Nested Acquire (Self-Deadlock)\n\n```rust\nasync fn simulate_and_persist(\n    sem: &Semaphore,\n    chain: &Chain,\n) -> Result\u003C()> {\n    let _outer = sem.acquire().await?; \u002F\u002F Mengambil 1 dari N permit\n\n    let result = simulate(chain).await?;\n\n    \u002F\u002F BUG: mengakuisisi semaphore YANG SAMA di dalam permit yang dipegang\n    let _inner = sem.acquire().await?; \u002F\u002F Jika N=1, deadlock. Jika N>1, mengurangi throughput\n    persist(result).await?;\n\n    Ok(())\n}\n```\n\nDengan `N=1`, ini deadlock instan. Dengan `N>1`, bekerja sampai beban meningkat dan semua permit dipegang oleh task yang menunggu inner acquire mereka. Perbaikan: gunakan semaphore terpisah untuk concern terpisah, atau restrukturisasi untuk menghindari akuisisi bersarang.\n\n### Skenario 3: Permit Dipegang Melintasi Await Point di Select\n\n```rust\nasync fn process_with_timeout(sem: &Semaphore) -> Result\u003C()> {\n    let permit = sem.acquire().await?;\n\n    tokio::select! {\n        result = do_work() => {\n            result?;\n        }\n        _ = tokio::time::sleep(Duration::from_secs(30)) => {\n            tracing::warn!(\"timeout, tetapi permit masih dipegang sampai drop\");\n        }\n    }\n    \u002F\u002F permit di-drop di sini — ini sebenarnya benar\n    \u002F\u002F TETAPI: jika do_work() men-spawn sub-task yang menangkap permit,\n    \u002F\u002F pembatalan do_work() TIDAK membatalkan sub-task\n    Ok(())\n}\n```\n\nMakro `select!` membatalkan branch yang kalah dengan men-drop future-nya, tetapi task yang di-spawn di dalam future tersebut terus berjalan. Jika task tersebut menangkap referensi ke permit, permit tidak dilepas ketika diharapkan.\n\n## Mendiagnosis Deadlock Semaphore\n\nKetika sistem Anda berhenti membuat progress, bagaimana mengidentifikasi deadlock semaphore versus dependency yang lambat?\n\n### Diagnosis Berbasis Tracing\n\nInstrumentasi acquire\u002Frelease dengan structured logging:\n\n```rust\nlet permits_before = semaphore.available_permits();\ntracing::debug!(\n    available = permits_before,\n    \"mengakuisisi permit semaphore\"\n);\n\nlet _permit = semaphore.acquire().await?;\n\ntracing::debug!(\n    available = semaphore.available_permits(),\n    \"permit semaphore diakuisisi\"\n);\n```\n\nJika log Anda menunjukkan \"mengakuisisi\" tetapi tidak pernah \"diakuisisi\", semua permit dipegang di suatu tempat.\n\n### Metrik Prometheus\n\nEkspos gauge metric untuk permit yang tersedia:\n\n```rust\nuse metrics::gauge;\n\ngauge!(\"semaphore_available_permits\", semaphore.available_permits() as f64);\n```\n\nGauge yang turun ke nol dan tetap di sana adalah deadlock. Gauge yang berfluktuasi dekat nol adalah contention.\n\n### tokio-console\n\n`tokio-console` adalah alat diagnostik yang terhubung ke aplikasi tokio yang berjalan dan menampilkan state task secara real time:\n\n```bash\ncargo add --dev console-subscriber\n```\n\n```rust\n#[cfg(debug_assertions)]\nconsole_subscriber::init();\n```\n\nJalankan `tokio-console` di terminal lain dan cari task yang stuck di state \"Idle\" pada semaphore acquire.\n\n## Solusi Produksi\n\n### Solusi 1: Selalu Gunakan OwnedSemaphorePermit dengan Arc\n\nKetika spawning task, lebih baik gunakan `acquire_owned()` daripada `acquire()`. Varian owned mengambil `Arc\u003CSemaphore>` dan mengembalikan permit yang tidak meminjam semaphore — ia memiliki clone dari `Arc`:\n\n```rust\nlet semaphore = Arc::new(Semaphore::new(20));\n\nfor item in items {\n    let permit = semaphore.clone().acquire_owned().await?;\n\n    tokio::spawn(async move {\n        \u002F\u002F permit dipindahkan ke task — tanpa masalah lifetime\n        do_work(item).await;\n        drop(permit); \u002F\u002F drop eksplisit untuk kejelasan\n    });\n}\n```\n\n### Solusi 2: Acquire dengan Timeout\n\nJangan pernah menunggu permit tanpa batas di produksi. Gunakan `tokio::time::timeout` untuk mendeteksi deadlock lebih awal:\n\n```rust\nuse tokio::time::{timeout, Duration};\n\nlet permit = timeout(\n    Duration::from_secs(30),\n    semaphore.acquire(),\n).await\n    .map_err(|_| anyhow!(\"acquire semaphore timeout — kemungkinan deadlock\"))?\n    .map_err(|_| anyhow!(\"semaphore ditutup\"))?;\n```\n\nKetika timeout menyala, log permit yang tersedia, jumlah task yang menunggu, dan stack trace.\n\n### Solusi 3: Structured Concurrency dengan JoinSet\n\nAlih-alih `tokio::spawn` tanpa batas, gunakan `tokio::task::JoinSet` untuk mempertahankan ownership dari task yang di-spawn dan padukan dengan semaphore:\n\n```rust\nuse tokio::task::JoinSet;\n\nlet semaphore = Arc::new(Semaphore::new(20));\nlet mut join_set = JoinSet::new();\n\nfor batch in batches {\n    let permit = semaphore.clone().acquire_owned().await?;\n    let db_pool = db_pool.clone();\n\n    join_set.spawn(async move {\n        let result = process_batch(&db_pool, &batch).await;\n        drop(permit); \u002F\u002F lepas sebelum JoinSet mengumpulkan\n        result\n    });\n}\n\n\u002F\u002F Drain semua task — kumpulkan error alih-alih kehilangan diam-diam\nwhile let Some(result) = join_set.join_next().await {\n    match result {\n        Ok(Ok(())) => {}\n        Ok(Err(e)) => tracing::error!(error = %e, \"pemrosesan batch gagal\"),\n        Err(e) => tracing::error!(error = %e, \"task panic\"),\n    }\n}\n```\n\nPola ini memastikan: (1) concurrency terbatas via semaphore, (2) tidak ada kegagalan task yang diam, (3) task induk tahu kapan semua anak selesai, (4) permit selalu dilepas meskipun task panic.\n\n### Solusi 4: Semaphore Terpisah untuk Concern Terpisah\n\nJangan pernah berbagi satu semaphore untuk operasi yang tidak terkait. Dalam pipeline MEV kami, kami menggunakan semaphore terpisah:\n\n```rust\npub struct ConcurrencyLimits {\n    \u002F\u002F\u002F Membatasi panggilan simulasi RPC bersamaan ke node\n    pub simulation: Arc\u003CSemaphore>,\n    \u002F\u002F\u002F Membatasi write database bersamaan untuk persistensi rantai\n    pub db_writes: Arc\u003CSemaphore>,\n    \u002F\u002F\u002F Membatasi handler langganan mempool bersamaan\n    pub mempool: Arc\u003CSemaphore>,\n}\n\nimpl ConcurrencyLimits {\n    pub fn new() -> Self {\n        Self {\n            simulation: Arc::new(Semaphore::new(4)),   \u002F\u002F node menangani 4 batch paralel\n            db_writes: Arc::new(Semaphore::new(20)),   \u002F\u002F 20 dari 50 koneksi pool\n            mempool: Arc::new(Semaphore::new(100)),    \u002F\u002F 100 handler tx bersamaan\n        }\n    }\n}\n```\n\nIni menghilangkan deadlock nested acquire sepenuhnya — task mengakuisisi permit `simulation`, lalu permit `db_writes`, dan ini adalah pool independen.\n\n## Performa: Overhead Semaphore\n\nApakah semaphore itu sendiri bottleneck? Dalam praktik, tidak. `tokio::sync::Semaphore` diimplementasikan dengan counter atomik dan linked list intrusif dari waiter. Mengakuisisi permit yang tidak dikontestasi adalah satu `fetch_sub` — nanosecond. Bahkan di bawah contention, overhead-nya adalah notifikasi waker (juga nanosecond) versus milidetik yang diambil I\u002FO aktual Anda.\n\nKami membenchmark overhead semaphore di pipeline kami:\n\n| Operasi | Tanpa Semaphore | Dengan Semaphore (20 permit) |\n|---------|-----------------|------------------------------|\n| 10.000 write DB | 1.340ms (semua bersamaan, error pool habis) | 1.580ms (terkontrol, zero error) |\n| 500 simulasi RPC | 8.200ms (node overload, timeout) | 9.100ms (4 sekaligus, zero timeout) |\n| Memory (2,7 juta task) | 15,4 GB | 0,8 GB (berbatch dengan JoinSet) |\n\nOverhead wall-clock 10-15% dapat diabaikan dibandingkan menghilangkan error, timeout, dan crash OOM.\n\n## Kesimpulan\n\nSemaphore di async Rust terlihat sederhana — `acquire`, lakukan kerja, drop permit. Kompleksitas muncul di produksi: permit yang bocor ke struct berumur panjang, nested acquire lintas call stack, permit yang dipegang melintasi batas pembatalan `select!`.\n\nPlaybook pertahanan:\n\n1. **Selalu** gunakan `acquire_owned()` ketika spawning task\n2. **Selalu** bungkus acquire dalam timeout\n3. **Jangan pernah** nest acquire pada semaphore yang sama\n4. **Pisahkan** semaphore untuk pool sumber daya yang berbeda\n5. **Instrumentasi** permit yang tersedia dengan metrik dan tracing\n6. **Gunakan JoinSet** alih-alih `tokio::spawn` tanpa batas untuk structured concurrency\n\nPola-pola ini telah bertahan melintasi jutaan block yang diproses, ratusan ribu task bersamaan, dan zero deadlock di produksi sejak mengadopsinya.","\u003Ch2 id=\"mengapa-semaphore-di-async-rust\">Mengapa Semaphore di Async Rust\u003C\u002Fh2>\n\u003Cp>Ketika Anda menjalankan pipeline throughput tinggi — bot MEV yang memproses 180.000 rantai arbitrase per block, server API yang menangani 10.000 request bersamaan, atau job ETL yang menulis jutaan baris — Anda pasti akan menabrak batas sumber daya. Connection pool database habis. Provider RPC membatasi rate Anda. Memory membengkak karena Anda men-spawn 50.000 task tokio, masing-masing memegang data sebesar satu rantai.\u003C\u002Fp>\n\u003Cp>Pendekatan naif adalah concurrency tanpa batas: \u003Ccode>tokio::spawn\u003C\u002Fcode> untuk setiap unit kerja dan berharap runtime mengurusnya. Tidak demikian. Dalam produksi, kami mengamati penggunaan memory 15,4 GB dari 2,7 juta task yang di-spawn, masing-masing memegang \u003Ccode>Vec&lt;Hop&gt;\u003C\u002Fcode> plus konteks simulasi. Perbaikannya adalah concurrency berbatch dengan backpressure berbasis semaphore, yang menurunkan memory ke 0,8 GB.\u003C\u002Fp>\n\u003Cp>Semaphore adalah primitif yang tepat ketika Anda perlu membatasi jumlah operasi bersamaan tanpa men-serialize mereka sepenuhnya. Tidak seperti mutex (yang mengizinkan tepat satu), semaphore mengizinkan N aksesor bersamaan. Ini membuatnya sempurna untuk:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Concurrency write database\u003C\u002Fstrong>: batasi ke ukuran connection pool (misalnya 20 write bersamaan)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Rate limiting RPC\u003C\u002Fstrong>: batasi request keluar untuk menghindari respons 429 dari provider\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Backpressure memory\u003C\u002Fstrong>: cegah spawning task tanpa batas dengan gating pada permit yang tersedia\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Pemrosesan batch\u003C\u002Fstrong>: kontrol berapa banyak batch simulasi yang berjalan paralel terhadap shared node\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"dasar-dasar-tokio-sync-semaphore\">Dasar-Dasar tokio::sync::Semaphore\u003C\u002Fh2>\n\u003Cp>\u003Ccode>tokio::sync::Semaphore\u003C\u002Fcode> adalah counting semaphore yang dirancang untuk kode async. Ia mempertahankan counter internal dari permit yang tersedia. Task mengakuisisi permit sebelum melanjutkan dan melepasnya ketika selesai.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use std::sync::Arc;\nuse tokio::sync::Semaphore;\n\n\u002F\u002F Izinkan hingga 20 write database bersamaan\nlet semaphore = Arc::new(Semaphore::new(20));\n\nfor chain in chains_to_persist {\n    let sem = semaphore.clone();\n    let db_pool = db_pool.clone();\n\n    tokio::spawn(async move {\n        \u002F\u002F Akuisisi permit — memblokir jika semua 20 sedang digunakan\n        let _permit = sem.acquire().await.unwrap();\n\n        \u002F\u002F Lakukan write — permit dipegang untuk scope ini\n        sqlx::query(\"UPDATE chains SET profit = $1 WHERE id = $2\")\n            .bind(chain.profit)\n            .bind(chain.id)\n            .execute(&amp;db_pool)\n            .await\n            .ok();\n\n        \u002F\u002F _permit di-drop di sini — otomatis dilepas\n    });\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>API surface kunci:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>Semaphore::new(permits)\u003C\u002Fcode> — buat dengan N permit\u003C\u002Fli>\n\u003Cli>\u003Ccode>acquire(&amp;self)\u003C\u002Fcode> — async, menunggu sampai permit tersedia, mengembalikan \u003Ccode>SemaphorePermit\u003C\u002Fcode> (guard RAII)\u003C\u002Fli>\n\u003Cli>\u003Ccode>try_acquire(&amp;self)\u003C\u002Fcode> — non-async, mengembalikan \u003Ccode>Err(TryAcquireError)\u003C\u002Fcode> langsung jika tidak ada permit\u003C\u002Fli>\n\u003Cli>\u003Ccode>acquire_owned(self: Arc&lt;Self&gt;)\u003C\u002Fcode> — mengembalikan \u003Ccode>OwnedSemaphorePermit\u003C\u002Fcode> yang memiliki \u003Ccode>Arc\u003C\u002Fcode>, berguna ketika permit harus outlive borrow\u003C\u002Fli>\n\u003Cli>\u003Ccode>add_permits(&amp;self, n)\u003C\u002Fcode> — tingkatkan jumlah permit secara dinamis\u003C\u002Fli>\n\u003Cli>\u003Ccode>close(&amp;self)\u003C\u002Fcode> — tutup semaphore; semua panggilan \u003Ccode>acquire\u003C\u002Fcode> yang menunggu mengembalikan \u003Ccode>Err(AcquireError)\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>available_permits(&amp;self)\u003C\u002Fcode> — inspeksi jumlah saat ini (berguna untuk metrik)\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Pilihan desain kritis: \u003Ccode>acquire()\u003C\u002Fcode> mengembalikan guard RAII. Ketika guard di-drop, permit dilepas. Ini berarti Anda mendapat cleanup otomatis bahkan pada panic, early return, dan bailout operator \u003Ccode>?\u003C\u002Fcode> — selama guard hidup di stack.\u003C\u002Fp>\n\u003Ch2 id=\"pola-write-fire-and-forget\">Pola Write Fire-and-Forget\u003C\u002Fh2>\n\u003Cp>Dalam sistem throughput tinggi, Anda sering ingin mempersist data tanpa memblokir hot path. Polanya: spawn task background yang mengakuisisi permit semaphore, melakukan write, dan melepas. Pemanggil tidak menunggu hasilnya.\u003C\u002Fp>\n\u003Cp>Dalam sistem yang dirancang dengan baik, ini diimplementasikan sebagai decorator yang membungkus service storage inner:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use std::sync::Arc;\nuse tokio::sync::Semaphore;\n\npub struct AsyncChainStore&lt;S: ChainStore&gt; {\n    inner: Arc&lt;S&gt;,\n    semaphore: Arc&lt;Semaphore&gt;,\n}\n\nimpl&lt;S: ChainStore + Send + Sync + 'static&gt; AsyncChainStore&lt;S&gt; {\n    pub fn new(inner: Arc&lt;S&gt;, max_concurrent_writes: usize) -&gt; Self {\n        Self {\n            inner,\n            semaphore: Arc::new(Semaphore::new(max_concurrent_writes)),\n        }\n    }\n\n    \u002F\u002F\u002F Fire-and-forget: persist profit rantai tanpa memblokir pemanggil.\n    \u002F\u002F\u002F Backpressure ditegakkan oleh semaphore — jika semua permit\n    \u002F\u002F\u002F terpakai, task yang di-spawn menunggu, tetapi pemanggil kembali segera.\n    pub fn save_profits_async(&amp;self, chains: Vec&lt;ChainProfit&gt;) {\n        let inner = self.inner.clone();\n        let sem = self.semaphore.clone();\n\n        tokio::spawn(async move {\n            \u002F\u002F Akuisisi permit — di sinilah backpressure terjadi\n            let _permit = match sem.acquire().await {\n                Ok(p) =&gt; p,\n                Err(_) =&gt; {\n                    tracing::warn!(\"semaphore ditutup, membuang write\");\n                    return;\n                }\n            };\n\n            if let Err(e) = inner.batch_update_profits(&amp;chains).await {\n                tracing::error!(\n                    error = %e,\n                    count = chains.len(),\n                    \"write fire-and-forget gagal\"\n                );\n            }\n            \u002F\u002F _permit di-drop di sini — task berikutnya yang mengantre melanjutkan\n        });\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Pola decorator ini menjaga \u003Ccode>ChainStore\u003C\u002Fcode> inner tetap murni — ia tidak tahu apa-apa tentang kontrol concurrency. Decorator menangani manajemen semaphore, logging error, dan spawning task. ServiceLocator menghubungkan mereka:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F Di locator\u002Fmod.rs\nlet chain_store = Arc::new(PgChainStore::new(db_pool.clone()));\nlet async_chain_store = AsyncChainStore::new(chain_store, 20);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Mengapa tidak cukup menggunakan batas koneksi bawaan database pool? Karena semaphore memberikan knob terpisah. Pool Anda mungkin memiliki 50 koneksi, tetapi Anda ingin write fire-and-forget menggunakan paling banyak 20, menyisakan 30 untuk read yang sensitif latensi. Semaphore menegakkan partisi ini di tingkat aplikasi.\u003C\u002Fp>\n\u003Ch2 id=\"skenario-deadlock\">Skenario Deadlock\u003C\u002Fh2>\n\u003Cp>Deadlock semaphore berbahaya karena tidak menyebabkan panic atau error — program hanya berhenti membuat progress. Berikut pola yang kami temui di produksi.\u003C\u002Fp>\n\u003Ch3>Skenario 1: Permit Tidak Dilepas pada Early Return\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">async fn process_batch(sem: &amp;Semaphore, batch: &amp;[Chain]) -&gt; Result&lt;()&gt; {\n    let permit = sem.acquire().await?;\n\n    \u002F\u002F Early return jika batch kosong — tetapi permit masih dipegang!\n    if batch.is_empty() {\n        return Ok(()); \u002F\u002F permit di-drop di sini — sebenarnya tidak masalah\n    }\n\n    \u002F\u002F Bahaya sebenarnya: menyimpan permit di struct yang outlive scope\n    let ctx = ProcessingContext {\n        permit: Some(permit), \u002F\u002F permit dipindahkan ke struct\n        batch,\n    };\n\n    \u002F\u002F Jika process() menyimpan ctx di tempat yang berumur panjang, permit bocor\n    process(ctx).await?;\n    Ok(())\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Guard RAII melindungi Anda dari kebanyakan early return. Bahaya datang ketika Anda memindahkan permit ke struct yang keluar dari lifetime yang diharapkan — disimpan di \u003Ccode>HashMap\u003C\u002Fcode>, dikirim melalui channel, atau dipegang di \u003Ccode>static\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch3>Skenario 2: Nested Acquire (Self-Deadlock)\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">async fn simulate_and_persist(\n    sem: &amp;Semaphore,\n    chain: &amp;Chain,\n) -&gt; Result&lt;()&gt; {\n    let _outer = sem.acquire().await?; \u002F\u002F Mengambil 1 dari N permit\n\n    let result = simulate(chain).await?;\n\n    \u002F\u002F BUG: mengakuisisi semaphore YANG SAMA di dalam permit yang dipegang\n    let _inner = sem.acquire().await?; \u002F\u002F Jika N=1, deadlock. Jika N&gt;1, mengurangi throughput\n    persist(result).await?;\n\n    Ok(())\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Dengan \u003Ccode>N=1\u003C\u002Fcode>, ini deadlock instan. Dengan \u003Ccode>N&gt;1\u003C\u002Fcode>, bekerja sampai beban meningkat dan semua permit dipegang oleh task yang menunggu inner acquire mereka. Perbaikan: gunakan semaphore terpisah untuk concern terpisah, atau restrukturisasi untuk menghindari akuisisi bersarang.\u003C\u002Fp>\n\u003Ch3>Skenario 3: Permit Dipegang Melintasi Await Point di Select\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">async fn process_with_timeout(sem: &amp;Semaphore) -&gt; Result&lt;()&gt; {\n    let permit = sem.acquire().await?;\n\n    tokio::select! {\n        result = do_work() =&gt; {\n            result?;\n        }\n        _ = tokio::time::sleep(Duration::from_secs(30)) =&gt; {\n            tracing::warn!(\"timeout, tetapi permit masih dipegang sampai drop\");\n        }\n    }\n    \u002F\u002F permit di-drop di sini — ini sebenarnya benar\n    \u002F\u002F TETAPI: jika do_work() men-spawn sub-task yang menangkap permit,\n    \u002F\u002F pembatalan do_work() TIDAK membatalkan sub-task\n    Ok(())\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Makro \u003Ccode>select!\u003C\u002Fcode> membatalkan branch yang kalah dengan men-drop future-nya, tetapi task yang di-spawn di dalam future tersebut terus berjalan. Jika task tersebut menangkap referensi ke permit, permit tidak dilepas ketika diharapkan.\u003C\u002Fp>\n\u003Ch2 id=\"mendiagnosis-deadlock-semaphore\">Mendiagnosis Deadlock Semaphore\u003C\u002Fh2>\n\u003Cp>Ketika sistem Anda berhenti membuat progress, bagaimana mengidentifikasi deadlock semaphore versus dependency yang lambat?\u003C\u002Fp>\n\u003Ch3>Diagnosis Berbasis Tracing\u003C\u002Fh3>\n\u003Cp>Instrumentasi acquire\u002Frelease dengan structured logging:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">let permits_before = semaphore.available_permits();\ntracing::debug!(\n    available = permits_before,\n    \"mengakuisisi permit semaphore\"\n);\n\nlet _permit = semaphore.acquire().await?;\n\ntracing::debug!(\n    available = semaphore.available_permits(),\n    \"permit semaphore diakuisisi\"\n);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Jika log Anda menunjukkan “mengakuisisi” tetapi tidak pernah “diakuisisi”, semua permit dipegang di suatu tempat.\u003C\u002Fp>\n\u003Ch3>Metrik Prometheus\u003C\u002Fh3>\n\u003Cp>Ekspos gauge metric untuk permit yang tersedia:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use metrics::gauge;\n\ngauge!(\"semaphore_available_permits\", semaphore.available_permits() as f64);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Gauge yang turun ke nol dan tetap di sana adalah deadlock. Gauge yang berfluktuasi dekat nol adalah contention.\u003C\u002Fp>\n\u003Ch3>tokio-console\u003C\u002Fh3>\n\u003Cp>\u003Ccode>tokio-console\u003C\u002Fcode> adalah alat diagnostik yang terhubung ke aplikasi tokio yang berjalan dan menampilkan state task secara real time:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\">cargo add --dev console-subscriber\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode class=\"language-rust\">#[cfg(debug_assertions)]\nconsole_subscriber::init();\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Jalankan \u003Ccode>tokio-console\u003C\u002Fcode> di terminal lain dan cari task yang stuck di state “Idle” pada semaphore acquire.\u003C\u002Fp>\n\u003Ch2 id=\"solusi-produksi\">Solusi Produksi\u003C\u002Fh2>\n\u003Ch3>Solusi 1: Selalu Gunakan OwnedSemaphorePermit dengan Arc\u003C\u002Fh3>\n\u003Cp>Ketika spawning task, lebih baik gunakan \u003Ccode>acquire_owned()\u003C\u002Fcode> daripada \u003Ccode>acquire()\u003C\u002Fcode>. Varian owned mengambil \u003Ccode>Arc&lt;Semaphore&gt;\u003C\u002Fcode> dan mengembalikan permit yang tidak meminjam semaphore — ia memiliki clone dari \u003Ccode>Arc\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">let semaphore = Arc::new(Semaphore::new(20));\n\nfor item in items {\n    let permit = semaphore.clone().acquire_owned().await?;\n\n    tokio::spawn(async move {\n        \u002F\u002F permit dipindahkan ke task — tanpa masalah lifetime\n        do_work(item).await;\n        drop(permit); \u002F\u002F drop eksplisit untuk kejelasan\n    });\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Solusi 2: Acquire dengan Timeout\u003C\u002Fh3>\n\u003Cp>Jangan pernah menunggu permit tanpa batas di produksi. Gunakan \u003Ccode>tokio::time::timeout\u003C\u002Fcode> untuk mendeteksi deadlock lebih awal:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use tokio::time::{timeout, Duration};\n\nlet permit = timeout(\n    Duration::from_secs(30),\n    semaphore.acquire(),\n).await\n    .map_err(|_| anyhow!(\"acquire semaphore timeout — kemungkinan deadlock\"))?\n    .map_err(|_| anyhow!(\"semaphore ditutup\"))?;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Ketika timeout menyala, log permit yang tersedia, jumlah task yang menunggu, dan stack trace.\u003C\u002Fp>\n\u003Ch3>Solusi 3: Structured Concurrency dengan JoinSet\u003C\u002Fh3>\n\u003Cp>Alih-alih \u003Ccode>tokio::spawn\u003C\u002Fcode> tanpa batas, gunakan \u003Ccode>tokio::task::JoinSet\u003C\u002Fcode> untuk mempertahankan ownership dari task yang di-spawn dan padukan dengan semaphore:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use tokio::task::JoinSet;\n\nlet semaphore = Arc::new(Semaphore::new(20));\nlet mut join_set = JoinSet::new();\n\nfor batch in batches {\n    let permit = semaphore.clone().acquire_owned().await?;\n    let db_pool = db_pool.clone();\n\n    join_set.spawn(async move {\n        let result = process_batch(&amp;db_pool, &amp;batch).await;\n        drop(permit); \u002F\u002F lepas sebelum JoinSet mengumpulkan\n        result\n    });\n}\n\n\u002F\u002F Drain semua task — kumpulkan error alih-alih kehilangan diam-diam\nwhile let Some(result) = join_set.join_next().await {\n    match result {\n        Ok(Ok(())) =&gt; {}\n        Ok(Err(e)) =&gt; tracing::error!(error = %e, \"pemrosesan batch gagal\"),\n        Err(e) =&gt; tracing::error!(error = %e, \"task panic\"),\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Pola ini memastikan: (1) concurrency terbatas via semaphore, (2) tidak ada kegagalan task yang diam, (3) task induk tahu kapan semua anak selesai, (4) permit selalu dilepas meskipun task panic.\u003C\u002Fp>\n\u003Ch3>Solusi 4: Semaphore Terpisah untuk Concern Terpisah\u003C\u002Fh3>\n\u003Cp>Jangan pernah berbagi satu semaphore untuk operasi yang tidak terkait. Dalam pipeline MEV kami, kami menggunakan semaphore terpisah:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub struct ConcurrencyLimits {\n    \u002F\u002F\u002F Membatasi panggilan simulasi RPC bersamaan ke node\n    pub simulation: Arc&lt;Semaphore&gt;,\n    \u002F\u002F\u002F Membatasi write database bersamaan untuk persistensi rantai\n    pub db_writes: Arc&lt;Semaphore&gt;,\n    \u002F\u002F\u002F Membatasi handler langganan mempool bersamaan\n    pub mempool: Arc&lt;Semaphore&gt;,\n}\n\nimpl ConcurrencyLimits {\n    pub fn new() -&gt; Self {\n        Self {\n            simulation: Arc::new(Semaphore::new(4)),   \u002F\u002F node menangani 4 batch paralel\n            db_writes: Arc::new(Semaphore::new(20)),   \u002F\u002F 20 dari 50 koneksi pool\n            mempool: Arc::new(Semaphore::new(100)),    \u002F\u002F 100 handler tx bersamaan\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Ini menghilangkan deadlock nested acquire sepenuhnya — task mengakuisisi permit \u003Ccode>simulation\u003C\u002Fcode>, lalu permit \u003Ccode>db_writes\u003C\u002Fcode>, dan ini adalah pool independen.\u003C\u002Fp>\n\u003Ch2 id=\"performa-overhead-semaphore\">Performa: Overhead Semaphore\u003C\u002Fh2>\n\u003Cp>Apakah semaphore itu sendiri bottleneck? Dalam praktik, tidak. \u003Ccode>tokio::sync::Semaphore\u003C\u002Fcode> diimplementasikan dengan counter atomik dan linked list intrusif dari waiter. Mengakuisisi permit yang tidak dikontestasi adalah satu \u003Ccode>fetch_sub\u003C\u002Fcode> — nanosecond. Bahkan di bawah contention, overhead-nya adalah notifikasi waker (juga nanosecond) versus milidetik yang diambil I\u002FO aktual Anda.\u003C\u002Fp>\n\u003Cp>Kami membenchmark overhead semaphore di pipeline kami:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Operasi\u003C\u002Fth>\u003Cth>Tanpa Semaphore\u003C\u002Fth>\u003Cth>Dengan Semaphore (20 permit)\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>10.000 write DB\u003C\u002Ftd>\u003Ctd>1.340ms (semua bersamaan, error pool habis)\u003C\u002Ftd>\u003Ctd>1.580ms (terkontrol, zero error)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>500 simulasi RPC\u003C\u002Ftd>\u003Ctd>8.200ms (node overload, timeout)\u003C\u002Ftd>\u003Ctd>9.100ms (4 sekaligus, zero timeout)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Memory (2,7 juta task)\u003C\u002Ftd>\u003Ctd>15,4 GB\u003C\u002Ftd>\u003Ctd>0,8 GB (berbatch dengan JoinSet)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Overhead wall-clock 10-15% dapat diabaikan dibandingkan menghilangkan error, timeout, dan crash OOM.\u003C\u002Fp>\n\u003Ch2 id=\"kesimpulan\">Kesimpulan\u003C\u002Fh2>\n\u003Cp>Semaphore di async Rust terlihat sederhana — \u003Ccode>acquire\u003C\u002Fcode>, lakukan kerja, drop permit. Kompleksitas muncul di produksi: permit yang bocor ke struct berumur panjang, nested acquire lintas call stack, permit yang dipegang melintasi batas pembatalan \u003Ccode>select!\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Playbook pertahanan:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Selalu\u003C\u002Fstrong> gunakan \u003Ccode>acquire_owned()\u003C\u002Fcode> ketika spawning task\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Selalu\u003C\u002Fstrong> bungkus acquire dalam timeout\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Jangan pernah\u003C\u002Fstrong> nest acquire pada semaphore yang sama\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Pisahkan\u003C\u002Fstrong> semaphore untuk pool sumber daya yang berbeda\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Instrumentasi\u003C\u002Fstrong> permit yang tersedia dengan metrik dan tracing\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Gunakan JoinSet\u003C\u002Fstrong> alih-alih \u003Ccode>tokio::spawn\u003C\u002Fcode> tanpa batas untuk structured concurrency\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Pola-pola ini telah bertahan melintasi jutaan block yang diproses, ratusan ribu task bersamaan, dan zero deadlock di produksi sejak mengadopsinya.\u003C\u002Fp>\n","id","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:25.366352Z","Semaphore di Async Rust — Berburu Deadlock dan Pola Fire-and-Forget","Pembahasan mendalam tokio::sync::Semaphore untuk backpressure, pola write fire-and-forget, diagnosis deadlock, dan solusi produksi dengan RAII permit dan structured concurrency.","semaphore async Rust",null,"index, follow",[22,27],{"id":23,"name":24,"slug":25,"created_at":26},"c0000000-0000-0000-0000-000000000022","Performance","performance","2026-03-28T10:44:21.513630Z",{"id":28,"name":29,"slug":30,"created_at":26},"c0000000-0000-0000-0000-000000000001","Rust","rust","Rekayasa",[33,40,47],{"id":34,"title":35,"slug":36,"excerpt":37,"locale":12,"category_name":38,"published_at":39},"d0000000-0000-0000-0000-000000000642","WASI 0.3 dan Kematian Cold Start: Wasm Sisi Server di Produksi","wasi-0-3-kematian-cold-start-wasm-sisi-server-di-produksi","WASI 0.3 dirilis pada Februari 2026 dengan async I\u002FO native, tipe stream, dan dukungan socket penuh. WebAssembly sisi server kini menghadirkan cold start dalam hitungan mikrodetik, dan setiap penyedia cloud besar menawarkan Wasm serverless.","DevOps","2026-03-28T10:44:47.445780Z",{"id":41,"title":42,"slug":43,"excerpt":44,"locale":12,"category_name":45,"published_at":46},"d0000000-0000-0000-0000-000000000620","Stack Backend Modern 2026: Rust + PostgreSQL 18 + Wasm + eBPF","stack-backend-modern-2026-rust-postgresql-wasm-ebpf","Empat teknologi konvergen untuk mendefinisikan ulang infrastruktur backend di 2026: Rust menghilangkan overhead garbage collection dan mengurangi jumlah container hingga 3x, PostgreSQL 18 menggantikan database khusus, WASI 0.3 memberikan cold start mikrodetik untuk fungsi serverless, dan eBPF memungkinkan observabilitas tanpa instrumentasi dengan biaya yang jauh lebih rendah dari monitoring tradisional.","Engineering","2026-03-28T10:44:45.804120Z",{"id":48,"title":49,"slug":50,"excerpt":51,"locale":12,"category_name":31,"published_at":52},"d0000000-0000-0000-0000-000000000548","Dari Autocomplete ke Otonom: Evolusi Alat AI Coding (2022-2026)","dari-autocomplete-ke-otonom-evolusi-alat-ai-coding-2022-2026","Kronik tentang bagaimana alat AI coding berevolusi dari autocomplete satu baris di 2022 menjadi agen multi-file otonom di 2026. Empat tahun yang mengubah pengembangan perangkat lunak selamanya, dengan pandangan tentang apa yang akan datang.","2026-03-28T10:44:41.351824Z",{"id":13,"name":54,"slug":55,"bio":56,"photo_url":19,"linkedin":19,"role":57,"created_at":58,"updated_at":58},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]