[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-29-semaphoren-async-rust-deadlock-fire-and-forget":3},{"article":4,"author":51},{"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},"d7000000-0000-0000-0000-000000000301","a0000000-0000-0000-0000-000000000076","Deep EVM #29: Semaphoren in async Rust — Deadlock-Jagd und Fire-and-Forget-Muster","deep-evm-29-semaphoren-async-rust-deadlock-fire-and-forget","Ein tiefer Einblick in tokio::sync::Semaphore fuer Backpressure-Kontrolle, Fire-and-Forget-Schreibmuster, Deadlock-Diagnose mit Tracing und tokio-console sowie produktionsgehaertete Loesungen mit RAII-Permits und acquire-Timeouts.","## Das Problem: Ressourcen-Obergrenzen\n\nWenn Sie eine Hochdurchsatz-Pipeline betreiben — einen MEV-Bot, der 180.000 Arbitrage-Ketten pro Block verarbeitet, einen API-Server mit 10.000 gleichzeitigen Anfragen oder einen ETL-Job, der Millionen von Zeilen schreibt — stossen Sie unweigerlich an eine Ressourcen-Obergrenze. Datenbank-Verbindungspools erschoepfen sich. RPC-Provider begrenzen Ihre Rate. Der Speicher blaetzt sich auf, weil Sie 50.000 tokio-Tasks gespawnt haben, die jeweils Daten einer Kette halten.\n\nDie Antwort: **Semaphoren** — ein Primitiv fuer die Kontrolle der Nebenlaeufigkeit, das die Anzahl gleichzeitiger Operationen auf eine feste Obergrenze begrenzt.\n\n## tokio::sync::Semaphore Grundlagen\n\n```rust\nuse tokio::sync::Semaphore;\nuse std::sync::Arc;\n\nlet sem = Arc::new(Semaphore::new(10)); \u002F\u002F Maximal 10 gleichzeitig\n\nfor item in items {\n    let sem = sem.clone();\n    tokio::spawn(async move {\n        \u002F\u002F Permit anfordern — blockiert wenn alle 10 vergeben\n        let _permit = sem.acquire().await.unwrap();\n        \n        \u002F\u002F Arbeit ausfuehren (maximal 10 gleichzeitig)\n        process(item).await;\n        \n        \u002F\u002F Permit wird automatisch freigegeben (RAII)\n    });\n}\n```\n\nDas Schluesselprinzip: `acquire()` wartet, bis ein Permit verfuegbar ist. Der RAII-Guard `_permit` gibt das Permit automatisch frei, wenn er den Scope verlaesst. Dies garantiert, dass nie mehr als N Operationen gleichzeitig laufen.\n\n## Fire-and-Forget-Schreibmuster\n\nIn vielen Systemen muessen Sie Ergebnisse in eine Datenbank schreiben, wollen aber nicht auf den Abschluss warten. Das klassische Fire-and-Forget-Muster:\n\n```rust\n\u002F\u002F SCHLECHT: Unbegrenztes Spawning\nfor result in results {\n    tokio::spawn(async move {\n        db.insert(result).await; \u002F\u002F Wenn db langsam -> unbegrenzte Tasks\n    });\n}\n\n\u002F\u002F GUT: Semaphore-begrenztes Fire-and-Forget\nlet write_sem = Arc::new(Semaphore::new(50)); \u002F\u002F Max 50 gleichzeitige Schreibvorgaenge\n\nfor result in results {\n    let sem = write_sem.clone();\n    let db = db.clone();\n    tokio::spawn(async move {\n        let _permit = sem.acquire().await.unwrap();\n        db.insert(result).await;\n    });\n}\n```\n\nOhne Semaphore: Wenn die Datenbank langsam ist, werden Tausende von Tasks gespawnt, die alle auf die Datenbank warten, Speicher belegen und die Verbindungspools ueberlasten.\n\nMit Semaphore: Maximal 50 Tasks warten gleichzeitig auf die Datenbank. Der Rest wartet auf ein Permit — kostenlos im Speicher.\n\n## Deadlock-Szenarien\n\n### Szenario 1: Verschachtelte acquire()-Aufrufe\n```rust\nlet sem = Arc::new(Semaphore::new(1));\n\nlet permit1 = sem.acquire().await.unwrap(); \u002F\u002F OK\n\u002F\u002F DEADLOCK: Wartet auf ein Permit, das wir selbst halten\nlet permit2 = sem.acquire().await.unwrap(); \u002F\u002F Haengt fuer immer\n```\n\n### Szenario 2: Permit ueber await-Punkt halten\n```rust\nlet sem = Arc::new(Semaphore::new(2));\n\nasync fn process(sem: Arc\u003CSemaphore>) {\n    let permit = sem.acquire().await.unwrap();\n    \n    \u002F\u002F Externer Aufruf, der laengeertete Minuten dauern kann\n    let result = slow_rpc_call().await; \u002F\u002F Permit bleibt gehalten!\n    \n    \u002F\u002F Andere Tasks warten auf dieses Permit\n    db.insert(result).await;\n}\n```\n\n### Loesung: Acquire-Timeout\n```rust\nuse tokio::time::{timeout, Duration};\n\nlet permit = timeout(\n    Duration::from_secs(5),\n    sem.acquire()\n).await;\n\nmatch permit {\n    Ok(Ok(permit)) => {\n        \u002F\u002F Permit erhalten, Arbeit ausfuehren\n    }\n    Ok(Err(_)) => {\n        \u002F\u002F Semaphore geschlossen\n    }\n    Err(_) => {\n        \u002F\u002F Timeout! Moglicher Deadlock.\n        tracing::warn!(\"Semaphore acquire timed out — possible deadlock\");\n    }\n}\n```\n\n## Deadlock-Diagnose mit Tracing\n\n```rust\nuse tracing::{instrument, warn};\n\n#[instrument(skip(sem, db))]\nasync fn write_results(\n    sem: Arc\u003CSemaphore>,\n    db: Arc\u003CDatabase>,\n    results: Vec\u003CResult>,\n) {\n    let available = sem.available_permits();\n    tracing::info!(available_permits = available, total_results = results.len());\n    \n    for (i, result) in results.into_iter().enumerate() {\n        let sem = sem.clone();\n        let db = db.clone();\n        \n        tokio::spawn(async move {\n            let start = Instant::now();\n            \n            let permit = timeout(\n                Duration::from_secs(30),\n                sem.acquire()\n            ).await;\n            \n            let wait_time = start.elapsed();\n            if wait_time > Duration::from_secs(5) {\n                warn!(\n                    wait_ms = wait_time.as_millis(),\n                    \"Semaphore wait exceeded 5 seconds\"\n                );\n            }\n            \n            if let Ok(Ok(_permit)) = permit {\n                db.insert(result).await;\n            }\n        });\n    }\n}\n```\n\n## tokio-console fuer Live-Debugging\n\ntokio-console ist ein Diagnose-Werkzeug, das Tasks, Ressourcen und Wartezeiten in Echtzeit visualisiert:\n\n```rust\n\u002F\u002F In main.rs:\nconsole_subscriber::init();\n\n\u002F\u002F Dann in einem separaten Terminal:\ntokio-console\n```\n\ntokio-console zeigt:\n- Alle laufenden Tasks mit ihrer Laufzeit\n- Semaphore-Wartezeiten pro Task\n- Tasks, die seit langem auf ein acquire() warten (Deadlock-Verdacht)\n\n## Strukturierte Nebenlaeufigkeit\n\nAnstatt unbegrenzt Tasks zu spawnen, verwenden Sie strukturierte Nebenlaeufigkeit:\n\n```rust\nuse futures::stream::{self, StreamExt};\n\nstream::iter(items)\n    .map(|item| {\n        let sem = sem.clone();\n        async move {\n            let _permit = sem.acquire().await.unwrap();\n            process(item).await\n        }\n    })\n    .buffer_unordered(100) \u002F\u002F Maximal 100 gleichzeitige Futures\n    .collect::\u003CVec\u003C_>>()\n    .await;\n```\n\nVorteil: Alle Tasks werden innerhalb eines einzigen Scopes verwaltet. Wenn der aeussere Future abgebrochen wird, werden alle inneren Tasks ebenfalls abgebrochen — kein Ressourcen-Leak.\n\n## Produktions-Checkliste fuer Semaphoren\n\n1. **Timeouts auf acquire()** — Immer. Deadlocks sind in Produktion unvermeidlich.\n2. **Monitoring der verfuegbaren Permits** — Prometheus-Metriken fuer `sem.available_permits()`.\n3. **Richtige Semaphore-Groesse** — Zu klein: kuenstlicher Engpass. Zu gross: Ressourcen-Ueberlastung.\n4. **RAII fuer Permit-Freigabe** — Niemals manuell `forget()` auf einem Permit aufrufen.\n5. **Kein verschachteltes acquire()** — Wenn unvermeidlich, `try_acquire()` fuer den inneren Aufruf verwenden.\n6. **tracing-Integration** — Jedes acquire() und release() loggen, um Deadlocks zu diagnostizieren.\n\n## Fazit\n\nSemaphoren sind das fundamentale Primitiv fuer Backpressure-Kontrolle in async Rust. Sie verhindern Ressourcen-Erschoepfung, begrenzen Nebenlaeufigkeit und ermoeglichen sicheres Fire-and-Forget. Aber sie bringen Deadlock-Risiken mit sich, die Timeouts, Monitoring und strukturierte Nebenlaeufigkeit erfordern. Behandeln Sie jeden Semaphore-Acquire als potenziellen Haltepunkt und instrumentieren Sie ihn entsprechend.","\u003Ch2 id=\"das-problem-ressourcen-obergrenzen\">Das Problem: Ressourcen-Obergrenzen\u003C\u002Fh2>\n\u003Cp>Wenn Sie eine Hochdurchsatz-Pipeline betreiben — einen MEV-Bot, der 180.000 Arbitrage-Ketten pro Block verarbeitet, einen API-Server mit 10.000 gleichzeitigen Anfragen oder einen ETL-Job, der Millionen von Zeilen schreibt — stossen Sie unweigerlich an eine Ressourcen-Obergrenze. Datenbank-Verbindungspools erschoepfen sich. RPC-Provider begrenzen Ihre Rate. Der Speicher blaetzt sich auf, weil Sie 50.000 tokio-Tasks gespawnt haben, die jeweils Daten einer Kette halten.\u003C\u002Fp>\n\u003Cp>Die Antwort: \u003Cstrong>Semaphoren\u003C\u002Fstrong> — ein Primitiv fuer die Kontrolle der Nebenlaeufigkeit, das die Anzahl gleichzeitiger Operationen auf eine feste Obergrenze begrenzt.\u003C\u002Fp>\n\u003Ch2 id=\"tokio-sync-semaphore-grundlagen\">tokio::sync::Semaphore Grundlagen\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-rust\">use tokio::sync::Semaphore;\nuse std::sync::Arc;\n\nlet sem = Arc::new(Semaphore::new(10)); \u002F\u002F Maximal 10 gleichzeitig\n\nfor item in items {\n    let sem = sem.clone();\n    tokio::spawn(async move {\n        \u002F\u002F Permit anfordern — blockiert wenn alle 10 vergeben\n        let _permit = sem.acquire().await.unwrap();\n        \n        \u002F\u002F Arbeit ausfuehren (maximal 10 gleichzeitig)\n        process(item).await;\n        \n        \u002F\u002F Permit wird automatisch freigegeben (RAII)\n    });\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Das Schluesselprinzip: \u003Ccode>acquire()\u003C\u002Fcode> wartet, bis ein Permit verfuegbar ist. Der RAII-Guard \u003Ccode>_permit\u003C\u002Fcode> gibt das Permit automatisch frei, wenn er den Scope verlaesst. Dies garantiert, dass nie mehr als N Operationen gleichzeitig laufen.\u003C\u002Fp>\n\u003Ch2 id=\"fire-and-forget-schreibmuster\">Fire-and-Forget-Schreibmuster\u003C\u002Fh2>\n\u003Cp>In vielen Systemen muessen Sie Ergebnisse in eine Datenbank schreiben, wollen aber nicht auf den Abschluss warten. Das klassische Fire-and-Forget-Muster:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F SCHLECHT: Unbegrenztes Spawning\nfor result in results {\n    tokio::spawn(async move {\n        db.insert(result).await; \u002F\u002F Wenn db langsam -&gt; unbegrenzte Tasks\n    });\n}\n\n\u002F\u002F GUT: Semaphore-begrenztes Fire-and-Forget\nlet write_sem = Arc::new(Semaphore::new(50)); \u002F\u002F Max 50 gleichzeitige Schreibvorgaenge\n\nfor result in results {\n    let sem = write_sem.clone();\n    let db = db.clone();\n    tokio::spawn(async move {\n        let _permit = sem.acquire().await.unwrap();\n        db.insert(result).await;\n    });\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Ohne Semaphore: Wenn die Datenbank langsam ist, werden Tausende von Tasks gespawnt, die alle auf die Datenbank warten, Speicher belegen und die Verbindungspools ueberlasten.\u003C\u002Fp>\n\u003Cp>Mit Semaphore: Maximal 50 Tasks warten gleichzeitig auf die Datenbank. Der Rest wartet auf ein Permit — kostenlos im Speicher.\u003C\u002Fp>\n\u003Ch2 id=\"deadlock-szenarien\">Deadlock-Szenarien\u003C\u002Fh2>\n\u003Ch3>Szenario 1: Verschachtelte acquire()-Aufrufe\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">let sem = Arc::new(Semaphore::new(1));\n\nlet permit1 = sem.acquire().await.unwrap(); \u002F\u002F OK\n\u002F\u002F DEADLOCK: Wartet auf ein Permit, das wir selbst halten\nlet permit2 = sem.acquire().await.unwrap(); \u002F\u002F Haengt fuer immer\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Szenario 2: Permit ueber await-Punkt halten\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">let sem = Arc::new(Semaphore::new(2));\n\nasync fn process(sem: Arc&lt;Semaphore&gt;) {\n    let permit = sem.acquire().await.unwrap();\n    \n    \u002F\u002F Externer Aufruf, der laengeertete Minuten dauern kann\n    let result = slow_rpc_call().await; \u002F\u002F Permit bleibt gehalten!\n    \n    \u002F\u002F Andere Tasks warten auf dieses Permit\n    db.insert(result).await;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Loesung: Acquire-Timeout\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">use tokio::time::{timeout, Duration};\n\nlet permit = timeout(\n    Duration::from_secs(5),\n    sem.acquire()\n).await;\n\nmatch permit {\n    Ok(Ok(permit)) =&gt; {\n        \u002F\u002F Permit erhalten, Arbeit ausfuehren\n    }\n    Ok(Err(_)) =&gt; {\n        \u002F\u002F Semaphore geschlossen\n    }\n    Err(_) =&gt; {\n        \u002F\u002F Timeout! Moglicher Deadlock.\n        tracing::warn!(\"Semaphore acquire timed out — possible deadlock\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"deadlock-diagnose-mit-tracing\">Deadlock-Diagnose mit Tracing\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-rust\">use tracing::{instrument, warn};\n\n#[instrument(skip(sem, db))]\nasync fn write_results(\n    sem: Arc&lt;Semaphore&gt;,\n    db: Arc&lt;Database&gt;,\n    results: Vec&lt;Result&gt;,\n) {\n    let available = sem.available_permits();\n    tracing::info!(available_permits = available, total_results = results.len());\n    \n    for (i, result) in results.into_iter().enumerate() {\n        let sem = sem.clone();\n        let db = db.clone();\n        \n        tokio::spawn(async move {\n            let start = Instant::now();\n            \n            let permit = timeout(\n                Duration::from_secs(30),\n                sem.acquire()\n            ).await;\n            \n            let wait_time = start.elapsed();\n            if wait_time &gt; Duration::from_secs(5) {\n                warn!(\n                    wait_ms = wait_time.as_millis(),\n                    \"Semaphore wait exceeded 5 seconds\"\n                );\n            }\n            \n            if let Ok(Ok(_permit)) = permit {\n                db.insert(result).await;\n            }\n        });\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"tokio-console-fuer-live-debugging\">tokio-console fuer Live-Debugging\u003C\u002Fh2>\n\u003Cp>tokio-console ist ein Diagnose-Werkzeug, das Tasks, Ressourcen und Wartezeiten in Echtzeit visualisiert:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F In main.rs:\nconsole_subscriber::init();\n\n\u002F\u002F Dann in einem separaten Terminal:\ntokio-console\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>tokio-console zeigt:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Alle laufenden Tasks mit ihrer Laufzeit\u003C\u002Fli>\n\u003Cli>Semaphore-Wartezeiten pro Task\u003C\u002Fli>\n\u003Cli>Tasks, die seit langem auf ein acquire() warten (Deadlock-Verdacht)\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"strukturierte-nebenlaeufigkeit\">Strukturierte Nebenlaeufigkeit\u003C\u002Fh2>\n\u003Cp>Anstatt unbegrenzt Tasks zu spawnen, verwenden Sie strukturierte Nebenlaeufigkeit:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use futures::stream::{self, StreamExt};\n\nstream::iter(items)\n    .map(|item| {\n        let sem = sem.clone();\n        async move {\n            let _permit = sem.acquire().await.unwrap();\n            process(item).await\n        }\n    })\n    .buffer_unordered(100) \u002F\u002F Maximal 100 gleichzeitige Futures\n    .collect::&lt;Vec&lt;_&gt;&gt;()\n    .await;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Vorteil: Alle Tasks werden innerhalb eines einzigen Scopes verwaltet. Wenn der aeussere Future abgebrochen wird, werden alle inneren Tasks ebenfalls abgebrochen — kein Ressourcen-Leak.\u003C\u002Fp>\n\u003Ch2 id=\"produktions-checkliste-fuer-semaphoren\">Produktions-Checkliste fuer Semaphoren\u003C\u002Fh2>\n\u003Col>\n\u003Cli>\u003Cstrong>Timeouts auf acquire()\u003C\u002Fstrong> — Immer. Deadlocks sind in Produktion unvermeidlich.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Monitoring der verfuegbaren Permits\u003C\u002Fstrong> — Prometheus-Metriken fuer \u003Ccode>sem.available_permits()\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Richtige Semaphore-Groesse\u003C\u002Fstrong> — Zu klein: kuenstlicher Engpass. Zu gross: Ressourcen-Ueberlastung.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>RAII fuer Permit-Freigabe\u003C\u002Fstrong> — Niemals manuell \u003Ccode>forget()\u003C\u002Fcode> auf einem Permit aufrufen.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Kein verschachteltes acquire()\u003C\u002Fstrong> — Wenn unvermeidlich, \u003Ccode>try_acquire()\u003C\u002Fcode> fuer den inneren Aufruf verwenden.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>tracing-Integration\u003C\u002Fstrong> — Jedes acquire() und release() loggen, um Deadlocks zu diagnostizieren.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"fazit\">Fazit\u003C\u002Fh2>\n\u003Cp>Semaphoren sind das fundamentale Primitiv fuer Backpressure-Kontrolle in async Rust. Sie verhindern Ressourcen-Erschoepfung, begrenzen Nebenlaeufigkeit und ermoeglichen sicheres Fire-and-Forget. Aber sie bringen Deadlock-Risiken mit sich, die Timeouts, Monitoring und strukturierte Nebenlaeufigkeit erfordern. Behandeln Sie jeden Semaphore-Acquire als potenziellen Haltepunkt und instrumentieren Sie ihn entsprechend.\u003C\u002Fp>\n","de","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:30.591424Z","Semaphoren in async Rust — Deadlock-Jagd und Fire-and-Forget-Muster","Tiefer Einblick in tokio::sync::Semaphore: Backpressure, Fire-and-Forget-Schreibvorgaenge, Deadlock-Diagnose, produktionsgehaertete Loesungen mit RAII-Permits und strukturierter Nebenlaeufigkeit.","Rust Semaphore async",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","Ingenieurwesen",[33,39,45],{"id":34,"title":35,"slug":36,"excerpt":37,"locale":12,"category_name":31,"published_at":38},"d0000000-0000-0000-0000-000000000680","Warum Bali 2026 zum Impact-Tech-Hub Südostasiens wird","warum-bali-2026-impact-tech-hub-suedostasiens","Bali rangiert auf Platz 16 unter den Startup-Ökosystemen Südostasiens. Mit einer wachsenden Konzentration von Web3-Entwicklern, AI-Nachhaltigkeits-Startups und Eco-Travel-Tech-Unternehmen formt die Insel ihre Nische als Impact-Tech-Hauptstadt der Region.","2026-03-28T10:44:49.720230Z",{"id":40,"title":41,"slug":42,"excerpt":43,"locale":12,"category_name":31,"published_at":44},"d0000000-0000-0000-0000-000000000679","ASEAN-Datenschutz-Flickenteppich: Compliance-Checkliste für Entwickler","asean-datenschutz-flickenteppich-compliance-checkliste-entwickler","Sieben ASEAN-Länder verfügen mittlerweile über umfassende Datenschutzgesetze mit unterschiedlichen Einwilligungsmodellen, Lokalisierungsanforderungen und Sanktionsstrukturen. Eine praktische Compliance-Checkliste für Entwickler.","2026-03-28T10:44:49.715484Z",{"id":46,"title":47,"slug":48,"excerpt":49,"locale":12,"category_name":31,"published_at":50},"d0000000-0000-0000-0000-000000000678","Indonesias 29-Milliarden-Dollar-Digitaltransformation: Chancen für Softwareunternehmen","indonesias-29-milliarden-dollar-digitaltransformation-chancen-softwareunternehmen","Indonesias IT-Dienstleistungsmarkt wird voraussichtlich 2026 29,03 Milliarden Dollar erreichen, gegenüber 24,37 Milliarden im Jahr 2025. Cloud-Infrastruktur, AI, E-Commerce und Rechenzentren treiben das schnellste Wachstum in Südostasien.","2026-03-28T10:44:49.697275Z",{"id":13,"name":52,"slug":53,"bio":54,"photo_url":19,"linkedin":19,"role":55,"created_at":56,"updated_at":56},"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"]