# Async-background: Lehkováhový systém pozadíových úkolů pro Ruby bez externích závislostí
Vývojáři Ruby aplikací se často setkávají s potřebou spouštění pozadíových úkolů, ale hotová řešení jako Sidekiq vyžadují dodatečnou infrastrukturu. Nový gem async-background nabízí alternativu, která běží na Async bez Redis nebo Postgres, s detailní kontrolou zdrojů.
Proč stávající řešení nevyhovují
Obvykle se v Ruby pro pozadíové úlohy používá Sidekiq, ale vyžaduje Redis. Pro malé projekty s pár úkoly je to zbytečné: další kontejner v docker-compose, další bod selhání a náklady na infrastrukturu. Navíc Sidekiq neumožňuje flexibilně rozkládat zátěž mezi workery: těžký noční report může zpomalit zpracování uživatelských e-mailů.
Další varianty mají také své nedostatky:
whenever— generuje systémový crontab, bez fronty a integrace s aplikací.rufus-scheduler— běží v samostatných vláknech, což v aplikacích Async způsobuje přepínání kontextu a problémy s duplikací při více procesech.good_jobasolid_queue— jsou vázané na ActiveRecord a Postgres. To vytváří rizika: pozadíové úlohy soutěží s transakční zátěží o zdroje databáze, což může vést ke zpomalení hlavní aplikace.
Tím vznikla potřeba řešení, které kombinuje cron, intervaly a dynamickou frontu bez externích závislostí a s možností kontroly využití zdrojů.
Klíčové principy návrhu
Autor gemu formuloval šest základních principů:
- Jeden event loop. Plánovač běží ve stejné reaktivní smyčce jako aplikace, bez samostatných vláken.
- Zero infrastructure. Fronta je uložena na disku (SQLite v WAL režimu nebo soubor v
tmp/), bez Redis nebo Postgres. - Volitelné závislosti. Dodatečné funkce (např. metriky) se připojují přes volitelné gemy.
- Multi-process safety bez koordinátora. Zajišťuje, že úkol se spustí pouze v jednom procesu při použití Falcon (forků).
- Persistence jako pojistka, rychlost jako samostatný mechanismus. Spolehlivost zajišťuje uložení do SQLite, rychlost probuzení přes UNIX soket.
- Řízené využití workerů. Vývojář může přiřadit úlohy konkrétním workerům přes proměnné prostředí a YAML konfiguraci.
Tyto principy umožnily vytvořit gem, který se snadno integruje do stávajících Async aplikací a nevyžaduje dodatečnou infrastrukturu.
Architektonické vlastnosti
Minimální halda místo tiků
Místo smyčky se sleep 1 (která se budí každou sekundu a kontroluje všechny úlohy) se používá binární minimální halda (MinHeap). To umožňuje:
- Získat nejbližší úkol za O(1) pomocí
peek. - Aktualizovat haldu po spuštění úkolu za O(log n) pomocí
replace_top. - Spát přesně do okamžiku další události a šetřit CPU zdroje.
Implementace zabírá pouze 74 řádků a nevyžaduje externí závislosti.
Dvě časové škály
Pro intervalové úlohy (např. every: 60) se používají monotónní hodiny (CLOCK_MONOTONIC), aby se vyhnuli problémům při synchronizaci času (NTP). Pro cron úlohy (např. cron: "0 3 *") se používají zdí hodin (Time.now), protože plán je vázaný na kalendářní čas.
V kódu to vypadá takto:
next_run_at = if task_config[:interval]
now + jitter + task_config[:interval]
else
now_wall = Time.now
wall_wait = task_config[:cron].next_time(now_wall).to_f - now_wall.to_f
now + jitter + [wall_wait, MIN_SLEEP_TIME].max
end
Sharding přes CRC32
Při použití Falcon (který se forkne) je nutné zajistit, že se úkol spustí pouze v jednom procesu. K tomu slouží deterministický sharding: pro název úkolu se počítá Zlib.crc32(name) % total_workers + 1. Pokud výsledek odpovídá indexu aktuálního workeru, úkol se načte.
assigned = config['worker']&.to_i || ((Zlib.crc32(name) % total_workers) + 1)
next unless assigned == worker_index
To eliminuje potřebu distribuovaných zámků.
Skip-on-overlap
Pokud úkol trvá déle než interval (např. interval 60 sekund, úkol 90), nový úkol se nespustí. Místo toho se aktuální úkol přeskočí, zaloguje a přesune na další slot:
if entry.running
logger.warn('Async::Background') { "#{entry.name}: skipped, previous run still active" }
metrics.job_skipped(entry)
entry.reschedule(monotonic_now)
heap.replace_top(entry)
next
end
Fronta: SQLite + UNIX-soket
Dynamická fronta využívá dva mechanismy:
- SQLite — zdroj pravdy. Úlohy se ukládají na disk, což zajišťuje spolehlivost.
- UNIX-soket — pro okamžité upozornění. Po přidání úkolu do DB se pošle signál do soketu, aby worker úkol okamžitě zpracoval.
Kromě toho je polling každých 5 sekund jako pojistka pro případ ztráty signálu.
Obnovení po selháních
Pokud worker spadne během provádění úkolu, stav zůstane „v práci". Při restartu Queue::Store#recover najde takové záznamy a vrátí je do fronty.
Co je důležité
- Žádná závislost na externích službách. Async-background používá pouze souborový systém, což zjednodušuje nasazení.
- Kontrola zdrojů. Možnost izolovat workery pro různé typy úkolů (cron, fronta) zabraňuje soutěži o zdroje.
- Spolehlivost. Úlohy se neztrácejí díky uložení do SQLite a mechanismu obnovení.
- Flexibilita. Konfigurace přes YAML a proměnné prostředí umožňuje snadné nastavení distribuce úkolů.
- Výkon. Minimální halda a UNIX soket zajišťují nízkou latenci a efektivní využití CPU.
— Editorial Team
Zatím žádné komentáře.