İçeriğe geç

Paralel AI agent'larla e2e testi: kilit ve kuyruk

Git worktree dosyaları ayırır, makineyi değil. Paralel yapay zeka agent'larının e2e koşularını dışlayıcı kilit dosyası ve FIFO kuyrukla nasıl sıraya soktuk.

Yazan: SuperDevSpace

Yayın 8 dk okuma

Diğer diller: English Deutsch Français Español Português (Brasil) 日本語 简体中文

Birden çok yapay zeka kodlama ajanını paralel çalıştırmaya başlamak kolay: her birine kendi git worktree'sini ve kendi terminalini verirsin, birbirlerinin dosyalarını ezmeyi bırakırlar. İş, paralel agent'larla e2e testi koşmaya gelince ilginçleşiyor. Worktree dosyaları ayırır, makineyi ayırmaz. Bu yazı kendi repomuzdan bir vaka: birkaç agent aynı anda uçtan uca (e2e) test koşunca ne bozuldu ve sıraya girsinler diye kurduğumuz küçük kilit + kuyruk düzeni.

Worktree'nin ayırmadığı şeyler

git worktree her agent'a ayrı bir checkout, ayrı bir dal ve ayrı bir node_modules verir. Kaynak ağacı bu kadar. Bir e2e koşusu ise çok daha fazlasına dokunur:

  • ekran — masaüstü uygulamamızın e2e paketi test ekranında gerçek pencereler açar, bazı spec'ler o an ekranda ne varsa onun ekran görüntüsünü alır;
  • kurulu tarayıcı — gerçek Chrome süren spec'ler aynı tarayıcı için yarışır;
  • pencere odağı — paketimiz uygulama aktivasyonlarını sayar ve odak çalan testi kırmızıya çevirir; bir koşunun yol açtığı aktivasyon ötekinin hanesine yazılır;
  • CPU — aynı anda iki paket, her zaman aşımını daraltır.

Bunların hiçbiri worktree'nin içinde değil. Hepsi makinedeki tüm agent'ların ortak malı.

Bunu doğrudan yaşayarak öğrendik. Bir sürüm kapısı 21 kırmızı testle döndü; hepsi gerçek Chrome açan ya da ekran yakalayan spec'lerdeydi ve o sırada başka worktree'lerdeki agent'lar koşunun ortasındaydı. Kodda hata yoktu. İki koşu birbirini ölçmüştü.

"Aynı anda tek e2e koşusu" kuralı birinin kafasında vardı. Tuttuğu güne kadar tuttu. Biz de onu bir mekanizmaya çevirdik.

İlk deneme: kilidi oku, sonra yaz

İlk kilit /tmp altında bir dosyaydı — worktree içinde değil, çünkü eşgüdümlediği koşular farklı worktree'lerde yaşıyor; worktree başına bir kilit onları hiç görmezdi. Dosyayı okuyor, makinenin boş olduğuna karar veriyor, sonra yazıyordu.

Klasik yarış durumu. İkisi de yazmadan önce okuyan iki koşu, makineyi ikisi de tuttuğunu sandı. Daha kötüsü: kısa koşu bitince bıraktığı kilit dosyayı sildi, uzun koşu hâlâ sürerken. Bir design:qa koşusu bir keresinde yaklaşık üç dakika hiç kilit dosyası olmadan çalıştı.

Çözüm tek bir bayrak. Dosyayı dışlayıcı oluştur — O_CREAT | O_EXCL; Node bunu wx bayrağı olarak sunar:

scripts/gate-lock.mjs
export function createExclusive(path, text) {
  try {
    writeFileSync(path, text, { flag: 'wx' })
    return true
  } catch (error) {
    if (error?.code === 'EEXIST') return false
    throw error
  }
}

İşletim sistemi dosyayı tam olarak bir çağıranın alacağını garanti eder. Oku-sonra-yaz penceresi kalmaz.

Kilit, çöken bir koşudan sağ çıkmalı

Sahibinden uzun yaşayan bir kilit dosyası herkesi sonsuza dek bekletir. Bu yüzden dosya sahibinin pid'ini tutar; bekleyen de kill -0 ile bakar — sinyal göndermez, yalnızca süreç var mı diye sorar. EPERM sürecin var olduğunu ama başkasına ait olduğunu söyler, yani canlı sayılır. Pid'i ölmüş bir kilit bayattır ve devralınır.

Aynısını düz bash ile de yapabilirsin. noclobber, >'nin var olan dosyanın üzerine yazmasını engeller; bu da sana dışlayıcı bir oluşturma verir:

Terminal
$ ( set -o noclobber; echo $$ > /tmp/e2e.lock ) && echo "acquired"
acquired
$ kill -0 "$(cat /tmp/e2e.lock)" && echo "holder alive"

Kilit yoksa makine boş demek değildir

Neredeyse kaçırdığımız tuzak şu: kilitten önce başlamış koşular kilit yazmaz. Elle başlatılan koşu da yazmaz. "Kilit dosyası yok"u yeşil ışık saymak, kilidin önlemek için var olduğu hatanın ta kendisi.

Bu yüzden kilit yoksa makinenin kendisine sorulur: süreç listesinde bir Playwright koşucusu var mı? Ama eşleşme kelimeye göre değil, çalıştırılabilir dosyanın biçimine göre olmalı. Süreç listesi üzerinde gevşek bir grep playwright başka agent'ların oturumlarını yakaladı; çünkü bir agent oturumu bütün prompt'unu komut satırında taşır ve bizim prompt'larımız Playwright'tan bahseder. Bu taramayı yazan agent'ın kendi kabuğu bile kendi taramasında çalışan bir kapı olarak göründü. Bir şey hakkındaki metin, o şeyin aynısı gibi görünür.

Yoklama döngüleri iki kez çöker: yarış ve açlık

Kilit doğru olsa da her agent'ın yine de onu beklemesi gerekiyordu ve her biri kendi yoklama döngüsünü yazdı. Sonuç, bir kuyruğun tam da çözmek için var olduğu iki hata oldu:

  • yarış — aynı saniyede deneyen birkaç döngü, kilit boşalır boşalmaz atlıyor;
  • açlık — kibar bir döngü ("boş" gördükten sonra 20 saniye bekle, sonra dene) hiç kazanamadı. Bir worker tek bir sıra alamadan 50 dakika bekledi.

Biz de kilidin önüne ilk gelen ilk alır (FIFO) bir kuyruk koyduk.

Bir e2e koşusu makineyi nasıl alırBir e2e koşusu makineyi nasıl alır
Bir e2e koşusu makineyi nasıl alır
Diyagramın metni
flowchart TD
  start["Agent bir e2e koşusu başlatır"] --> join["Bilet dosyasını dışlayıcı oluştur"]
  join --> head{"Biletim sıranın başında mı?"}
  head -->|"hayır"| wait["2 sn + rastgele gecikme bekle, nabzı tazele"]
  wait --> head
  head -->|"evet"| lock{"Kilit dosyasını dışlayıcı oluştur"}
  lock -->|"canlı bir pid tutuyor"| wait
  lock -->|"oluşturuldu"| run["Tek spec'i koş"]
  run --> release["Kilidi kaldır, log satırı yaz"]

Tasarım beş kuralda:

  1. Her bekleyene bir dosya, dışlayıcı oluşturulur. Sıraya girmek ortak bir listeyi okuyup yeniden yazmak demek değildir. Dosya adı biletin yeridir: <band>-<joined-at ms>-<sequence>-<pid>.json; adlar sıra düzeninde dizilir.
  2. Kilidi yalnızca baştaki alabilir. Kilidi boş görüp başta olmayan bekler. Hiç sıraya girmemiş bir çağıran, biri beklerken reddedilir.
  3. Ölü ve takılı bekleyenler çıkar. Pid'i ölmüş bilet silinir. Nabzı 60 saniyeden eski olan da — askıya alınmış bir bekleyen sıranın başından makineyi rehin tutamaz. Biletin silindiyse en sona yeniden girersin.
  4. Yoklamaya rastgelelik kat. Her 2 saniyede bir, üstüne 500 ms'ye kadar rastgele gecikme; bekleyenler aynı anda uyanmasın.
  5. Öncelik bileti öne alır, sahibini asla düşürmez. Ön bandı yalnızca sürüm kapısı alır; bu biletine ve log'a yazılır. O da kilidi tutan koşuyu bekler.

Bekleyen 60 dakika sonra vazgeçer ve 3 koduyla çıkar — reddedilmeyle aynı kod. --no-wait bayrağı ise hemen reddeder; biri beklerken de sırayı atlamasına izin verilmez, reddedilir.

Her geçişi yaz

O kilit üç dakika kaybolduğunda sonradan kimse açıklayamadı: dosyaya kimin dokunduğunu kaydeden hiçbir şey yoktu. Artık her alma, ret, bayat kilidi devralma, sıraya girme, çıkma, atılma ve bırakma kilidin yanındaki bir log'da tek satır; boyutu 1 MB ile sınırlı. "Saat 14:02'de makine kimdeydi?" bir tartışma değil, bir grep.

Agent'lara talimatlarında söyle

Agent'ların etrafından dolaştığı mekanizma, mekanizma değildir. Repomuzdaki CLAUDE.md — Claude Code'un proje hafızası olarak okuduğu dosya — bunu açıkça yazar: varsayılan beklemektir ve kendi yoklama döngünü yazma. Ayrıca her agent'a yalnızca kendi değişikliğini kapsayan tek spec'i, bir kez koşmasını söyler. Daha az koşu, daha kısa kuyruk demek.

Altı testi geçen tek spec'lik bir e2e koşusunu ve kapı özetini gösteren terminal; yanında canlı pencere konumu paneli
Tek spec, tek koşu: SuperDevSpace'in kendi terminalinde e2e kapı özeti

Paralel agent çalıştırıyorsan bu listeyi al

Agent başına yoklama döngüsüKilit + FIFO kuyruk
Aynı ekranda iki koşu✓ mümkün✗
Çöken bir koşu herkesi kilitler✓ (pid kontrolü yoksa)✗
Bekleyen aç kalabilir✓✗
Makinenin kimde olduğu bilinir✗✓
  • Kilidi her worktree'nin görebileceği yere koy (/tmp, Windows'ta işletim sisteminin geçici dizini).
  • Dışlayıcı oluştur; pid'i sakla; ölü pid'i boş say.
  • Kilidin yokluğuna güvenme — gerçek koşucunun ikili dosyasına bak.
  • Her bekleyene bir dışlayıcı dosya, nabız ve rastgele gecikmeyle sıraya koy.
  • Her geçişi log'a yaz.
  • Kuralı agent'ların talimat dosyasına yaz.

SuperDevSpace'te bu nasıl

SuperDevSpace'te manager sprint'i görevlere böler ve her worker kendi git worktree'sinde, kendi terminalinde çalışır; aynı repoda paralel iş dosyalarda çakışmaz. Yukarıdaki kilit ve kuyruk bir ürün özelliği değil — kendi repomuzdaki betikler; SuperDevSpace'i geliştiren agent'lar, worktree'nin bölemediği tek kaynak için yazdı. Agent'ların bir test ekranını, tarayıcıyı ya da portu paylaşıyorsa aynısına ihtiyacın olacak; desen kopyalanacak kadar küçük. Claude Code tam destekli; Codex ve Gemini CLI gibi diğer CLI'lar deneysel.

İşin worktree tarafı için Paralel kodlama agent'larını git worktree'lerde çalıştırmak yazısına, uygulamanın kurulumu için dokümanlara bak.

SuperDevSpace'i indir

SuperDevSpace

SuperDevSpace’i yapan ekip yazdı: zaten sahip olduğun CLI aboneliğiyle bir yapay zekâ kod ajanı ekibi çalıştıran masaüstü uygulaması.

Paylaş

Lansman e-postasını al

SuperDevSpace herkese açıldığında tek bir e-posta. Önce adresini gelen kutundan doğrularsın; tek tıkla ayrılabilirsin.

Kendi yapay zekâ ekibini çalıştır

SuperDevSpace zaten sahip olduğun CLI aboneliğiyle çalışır. 14 gün ücretsiz, kart gerekmez.

Ücretsiz denemeyi başlat

Okumaya devam et