- Pradžia
- Atvejų analizės
- Patvari užduočių eilė, pakeitusi „no-code“ automatizacijų sieną
Patvari užduočių eilė, pakeitusi „no-code“ automatizacijų sieną
Trapių „no-code“ automatizacijų pakeitimas viena patvaria eile, kurioje gedimai matomi, nutrūkusios užduotys tęsiamos, o ribojamas API darbas vykdomas saugiai.
Paskelbta 2026-06-30Peržiūrėta 2026-07-02
Trumpai
- Problema
- Auganti „no-code“ srautų krūva tapo sunkiai stebima, sunkiai kartojama ir nesaugi, kai išoriniai API darbai nutrūkdavo viduryje.
- Apribojimas
- Pakeitimas turėjo išlaikyti produkcinį elgesį paaiškinamą, ribojamą, autentifikuotą ir atkuriamą be didelio platformos perrašymo.
- Sukurta sistema
- Postgres paremta užduočių eilė su nuomomis, širdies dūžiais, tiekėjų greičio ribomis, vaikų idempotentiškumo raktais, sausais paleidimais ir šešėline vykdyba.
- Rezultatas
- Gedimai tapo užklausiamomis eilutėmis, pakartojimai — saugūs pagal konstrukciją, o naujas automatizacijas galima tikrinti su gyvu srautu prieš joms veikiant.
- Kur tai pritaikoma
- Aktualu komandoms, kurių Zapier, Make ar n8n tipo srautai peraugo nepermatomus valdymo ekranus ir reikalauja gamybinės programinės įrangos.
- Technologijos
- Go · Postgres leases · gocron · bearer + HMAC auth
Kodėl tai svarbu
Jei jūsų komanda planuoja sistemą su tais pačiais gedimo scenarijais — būtent nuo tokio mąstymo prasideda pirmas pokalbis.
Apžvalga
Užduotis buvo siauresnė, nei skamba. Ne „pakeisti no-code geresniu no-code“ ir ne „pastatyti naują platformą“ — tas pats produkcinis elgesys turėjo veikti toliau, tik dabar tokia forma, kurioje gedimas yra paaiškinamas, o ne nuleidžiamas pečiais, iškvietimai į išorinius API neperžengia to, ką konkretus tiekėjas iš tikrųjų toleruoja, kiekvienas įėjimo taškas į sistemą autentifikuotas, o ne plika „webhook“ nuoroda, kurią gali paliesti kiekvienas ją radęs, ir užduotis, sugedusi pusiaukelėje, gali būti pratęsta, o ne tyliai prarasta. Visa tai turėjo įvykti be aplinkinės programos perrašymo — eilė turėjo įsiterpti kaip dar vienas infrastruktūros gabalas, o ne kaip pusmetį trunkanti platformos migracija, dėl kurios sustotų visi kiti planai. Toks apibrėžimas iš karto atmetė akivaizdų kelią — tiesiog uždėti geresnius valdymo ekranus ant esamų „no-code“ įrankių: valdymo ekranas aprašo juodąją dėžę, bet nepaverčia jos užklausiama. Sprendimas turėjo gyventi tame lygmenyje, kaip darbas paimamas, kartojamas ir ribojamas — o ne tame, kaip jis rodomas ekrane.
Užduočių paėmimas nuomos principu su širdies dūžiais
Kiekvienas darbo vienetas eilėje — eilutė jobs lentelėje, ir pačiai eilutei nereikia nieko daugiau, kad būtų aišku, ar ji šiuo metu dirbama.
Tam skirti trys stulpeliai: leased_until laiko žyma, sveikasis skaičius claim_count ir heartbeat_at laiko žyma, atnaujinama, kol užduotis vykdoma.
Darbininkas paima užduotį atomiškai nustatydamas leased_until kelioms minutėms į priekį — ne užimdamas programos lygmens užraktą, kuris išnyksta, jei procesas miršta jį laikydamas, o įrašydamas galiojimo pabaigą tiesiai į pačią eilutę, toje pačioje transakcijoje, kuri ją skaito.
UPDATE jobs
SET leased_until = now() + interval '5 minutes',
claim_count = claim_count + 1,
heartbeat_at = now()
WHERE id = (
SELECT id FROM jobs
WHERE status = 'pending' AND leased_until < now()
ORDER BY priority DESC, created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *;
Kito laisvos užduoties paėmimas — SKIP LOCKED užtikrina, kad lygiagretūs darbininkai niekada nesiblokuoja vienas kito ties ta pačia eilute
Kol užduotis vykdoma, darbininkas periodiškai atnaujina heartbeat_at ir pratęsia leased_until — tai gyvybės įrodymas, o ne vien paėmimo įrodymas.
Jei procesas miršta viduryje darbo — OOM nužudymas, diegimas, perkūręs konteinerį, ar panika, nutraukusi „goroutine“ be galimybės susitvarkyti — niekas neprivalo to pastebėti ir reaguoti.
Nuoma tiesiog pasibaigia.
Kitas darbininkas, apklausiantis laukiančias užduotis, pamato eilutę, kurios leased_until jau praėjo, ir ją paima lygiai taip pat, kaip paimtų dar niekieno neliestą užduotį.
Joks procesas nestebi mirusių darbininkų, joks prižiūrėtojas jų „nerenka“ — pačios eilutės laiko žyma yra visas gyvybingumo patikrinimas.
claim_count fiksuoja, kiek kartų tai įvyko, todėl teiginys „ši užduotis vis krenta“ iš anekdoto virsta WHERE claim_count > 3 užklausa.
Kodėl ne tiesiog palikti „no-code“ įrankį ir prisegti stebėseną
Kai „no-code“ srautas pradeda elgtis blogai, natūralu į jį kažką nutaikyti — veikimo laiko patikrą ant paleidiklio, įspėjimą ant klaidos „webhook“, būsenos puslapį, nuskaitytą iš tiekėjo vykdymų istorijos. Nė vienas iš šių žingsnių neliečia tikrosios problemos. Stebėsena gali pasakyti, kad srautas sužlugo; ji negali pasakyti, kur jis sustojo, nes „no-code“ įrankis apskritai neturi tęstinio darbo vieneto sąvokos — vykdymas arba baigėsi, arba ne, o „pusiaukelėje“ tiesiog neturi vietos jo duomenų modelyje. Prisegus stebėseną, atsiranda antra nepermatoma dėžė greta pirmosios: dabar reikia derinti du sistemas vietoj vienos, o pati srauto logika — realūs žingsniai, realus pakartojimų elgesys — vis tiek lieka tiekėjo sąsajoje, kur jos negalima nei užklausti, nei versijuoti, nei paimti nuoma. Nepermatomumą pataisyti reiškia perkelti pačią srauto logiką į kažką, kas turi eilutes, o ne uždėti valdymo ekraną ant dėžės, kurioje ji ir toliau užrakinta.
Greičio ribojimas pagal kiekvieną tiekėją
Vienas bendras greičio limitas šiai problemai netinka, nes apribojimas yra ne „kiek srauto ši eilė gali sukurti“, o „kiek srauto konkretus tiekėjas toleruos, kol pradės grąžinti 429 arba, dar blogiau, tyliai stabdyti“. Kiekvieno tiekėjo limitas skirtingas: vieni skaičiuoja per sekundę, kiti — per minutę, vieni dalija biudžetą visai paskyrai, kiti — kiekvienam API raktui atskirai, o bendras limiteris, pakankamai dosnus mažiausiai ribojančiam tiekėjui, greičiausiai riboto tiekėjo paskyrą uždarys.
Eilė laiko greičio biudžetą kiekvienam tiekėjui atskirai — ne kiekvienai užduočiai ir ne bendrai. Prieš siųsdamas iškvietimą, darbininkas išima pajėgumą iš to tiekėjo biudžeto; jei biudžetas tuščias, užduotis grąžinama į laukiančių būseną su trumpu vėlinimu, o ne kartojama iškart, o darbininkas pereina prie kitos, kito tiekėjo užduoties, kuriai biudžeto dar užtenka. Tai ir yra kita priežastis, kodėl bendras limiteris čia netinka: lėtas ar smarkiai ribojamas tiekėjas priešingu atveju badautų visus kitus, už jo eilėje stovinčius tiekėjus, nors tarp jų nėra nieko bendro.
Kiekvienas siunčiamas iškvietimas taip pat neša eilės pačios autentifikaciją, o ne pasikliauja tuo, ką paskirties vieta sutinka priimti — bearer prieigos raktą iškvietimams į pačios eilės vidinius valdymo galinius taškus (užduoties įtraukimas, pristabdymas, pakartotinis paleidimas pagal id), ir HMAC parašą virš siunčiamo turinio tada, kai gaunančioji pusė turi patikrinti, ar užklausa iš tikrųjų atėjo iš šios eilės ir nebuvo pakeista kelyje. Nė vienas iš šių dalykų nėra malonumas dėl malonumo — eilė, kuriai bet kas, radęs nuorodą, gali tyliai liepti įtraukti ar pakartoti darbą, nėra iš esmės saugesnė nei „no-code“ įrankis, kurį ji pakeitė, tiesiog jos piktnaudžiavimą sunkiau pastebėti.
Vaikiniai idempotencijos raktai siunčiamiems iškvietimams
Užduotis, sugedusi pusiaukelėje ir paimta kito darbininko, iš išorės neatskiriama nuo tiesiog lėtos užduoties — niekas pakartojime nepasako „tai jau kartą vyko ir sužlugo po to, kai API iškvietimas jau buvo išsiųstas, bet dar prieš gaunant atsakymą“. Tai pavojingiausias tarpinis atvejis: apmokestinimas įvykdytas, laiškas išsiųstas, įrašas sukurtas kitoje sistemoje, ir tada darbininkas mirė, nespėjęs to sėkmingo rezultato įrašyti į savo pačios eilutę. Naiviai pakartojus, šalutinis poveikis pasikartoja.
Sprendimas — vaikinis idempotencijos raktas, sukuriamas vieną kartą kiekvienai užduočiai ir naudojamas iš naujo kiekviename tos užduoties pakartojime, o ne generuojamas kiekvienam bandymui iš naujo. Jis kildinamas iš pačios užduoties tapatybės — jos eilutės id plius žingsnio identifikatoriaus — todėl raktas stabilus, kad ir kiek kartų užduotis būtų paimta iš naujo, bet skiriasi nuo kiekvienos kitos užduoties rakto, kad nesusiję darbai niekada netyčia nesusidurtų su tuo pačiu raktu.
func childIdempotencyKey(jobID uuid.UUID, step string) string {
h := sha256.Sum256([]byte(jobID.String() + ":" + step))
return hex.EncodeToString(h[:])
}
Determinuotas, nuo bandymo numerio nepriklausantis raktas kiekvienam žingsniui — pakartojimas jį tiesiog panaudoja iš naujo
Šis raktas siunčiamas kaip antraštė ar laukas su kiekvienu užduoties siunčiamu iškvietimu, ir bet kuris tiekėjas, gerbiantis idempotencijos raktus — o tai šiandien dauguma mokėjimų, žinučių ir CRM API — atpažįsta pasikartojantį raktą ir grąžina pradinį atsakymą, o ne įvykdo šalutinį poveikį antrą kartą. Eilei nereikia žinoti, ar konkretus pakartojimas yra pirmas bandymas, ar penktas — ji tiesiog visada siunčia tą patį raktą tam pačiam darbo vienetui, o dubliavimąsi leidžia sugerti paties tiekėjo idempotencijos logikai. Pakartojimai nustoja būti rizikos šaltiniu ir tampa tuo, kuo ir turėjo būti nuo pat pradžių — mechanine smulkmena, apie kurią iškvietimo pusei nereikia galvoti.
Šešėlinis režimas ir sausas paleidimas
Kiekviena „no-code“ migracija neša tą pačią riziką atvirkščiai: srautas, kurį ji pakeičia, perrašytas gali elgtis šiek tiek kitaip, o vienintelis būdas tai tiksliai sužinoti — paleisti jį su realiu srautu. Bet nepatikrintos automatizacijos paleidimas su realiu srautu yra būtent tai, ką visa ši sistema turi padaryti saugu, o ne daryti nerūpestingai. Eilė tai išsprendžia paleisdama tą patį variklį, tas pačias užduotis, tas pačias paleidimo sąlygas dviem režimais, kurie sustoja žingsniu prieš tikrą veiksmą.
„Dry-run“ (sauso paleidimo) režimu užduotis vykdo visą savo logiką — skaitymus, transformacijas, sprendimus — lygiai iki siunčiamo iškvietimo taško, ir tada registruoja, ką būtų išsiuntusi, vietoj to, kad iš tikrųjų siųstų. Tai pigi patikra: ar automatizacija priima tuos pačius sprendimus, kuriuos priimtų žmogus, gavęs realius įeinančius duomenis, be rizikos, kad laiškas ar API iškvietimas iš tikrųjų iškeliautų.
Šešėlinis režimas žengia žingsniu toliau, ir būtent jis realiai užsitarnauja pasitikėjimą prieš perjungiant jungiklį. Šešėlinė užduotis veikia lygiagrečiai su automatizacija, kurią ji turi pakeisti ar papildyti, paleidžiama tų pačių gyvų įvykių, o jos pačios siunčiami iškvietimai vis dar slopinami arba nukreipiami į smėlio dėžės galinį tašką — tačiau jos sprendimai, pasirinktos šakos, apskaičiuoti duomenys registruojami ir lyginami su tuo, ką iš tikrųjų padarė pakeičiama sistema. Nesutapimai tarp jų dviejų yra svarbiausias signalas: ne „ar naujoji automatizacija sudužo“, bet „ar ji šiam konkrečiam realiam įvediniui nusprendė kitaip, nei nuspręstų senoji“. Nauja automatizacija tik tada užsitarnauja teisę veikti — palikti sausą paleidimą ir šešėlį bei realiai iššauti savo siunčiamus iškvietimus — kai su realiu srautu ji išbandyta pakankamai ilgai, kad jos nesutapimai su senuoju elgesiu būtų suprasti, o ne vien nepasitaikę.
Tai iš esmės kitokia kartelė, nei paprastai siūlo „no-code“ įrankio įmontuotas testavimo režimas — vienas sintetinis pavyzdys, peržiūrėtas redaktoriuje. Šešėlinio režimo testiniai duomenys yra tai, ką produkcija iš tikrųjų siunčia tą dieną, o tai vienintelis duomenų šaltinis, patikimai turintis ribinius atvejus, kurių sintetinis pavyzdys nepagauna.
Kompromisai ir apribojimai
- „No-code“ srauto perkėlimas į eilę yra perrašymas po vieną, o ne masinis importas — kiekvienos automatizacijos logiką reikia iš naujo išreikšti kaip eilės užduotis rankomis, todėl nauda atsiranda palaipsniui, kartu su kiekviena perkelta automatizacija, o ne pirmą dieną.
- Nuomos ir širdies dūžio laikai — derinimo, o ne kartą išspręstas klausimas: trumpa nuoma greitai aptinka mirusį darbininką, bet rizikuoja iš naujo paimti tiesiog lėtai dirbančią užduotį ir ją įvykdyti du kartus; ilga nuoma šio klaidingo pozityvo išvengia, bet užstrigusi užduotis ilgiau prastovi nepastebėta.
- Tiekėjų greičio ribos — tik konkretaus momento sąlygų nuotrauka. Tiekėjui susiaurinus limitus ar pakeitus apskaitos langą, biudžeto konfigūraciją reikia peržiūrėti iš naujo — eilė to pati, vien pagal atsakymų kodus, neišsiaiškina.
- Vaikiniai idempotencijos raktai veikia tik tada, kai gaunančioji API juos iš tikrųjų gerbia. Dauguma šiuolaikinių mokėjimų ir žinučių API tai daro; sena ar vidinė API, ignoruojanti idempotencijos antraštę, jokios apsaugos nesuteikia, ir pakartojimas prieš ją vėl tampa realia rizika.
- Šešėlinis režimas atskleidžia tik tuos nesutapimus, kurie pasirodo sraute, kurį jis iš tikrųjų mato. Retai pasitaikanti paleidimo sąlyga, dar nespėjusi įvykti per šešėlinio stebėjimo langą, lieka neišbandyta, kai automatizacija paleidžiama iš tikrųjų.
- Nė vienas iš šių dalykų nepakeičia sprendimo, kurias automatizacijas apskritai verta perkelti. Srautas, veikiantis du kartus per metus be jokio išorinio šalutinio poveikio, iš perrašymo į patvarią eilę beveik nieko nelaimi; verta perkelti tas, kurios jau kelia aprašytą skausmą.
Šaltiniai ir tolesnis skaitymas
Atskiri čia naudoti sprendimai nėra nauji išradimai — vertė atsirado sujungus juos į vieną nuobodžią, užklausiamą sistemą vietoj tuzino nepermatomų:
Susijusios atvejų analizės
Sistemos, kurias su šia sieja bendras apribojimas — pakartojimai, izoliacija, kaina, atkūrimas.
Agentų valdomas užduočių variklis
Agentai gali saugiai paimti, pratęsti, užbaigti, atmesti ir eskaluoti darbą, o žmonės realiu laiku mato tą pačią lentą.
Drizzle · Postgres LISTEN/NOTIFY · SSE · contract-first APIIdempotentinis „webhook“ priėmimas dideliu mastu
Duomenų praradimo rizika virto nuolatinėmis patikromis, kurios aptinka spragas, jas taiso ir rodo operacinę būklę.
Go · Postgres (SKIP LOCKED, matviews) · HMAC · advisory locks