git commit Döndüğünde Kendi Repack'i Hâlâ .git'e Yazıyor
Programınız git commit'i bekleyip ardından depoyu siliyor, kopyalıyor ya da arşivliyorsa, o commit'in başlattığı bir repack .git içine hâlâ yazıyor olabilir. Aşağıda mekanizmayı git'in kendi kaynak kodunda adım adım izliyor, Ephesus'un eklediği iki config bayrağını ve bunların commit süresine maliyetini anlatıyorum.
Silme bütçesinin tamamını harcayan tek bir CI teardown'ı
Ephesus, terminal tabanlı kodlama ajanlarını bir ekip gibi çalıştıran, pre-alpha aşamasındaki bir Electron uygulaması. Ajanlar Agora adlı yerel bir git deposu üzerinden haberleşiyor ve bu depoya giden her git komutunu tek bir kuyruk çalıştırıyor. Ubuntu CI'da, git 2.55.0 ile bir test yalnızca bir kez, teardown sırasında ENOTEMPTY: directory not empty, rmdir '.../agora/.git' hatasıyla düştü.
İpucu süredeydi. Düşen test 2.841 ms sürmüştü; git kullanan yedi kardeş testin her biri 77 ile 107 ms arasında bitiyordu (PR #62). Teardown, fs.rmSync'i maxRetries: 10 ve retryDelay: 50 ile çağırıyordu.
Node 20'nin senkron silme fonksiyonu bir dizinin içeriğini yalnızca bir kez listeliyor, listelediklerini siliyor, sonra sadece rmdir'i tekrar deniyor ve i + 1. denemeden önce i × 50 ms bekliyor. On tekrarın beklemeleri 50 × 55 = 2.750 ms ediyor. Bunu 2.841'den çıkarınca geriye normal bir test gövdesi kalıyor; yani teardown bütün bütçesini, listelemeden sonra ortaya çıkan tek bir girdiye harcamış.
git commit kendi bakım işini daemon olarak bırakıyor
Commit kuyruğu boşta olduğunu bildirdikten sonra bir şey hâlâ yazıyordu. Yazan, commit'in kendisiydi.
git commit işini git'in otomatik bakımını başlatarak bitiriyor. git v2.55.0'da prepare_auto_maintenance() bu komutu kuruyor ve detach bayrağını açıyor; yalnızca maintenance.autoDetach, o yoksa gc.autoDetach false ise açmıyor. İkisi de ayarlı değilse varsayılan davranış ayrılmak. Ephesus'un probu, commit döndükten sonra hâlâ çalışan süreci tam olarak bu haliyle buldu:
git maintenance run --auto --no-quiet --detach
Ayrılan süreç daemonize() çağırıyor: fork ediyor ve ebeveyn süreç çıkıyor.
Node'un execFile callback'i git commit çıktığında tetikleniyor. Bakım süreci ise bilerek kopmuş bir torun süreç; uygulamadaki hiçbir await ona ulaşamazdı.
Windows'ta daemonize() ENOSYS döndürüyor, çünkü Windows derlemeleri NO_POSIX_GOODIES ayarlıyor. Dolayısıyla Git for Windows aynı bakımı ön planda çalıştırıyor. Projenin Windows geliştirme makinesinin bu hatayı hiç üretmemesi, Linux CI'ın ise üretmesi bundan.
objects/17 içindeki iki loose nesne arka planda repack başlatıyor
Ayrılan aşamanın gerçekten iş yapıp yapmaması git sürümüne bağlı. v2.53.0'da zamanlanmamış bakım gc stratejisini kullanıyor. v2.55.0'da ise geometric kullanılıyor; bu stratejinin geometric-repack görevi, başka tetikleyicilerin yanında, loose nesne sayısı 256'ya yuvarlanan 100 sınırını aştığında çalışıyor.
Bu sayı bir tahmin. odb/source-loose.c yalnızca objects/17 klasöründeki dosyaları sayıp 256 ile çarpıyor. ID'si 17 ile başlayan iki loose nesne 512 ediyor, bu da 256'yı geçiyor ve commit döndükten sonra bir git repack objects/pack içine yazmaya başlıyor.
Ephesus incelemesi bunu iş üstünde yakaladı (uygulama kaydı). Testin enstrümante edilmiş bir kopyası, kuyruğun drained() promise'i çözüldüğü anda /proc'u okudu. 1.600 teardown'ın 8'inde git orada hâlâ çalışıyordu: maintenance, repack ve pack-objects.
Tetikleyici git'in kendi config'i üzerinden zorlandığında, değiştirilmemiş test 50 koşunun 12'sinde ENOTEMPTY ile düştü; hata ya CI'daki gibi .git'te, ya .git/objects/pack'te ya da .git'i içeren Agora dizininin kendisinde çıktı. git yazdığı dizinleri yeniden oluşturuyor; geç kalan bir repack, teardown'ın çoktan sildiği .git'i bile geri getirebiliyor. Bu koşular WSL2'de ve CI'ın binary'si yerine yerelde derlenmiş bir git 2.55.0 ile yapıldı.
İki -c bayrağı commit'i kendi repack'ini beklemeye zorluyor
Çözüm, Agora'ya giden her git çağrısının geçtiği runner'da. git.ts artık her komutun başına -c maintenance.autoDetach=false -c gc.autoDetach=false ekliyor. Komut satırından verilen config kullanıcının global ayarını eziyor; arka plan bakımını açıkça isteyen bir geliştirici bile yarışı geri getiremiyor.
git 2.47 ve sonrası ilk anahtarı okuyor. İkincisi, otomatik gc'si yalnızca onu okuyan eski git sürümleri için.
Değişiklikten sonra aynı enstrümante döngü 1.600 teardown'ın 0'ında canlı bir yazıcı buldu; zorlanan tetikleyiciyle test 50 koşunun 50'sinde geçti. Kayıt, reddedilen alternatifleri de sıralıyor:
- Daha büyük bir
maxRetries. Silme fonksiyonu dizini bir daha listelemiyor; tek bir geç yazma her tekrar sayısını boşa çıkarıyor. objects/maintenance.lockdosyasının kaybolmasını beklemek. git 2.53.0 bu kilidi ayrılan aşama boyunca tutmuyor; bekleme, repack sürerken geçerdi.maintenance.auto=false. Agora süresiz olarak 4 MiB'a kadar log blob'ları commit ediyor; bakım olmazsa nesne deposu sınırsız büyür.
Bedel açılışta ve kapanışta ödeniyor
Bakım ön plana alınınca ara sıra bir commit yavaşlıyor. PR bunu üretimdekine benzeyen bir Agora üzerinde, WSL2'de, her yapılandırma için tek koşuyla ölçtü. git 2.55.0'da 3.200 loose nesne üzerindeki ilk bakım 2.799 ms, 60 commit sonraki artımlı geometric repack 348 ms sürdü. git 2.53.0'da 7.201 loose nesnede tam bir gc 15.456 ms sürdü; bu, runner'ın 20 s'lik zaman aşımına yakın.
Agora'ya teslimat bir dosya rename'i, dolayısıyla hiçbir zaman commit beklemiyor. Açılış ve kapanış ise bekliyor: ikisi de pencere açılmadan ya da uygulama çıkmadan önce bir commit'i bekliyor. Commit zaman aşımına takılırsa zaten yerine oturmuş oluyor; kuyruğun yeniden denemesi temiz bir çalışma ağacı buluyor ve sahipsiz kalan gc, değişiklikten önce olduğu gibi kendi başına bitiyor.
Commit'in dönmesinin "bitti" demek olduğu her yere bayrakları ekleyin
Linux ya da macOS'ta git commit'i bekleyip ardından depoyu silen, arşivleyen ya da hash'leyen bir kodunuz varsa, o çağrıya -c maintenance.autoDetach=false, 2.47'den eski git için de -c gc.autoDetach=false ekleyin. Windows'ta test etmek buna ihtiyacınız olup olmadığını göstermez, çünkü Git for Windows hiç ayrılmıyor.
Ardından kendi commit sürenizi, dağıttığınız boyuttaki bir depoda ölçün. Ephesus, "drained" sözcüğünün başlattığı hiçbir git sürecinin hâlâ yazmadığı anlamına gelmesi karşılığında açılışta ara sıra birkaç saniyelik bir commit'i kabul etti. Kaydı, açılış gecikmesi bir gün sorun olursa diye henüz yapılmamış bir adım öneriyor: iki açılış commit'inde bakımı atlayıp sonraki sıradan commit'e bırakmak.