Zaman Damgası Kesmesi: Bir Ruby on Rails ActiveRecord Öyküsü
Yayınlanan: 2022-03-11Testlerin, uygulamaların kesintiye uğramasını önlemeye yardımcı olması gerekiyor. Ancak arada bir, testlerin kendileri de belirsiz hale gelebilir - en basit olanlar bile. PostgreSQL tarafından desteklenen bir Ruby on Rails uygulamasında sorunlu bir teste şu şekilde daldık ve neleri ortaya çıkardık.
Belirli bir iş mantığının ( perform
yöntemiyle çağrılır) bir calendar
modelini (bir Calendar
örneği, bir Ruby on Rails ActiveRecord model sınıfı) değiştirmediğini kontrol etmek istedik ve şunu yazdık:
let(:calendar) { create(:calendar) } specify do expect do perform # call the business action calendar.reload end .not_to change(calendar, :attributes) end
Bu, bir geliştirme ortamından (MacOS) geçiyordu, ancak CI'de (Linux) neredeyse her zaman başarısız oluyordu.
Neyse ki, bir mesajla başarısız olduğu başka bir geliştirme ortamında (Linux) yeniden oluşturmayı başardık:
expected `Calendar#attributes` not to have changed, but did change from {"calendar_auth_id"=>8, "created_at"=>2020-01-02 13:36:22.459149334 +0000, "enabled"=>false, "events_...t_sync_token"=>nil, "title"=>nil, "updated_at"=>2020-01-02 13:36:22.459149334 +0000, "user_id"=>100} to { "calendar_auth_id"=>8, "created_at"=>2020-01-02 13:36:22.459149000 +0000, "enabled"=>false, "events_...t_sync_token"=>nil, "title"=>nil, "updated_at"=>2020-01-02 13:36:22.459149000 +0000, "user_id"=>100}
Balık gibi bir şey gördün mü?
Soruşturma
Daha yakından incelediğimizde, created_at
ve updated_at
zaman damgalarının expect
bloğu içinde biraz değiştiğini fark ettik:
{"created_at"=>2020-01-02 13:36:22.459149334 +0000, "updated_at"=>2020-01-02 13:36:22.459149334 +0000} {"created_at"=>2020-01-02 13:36:22.459149000 +0000, "updated_at"=>2020-01-02 13:36:22.459149000 +0000}
Saniyenin kesirli kısmı 13:36:22.459149334
13:36:22.459149000
.
perform'ın calendar
nesnesini güncellemediğinden perform
, bu yüzden zaman damgalarının veritabanı tarafından kesildiğine dair bir hipotez oluşturduk. Bunu test etmek için bilinen en gelişmiş hata ayıklama tekniğini kullandık, yani hata ayıklama koyar:
let(:calendar) { create(:calendar) } specify do expect do puts "before perform: #{calendar.created_at.to_f}" perform puts "after perform: #{calendar.created_at.to_f}" calendar.reload puts "after reload: #{calendar.created_at.to_f}" end .not_to change(calendar, :attributes) end
Ancak çıktıda kesme görünmüyordu:
before perform: 1577983568.550754 after perform: 1577983568.550754 after reload: 1577983568.550754
Bu oldukça şaşırtıcıydı; #created_at
, attributes['created_at']
öznitelik karma değeriyle aynı değere sahip olması gerekirdi. İddiada kullanılanla aynı değeri yazdırdığımızdan emin olmak için created_at
şeklimizi değiştirdik.
calendar.created_at.to_f
erişimcisini kullanmak yerine, onu doğrudan hash özniteliklerinden getirmeye geçtik: calendar.attributes['created_at'].to_f
. calendar.reload
ile ilgili şüphelerimiz doğrulandı!
before perform: 1577985089.0547702 after perform: 1577985089.0547702 after reload: 1577985089.05477
Gördüğünüz gibi, perform
çağrısı created_at
değiştirmedi, ancak reload
yaptı.
Değişikliğin başka bir calendar
örneğinde gerçekleşmediğinden ve ardından kaydedildiğinden emin olmak için bir deney daha gerçekleştirdik. Testten önce calendar
yeniden yükledik:
let(:calendar) { create(:calendar).reload } specify do expect do perform calendar.reload end .not_to change(calendar, :attributes) end
Bu, testi yeşil yaptı.

Çözüm
Zaman damgalarımızı kesen ve testlerimizde başarısız olanın veritabanı olduğunu bilerek, bu kesintiyi önlemeye karar verdik. Bir DateTime
nesnesi oluşturduk ve onu tam saniyeye yuvarladık. Ardından, Rails'in Aktif Kayıt zaman damgalarını açıkça ayarlamak için bu nesneyi kullandık. Bu değişiklik testleri düzeltti ve stabilize etti:
let(:time) { 1.day.ago.round } let(:calendar) { create(:calendar, created_at: time, updated_at: time) } specify do expect do perform calendar.reload end .not_to change(calendar, :attributes) end
Neden
Bu neden oldu? Aktif Kayıt zaman damgaları, Rails'in ActiveRecord::Timestamp
modülü tarafından Time.now
kullanılarak ayarlanır. Time
kesinliği işletim sistemine bağlıdır ve belgelerin durumu olarak kesirli saniyeler içerebilir .
MacOS ve Linux'ta Time.now
çözünürlüğünü kesirli parça uzunluklarının frekanslarını sayan bir komut dosyasıyla test ettik:
pry> 10000.times.map { Time.now.to_f.to_s.match(/\.(\d+)/)[1].size }.group_by{|a| a}.map{|k, v| [k, v.count]}.to_h # MacOS => {6=>6581, 7=>2682, 5=>662, 4=>67, 3=>7, 2=>1} # Linux => {6=>2399, 7=>7300, 5=>266, 4=>32, 3=>3}
Gördüğünüz gibi, Linux'taki zaman damgalarının yaklaşık %70'i ondalık sayıdan sonra yedi basamaklı kesinliğe sahipken, MacOS'ta yalnızca %25'i vardı. Testlerin çoğu zaman MacOS'ta geçmesinin ve çoğu zaman Linux'ta başarısız olmasının nedeni budur. Test çıktısının dokuz basamaklı kesinliğe sahip olduğunu fark etmiş olabilirsiniz; bunun nedeni, RSpec'in zaman çıktısını biçimlendirmek için Time#nsec
nsec kullanmasıdır.
Rails modelleri veritabanına kaydedildiğinde, sahip oldukları tüm zaman damgaları, PostgreSQL'de timestamp without time zone
adı verilen ve mikrosaniye çözünürlüğe sahip, yani ondalıktan sonraki altı basamaklı bir tür kullanılarak depolanır. Dolayısıyla, 1577987974.6472975
PostgreSQL'e gönderildiğinde, kesirli kısmın son basamağını keser ve bunun yerine 1577987974.647297
kaydeder.
Sorular
calendar.attributes['created_at']
yeniden yüklenmiş olmasına rağmen, calendar.reload
calendar.created_at
yeniden yüklenmediği sorusu hala var.
Ayrıca, Time
kesinliği testinin sonuçları biraz şaşırtıcı. MacOS'ta maksimum hassasiyetin altı olmasını bekliyorduk. Neden bazen yedi basamaklı olduğunu bilmiyoruz. Bizi daha çok şaşırtan, son hanelerin değerinin dağılımıydı:
pry> 10000.times.map { Time.now}.map{|t| t.to_f.to_s.match(/\.(\d+)/)[1] }.select{|s| s.size == 7}.group_by{|e| e[-1]}.map{|k, v| [k, v.size]}.to_h # MacOS => {"9"=>536, "1"=>555, "2"=>778, "8"=>807} # Linux => {"5"=>981, "1"=>311, "3"=>1039, "9"=>309, "8"=>989, "6"=>1031, "2"=>979, "7"=>966, "4"=>978}
Gördüğünüz gibi, MacOS'ta yedinci rakam her zaman 1, 2, 8 veya 9'dur.
Bu sorulardan herhangi birinin cevabını biliyorsanız, lütfen bizimle bir açıklama paylaşın.
Gelecek
Ruby on Rails'in Aktif Kayıt zaman damgalarının uygulama tarafında oluşturulması gerçeği, bu zaman damgaları veritabanına kaydedilen olayların güvenilir ve kesin sıralaması için kullanıldığında da zarar verebilir. Uygulama sunucusu saatlerinin senkronizasyonu bozulabileceğinden, created_at
tarafından sıralanan olaylar, gerçekte meydana geldiklerinden farklı bir sırada görünebilir. Daha güvenilir davranış elde etmek için, veritabanı sunucusunun zaman damgalarını işlemesine izin vermek daha iyi olacaktır (örneğin, PostgreSQL's now()
).
Ancak bu, başka bir makaleye değer bir hikaye.
Bu makaleyi oluşturmaya yardımcı olduğu için Gabriele Renzi'ye özel teşekkürler.