Ders 10 / 13
Arka Plan İşleri
Kuyruğa konan bir işin kaydının hangi isteğe kadar kaldığının, işçi onu boşaltınca ne olduğunun ve işin sonucunun neden aynı kalışta olmadığının ölçümü.
Yayın Güncelleme
Bu derste
- İş, kuyruk ve işçi kavramlarını bir isteğin yanıtından ayırarak tanımlar
- Kuyruktaki iş kaydının hangi isteğe kadar kaldığını kalış kuralıyla ölçer
- İşçi kuyruğu boşalttığında kaydın kalışının nasıl kısaldığını sınıflandırır
- İşin argümanı ile sonucunun farklı kalışlarda olduğunu gösterir
İçindekiler
Önceki derste kimliğin oturumda kaldığını, yetki kararının ise her istekte yeniden çıktığını ölçtük. Her iki değer de bir isteğin içinde, o isteğin yanıtını beklerken hesaplanıyordu. Fakat bir uygulamanın yapacağı her iş yanıtı beklemek zorunda değildir. Bir fatura göndermek, bir görseli küçültmek ya da bir raporu hazırlamak, kullanıcı yanıtı aldıktan sonra da yapılabilir. Bu derste soru şu: istek bittikten sonra koşacak bir iş, o ana kadar nerede bekler ve bu bekleyiş kaç istek sürer?
Arka plan işi (background job), isteğin yanıtını geciktirmemek için sonraya bırakılan işe verilen addır. İşi sonraya bırakmak, onu yapılacak bir kayda çevirmektir: iş adı ve argümanlardan oluşan bir kayıt, bir kuyruğa (queue) konur. Kuyruğu daha sonra bir işçi (worker) boşaltır ve kayıttaki işi koşturur. M12/K04’te de bir parçanın bu istekte koşmayıp ertelendiği görülmüştü; orada ölçülen şey kodun koşup koşmadığıydı. Burada kodu bırakıp değeri ölçüyoruz: bu istekte koşmasa bile kuyruktaki kayıt hangi isteğe kadar yaşıyor? Ölçü ekseni kursun ekseni: bu değer hangi isteğe kadar kalıyor? Dört kalışı hatırlayalım: bu istek, sonraki istek, oturum ve kalıcı.
Çekirdek
Ölçümler yine aynı küçük istek döngüsüne dayanır. Dosya kursun çekirdeğidir ve olduğu gibi verilir. Kuyruk bu çekirdekte Uygulama#depo içindeki bir dizidir; gerçek bir uygulamada kuyruk bir dış depoda durur ve çekirdek onu modellemez, yalnız davranışını taşır.
# kalis.rb — kucuk bir istek dongusu ve bir degerin hangi istege kadar kaldigini siniflandiran yardimci
# Flash: bu istekte yazilan deger yalniz bir sonraki istege tasinir.
class Flash
def initialize(gelen)
@gelen = gelen
@yeni = {}
end
def [](anahtar) = @yeni.fetch(anahtar) { @gelen[anahtar] }
def []=(anahtar, deger)
@yeni[anahtar] = deger
end
def birakilan = @yeni
end
# Her istek yeni bir baglam alir: params, flash ve istek deposu her istekte bostur.
class Baglam
attr_reader :uygulama, :params, :oturum, :flash, :istek_deposu
def initialize(uygulama, oturum)
@uygulama = uygulama
@params = {}
@oturum = oturum
@flash = Flash.new(oturum.delete(:flash) || {})
@istek_deposu = {}
end
end
# Uygulamanin deposu butun isteklerde ve butun istemcilerde aynidir;
# oturum istemciye gore secilir.
class Uygulama
attr_reader :depo
def initialize
@oturumlar = Hash.new { |h, istemci| h[istemci] = {} }
@depo = { urunler: {}, sema: { urunler: %i[ad fiyat] }, sablonlar: {},
kuyruk: [], onbellek: {}, sayac: 0 }
end
def istek(istemci)
baglam = Baglam.new(self, @oturumlar[istemci])
sonuc = yield baglam
baglam.oturum[:flash] = baglam.flash.birakilan unless baglam.flash.birakilan.empty?
sonuc
end
end
# Kural: istek 1'de (istemci A) deger yazilir; istek 2 ve 3 (A) ile istek 4 (B) okur.
# B goruyorsa kalici; A'nin ucuncu istegi goruyorsa oturum; yalniz ikinci istek
# goruyorsa sonraki; hicbiri gormuyorsa bu_istek.
def kalis(yaz, oku)
uygulama = Uygulama.new
ilk = uygulama.istek("A") { |b| yaz.call(b); oku.call(b) }
raise ArgumentError, "deger yazildigi istekte okunamadi" if ilk.nil?
gorulen = [["A", 2], ["A", 3], ["B", 4]].map do |istemci, sira|
uygulama.istek(istemci) { |b| oku.call(b) }.nil? ? "--" : "#{istemci}#{sira}"
end
tur = if gorulen[2] != "--" then "kalici"
elsif gorulen[1] != "--" then "oturum"
elsif gorulen[0] != "--" then "sonraki"
else "bu_istek"
end
[tur, ["A1", *gorulen].join(" "), ilk]
end
Bu blok kaynaktır; dosya diske yazılır ve require_relative ile çekilir. Çekirdek bir çerçeve değil, bir çerçevenin modelidir. Kural değerin davranışını okur, saklandığı yeri okumaz; ölçümler tek bir uygulama süreci içinde anlamlıdır.
Kuyruk, İş ve İşçi
Bir iş, bir ad ve bir argüman listesidir. Kuyruğa alma, bu ikiliyi kuyruk dizisinin sonuna eklemektir; işçi ise baştan okur, işin adından koşturulacak kodu bulur ve argümanlarla çağırır. Aşağıdaki kesit bu üç parçayı tanımlar. İş tablosu ISLER, adı koda çeviren bir karma yapıdır.
# isler.rb — kuyruga alma, is tablosu ve isci
require_relative "kalis"
def kuyruga_al(b, is_adi, *argumanlar) = b.uygulama.depo[:kuyruk] << [is_adi, argumanlar]
ISLER = { "fatura_gonder" => ->(fatura_id) { "fatura #{fatura_id} gonderildi" } }
def isci_calistir(b)
kuyruk = b.uygulama.depo[:kuyruk]
sonuclar = []
until kuyruk.empty?
is_adi, argumanlar = kuyruk.shift
sonuclar << ISLER.fetch(is_adi).call(*argumanlar)
end
sonuclar
end
def goster(ad, tur, kanit, deger)
printf("%-14s%-14s%-13s%s\n", ad, tur, kanit, deger.inspect)
end
Bu blok da kaynaktır. isci_calistir kuyruk boşalana kadar baştaki kaydı çıkarır, ISLER tablosunda arar ve sonuçları bir dizide toplar. Önce işi isteğin içinde değil, isteğin dışında koşturduğumuzu, yani yanıtın işin bitmesini beklemediğini görelim. Bir istek yalnız kaydı bırakıp yanıt veriyor; kayıt, ayrı bir çağrıda çalışan işçiye kadar kuyrukta bekliyor.
# erteleme.rb — istek yalniz kaydi birakir, isi isci kosturur
require_relative "isler"
uygulama = Uygulama.new
yanit = uygulama.istek("A") { |b| kuyruga_al(b, "fatura_gonder", 1); "yanit hazir" }
p yanit
p uygulama.depo[:kuyruk].size
p uygulama.istek("isci") { |b| isci_calistir(b) }
p uygulama.depo[:kuyruk].size
"yanit hazir" 1 ["fatura 1 gonderildi"] 0
İstek yanıtını verdi ve kuyrukta bir kayıt kaldı. İşçi ayrı bir çağrıda kaydı çıkarıp işi koşturdu, kuyruk yeniden boşaldı. Yanıt ile işin arasındaki bu aralık, arka plan işinin temel gerekçesidir; kullanıcı yanıtı, işin süresini beklemeden alır. UY4. Çekirdekte işçi, istekten sonra elle çağrılan bir yöntemdir. Gerçek bir işçi çoğu zaman ayrı bir süreçtir ve ne zaman koşacağını ölçüm belirlemez; burada bu zamanlama modellenmedi.
Kuyruktaki Kaydın Kalışı
Birinci gözlem, kuyruğa alınan işin kaydıdır. Yaz lambda’sı kaydı bırakır, oku lambda’sı kuyruktaki ilk kaydı döndürür.
# kuyruk.rb — kuyruktaki is hangi istege kadar kaliyor
require_relative "isler"
yaz = ->(b) { kuyruga_al(b, "fatura_gonder", 1) }
oku = ->(b) { b.uygulama.depo[:kuyruk].first }
goster("kuyruktaki is", *kalis(yaz, oku))
kuyruktaki is kalici A1 A2 A3 B4 ["fatura_gonder", [1]]
Kayıt dört isteğin dördünde de göründü; B’nin isteği de dahil. Bu kalıcı kalıştır. Oturumdan farkı açıktır: kayıt bir istemciye bağlı değildir, uygulamanın ortak deposundadır. Bir istemcinin isteğiyle kuyruğa konan fatura işini, başka bir istemcinin isteğini işleyen süreç de görür; zaten işçinin işi budur. M12/K04’te kayıt “ertelendi” sınıfındaydı, yani bu istekte koşmadı. Burada o erteleme, kaydın isteğin ötesine geçmesiyle aynı şeydir: koşmayan kayıt, işçi gelene kadar uygulamanın ortak deposunda bekler.
Kuyruğun sırası da bu kalışın parçasıdır. İki işi sırayla alıp kuyruğa bakalım.
# sira.rb — iki is kuyruga girdigi sirayla durur
require_relative "isler"
uygulama = Uygulama.new
uygulama.istek("A") do |b|
kuyruga_al(b, "x")
kuyruga_al(b, "y", 2)
end
p uygulama.depo[:kuyruk]
[["x", []], ["y", [2]]]
Argümansız bir iş boş bir liste taşır, y işi ise 2 değerini. Sıra korundu: işçi önce x’i, sonra y’yi alır. UY5. Yalnız iki işle ölçüldü; çok sayıda işte sırayı bozabilecek bir şey, örneğin birden çok işçinin aynı anda kuyruğu okuması, bu çekirdekte yoktur.
Kayıt Bir Kez Koşar
Kuyruk bir kaydı tutar; işçi onu çıkarır. Çıkarma, kaydı kuyruktan siler. Bunun bir sonucu vardır: aynı işçiyi ikinci kez çağırmak aynı işi yeniden koşturmaz, çünkü ortada koşturulacak kayıt kalmamıştır. Ölçmek için bir iş kuyruğa alınır ve işçi art arda iki istekte çağrılır.
# tekrar.rb — isci iki kez cagrilirsa is kac kez kosar
require_relative "isler"
uygulama = Uygulama.new
uygulama.istek("A") { |b| kuyruga_al(b, "fatura_gonder", 1) }
p uygulama.istek("isci1") { |b| isci_calistir(b) }
p uygulama.istek("isci2") { |b| isci_calistir(b) }
p uygulama.depo[:kuyruk].size
["fatura 1 gonderildi"] [] 0
İlk çağrı işi koşturdu, ikinci çağrı boş bir liste döndürdü. İşin bir kez koşması, kaydın kuyruktan çıkmasının doğrudan sonucudur; bunun için ayrıca bir “bu iş yapıldı” işareti gerekmedi. Bu da işin kalışını kuyrukta bulunmasına bağlar: kayıt kuyruktan çıktığı anda iş, bu çekirdek için artık yoktur. Bir işin koştuğunu sonradan öğrenmek isteyen kod, kuyrukta bu bilgiyi bulamaz. Bu, tek işçili bu çekirdeğin sonucudur; gerçek kuyruk sistemlerinin teslim garantisi ayrıca incelenir.
Kuyruğa Konan Argümanın Kalışı
Kuyruğa alınan bir işin argümanı, kayıtla birlikte kalıcıdır. Fakat kayıt argümanın bir kopyasını mı, yoksa kendisini mi tutuyor? Bu soru önemlidir, çünkü isteği yapan kod argümanı kuyruğa koyduktan sonra değiştirebilir. Ölçüm şöyle yapılır: bir liste kuyruğa argüman olarak verilir, sonra aynı liste değiştirilir ve kuyruktaki kayda bakılır.
# arguman.rb — kuyruga konan arguman, sonradan degisirse kayit ne gorur
require_relative "isler"
uygulama = Uygulama.new
liste = [1]
uygulama.istek("A") { |b| kuyruga_al(b, "fatura_gonder", liste) }
liste << 2
p uygulama.depo[:kuyruk]
p uygulama.depo[:kuyruk].first[1].first.equal?(liste)
[["fatura_gonder", [[1, 2]]]] true
Kayıt, listenin kendisini tuttu; kopyasını değil. İstek bittikten sonra yapılan değişiklik kuyruktaki işe de ulaştı. Dolayısıyla iş koşarken gördüğü argüman, kuyruğa alındığı andaki değer olmayabilir. Kalış açısından bunun anlamı şudur: kayıt kalıcıdır, ama kaydın içindeki nesne kalıcı bir kopya değil, paylaşılan bir nesnedir ve onu tutan her yer onun değişimini görür. Bu çekirdekte bir işin argümanını sabitlemek isteyen kod, kuyruğa almadan önce değeri kopyalar.
Bilinmeyen İş Adı
İşçi, işin adından koşturulacak kodu ISLER tablosunda arar. Tabloda olmayan bir ad kuyruğa girebilir; kuyruk adı denetlemez. Hata, işçi o kayda geldiğinde ortaya çıkar. Ölçüm iki kayıt kullanır: ilki tabloda olmayan bir ad, ikincisi geçerli bir iş. İstisnanın yalnız sınıfı basılır.
# bilinmeyen.rb — tabloda olmayan is adi isciyi nasil durdurur
require_relative "isler"
uygulama = Uygulama.new
uygulama.istek("A") do |b|
kuyruga_al(b, "bilinmeyen")
kuyruga_al(b, "fatura_gonder", 1)
end
begin
uygulama.istek("isci") { |b| isci_calistir(b) }
rescue => e
puts e.class
end
p uygulama.depo[:kuyruk]
KeyError [["fatura_gonder", [1]]]
İşçi ilk kayıtta durdu. Kayıt hata vermeden önce kuyruktan çıkarılmıştı, bu yüzden hatalı kayıt kuyrukta kalmadı; ikinci kayıt ise hiç okunmadığı için yerinde duruyor. Bu çekirdekte hatalı bir iş kendi kaydını kaybeder ve kendisinden sonraki işleri geciktirir. Bu çekirdekte bir yeniden deneme düzeneği yoktur; kaybolan kaydı geri getirecek bir şey yazılmadı. Kalış bakımından bu, kaydın kalıcılığının bir sınırıdır: kayıt kuyruktayken kalıcıdır, ama işçinin eline geçtiği an kuyruktaki varlığı biter. Bir işin kuyruktan çıkması işin bittiği anlamına gelmez; kuyruk yalnız çıkarma anını bilir, işin başarıyla bitip bitmediğini bilmez. Hatayı yeniden denemek isteyen çekirdek, kaydı koşturmadan önce kuyruktan silmek yerine, işin bitişini bekleyen ayrı bir kayıt düzeni kurmak zorundadır; gerçek kuyruk sistemlerinde bu düzenek yapılandırılabilir.
İşçi Kuyruğu Boşaltınca
Kalıcı kalış sonsuz değildir. Kaydı boşaltan bir işçi, kalışı kısaltır. Ölçmek için oku lambda’sı çağrı sayısını tutar: ikinci istekte kayda baktıktan sonra işçiyi çalıştırır ve kuyruğu boşaltır.
# bosaltma.rb — ikinci istekte isci kuyrugu bosaltirsa kalis ne olur
require_relative "isler"
yaz = ->(b) { kuyruga_al(b, "fatura_gonder", 1) }
cagri = 0
oku = lambda do |b|
cagri += 1
kayit = b.uygulama.depo[:kuyruk].first
isci_calistir(b) if cagri == 2
kayit
end
goster("bosaltilan", *kalis(yaz, oku))
bosaltilan sonraki A1 A2 -- -- ["fatura_gonder", [1]]
Kural bunu sonraki istek olarak sınıflandırdı: kayıt birinci ve ikinci istekte göründü, işçi çalıştıktan sonra üçüncü ve dördüncü istekte yok. Aynı kayıt işçisiz kalıcı idi. Kalışı belirleyen şey saklandığı yer değil, davranıştır: kuyruğu boşaltan bir süreç varsa kayıt, onu işleyen süreç gelene kadar kalır. UY6. Boşaltma ikinci istekte düzenek olarak yaptırıldı. Gerçekte işçinin hangi istekte çalışacağı, kaç isteğin kaydı görebileceğini belirler ve bu sayıyı kuyruğun uzunluğuna ve işçinin hızına bırakmak gerekir.
Sonucu İşçinin İsteğinde Saklamak
Aşağıdaki sınırlayıcı ölçümde sonuç istek deposunda kalıyor. Peki sonucu işçinin isteğinin içinde uygulamanın ortak deposuna yazarsak kalış değişir mi? Ölçüm, öncekiyle aynı kuyruğa alma ve işçi çağrısını kullanır; fark yalnız sonucun nereye yazıldığıdır.
# sonuc_depo.rb — sonucu ortak depoya yazmak kalisi nasil degistirir
require_relative "isler"
yaz = lambda do |b|
kuyruga_al(b, "fatura_gonder", 1)
b.uygulama.depo[:sonuc] = isci_calistir(b).first
end
oku = ->(b) { b.uygulama.depo[:sonuc] }
goster("sonuc depoda", *kalis(yaz, oku))
sonuc depoda kalici A1 A2 A3 B4 "fatura 1 gonderildi"
Sonuç bu kez kalıcı kalıştadır: dört isteğin dördünde de göründü. Değişen yalnız yazılan yerdir. Sonucun hangi isteğe kadar kalacağını işçi değil, sonucu yazan kod belirler. Bu, bir sonraki bölümdeki sonucun doğrudan karşılığıdır: kuyruk “ne oldu” bilgisini taşımaz, onu taşımak isteyen kod bunu uygulamanın ortak deposuna açıkça yazar.
Sınırlayıcı Ölçüm
Şimdiye kadar kalıcı olan şey işin kaydı, yani adı ve argümanlarıydı. Peki işin sonucu nerede? ISLER tablosundaki lambda bir dize döndürüyor, ama isci_calistir o dizeyi yalnız bir dizide topluyor. Sonucu da aynı kalış kuralıyla ölçelim: iş kuyruğa alınıp aynı istekte işçi tarafından koşturuluyor ve sonuç isteğin istek deposuna konuyor.
# sonuc.rb — isin sonucu hangi istege kadar kaliyor
require_relative "isler"
yaz = lambda do |b|
kuyruga_al(b, "fatura_gonder", 1)
b.istek_deposu[:sonuc] = isci_calistir(b).first
end
oku = ->(b) { b.istek_deposu[:sonuc] }
goster("is sonucu", *kalis(yaz, oku))
is sonucu bu_istek A1 -- -- -- "fatura 1 gonderildi"
Sonuç bu istek kalışında. İşin argümanı kalıcıydı, sonucu değil. Bunun nedeni basittir: işçi sonucu döndürür, ama döndürülen değeri bir yere yazmak işin kendi görevidir. Sonucun sonraki isteklerde görünmesi isteniyorsa iş, sonucu kendisi saklamalıdır. Bu çekirdekte kuyruk yalnız “ne yapılacak” bilgisini taşır, “ne oldu” bilgisini taşımaz. Bu ayrım bir varsayım hatasına yol açabilir: kuyruğa bir iş atıp sonucunu daha sonra kuyruktan okumayı beklemek, kuyruğun taşımadığı bir değeri aramaktır.
Özet
- Arka plan işi, yanıtı geciktirmemek için sonraya bırakılan işin adı ve argümanlarından oluşan bir kayıttır; kuyruk kaydı tutar, işçi boşaltıp koşturur.
- Kuyruğa alınan işin kaydı, istemciden bağımsız olarak uygulamanın ortak deposundadır; bu dersin kalışı kalıcıdır. Kuyruk işleri girdikleri sırayla tutar.
- İşçi çalışınca kayıt kuyruktan çıkar ve bir kez koşar; ikinci işçi çağrısı boş döner. İşçi kaydı ikinci istekte boşaltırsa kural onu sonraki istek olarak görür.
- Kuyruk argümanın kopyasını değil kendisini tutar; sonradan yapılan değişiklik işe ulaşır. Bilinmeyen iş adı
KeyErrorile işçiyi durdurur ve hatalı kayıt kuyruktan çıkmış olur. - İşin argümanı kalıcıdır, sonucu değildir; sonucu saklamak işin kendi işidir.
- Sonuç yazıldığı yere bağlıdır: ortak depoya yazılırsa kalıcı, istek deposuna yazılırsa bu istek kalır.
Sonraki Adım
Kuyruk, bir işi ertelemek için kaydı kalıcı kılıyor. Peki aynı hesabı her istekte yeniden yapmak yerine sonucunu kalıcı kılmak istersek ne olur? Bir sonraki ders önbelleklemeyi ölçer: bir parçanın bir kez üretilip sonraki isteklere sunulması ve bu bayatlamanın nasıl yönetildiği.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.