Ders 05 / 15
Dış Servis Taklidi
Başka bir ekibin servisinin kayıt ve tekrar oynatma ile taklit edilmesi: kayıttan beslenen koşumun süreç ve ağ çağrısı olmadan çalışması, istek biçimi kaymasının kasette karşılık bulamayarak yakalanması ve kaydın dondurduğu yanıtın sözleşme değiştiğinde yeşil kalması.
İçindekiler
Önceki derste gerçek çalıştırılan bağımlılık aynı makinede duruyordu: şeması elde, göçü tekrarlanabilirdi. Kütüphanenin ödünç akışı bir de künye servisine bağlıdır ve o servis başka bir ekibindir. Testin komutuyla ayağa kalkmaz, her koşumda aynı yanıtı vermeyebilir ve bazı koşumlarda hiç yanıt vermez.
Bu bağımlılık için kullanılan yaklaşımın adı kayıt ve tekrar oynatmadır (record and replay): gerçek yanıtlar bir kez kaydedilir, sonraki koşumlar kayıttan beslenir. Kayıt dosyasına kaset denir. Bu ders kasetin yakaladığı ve kaçırdığı kusur sınıflarını ölçer.
Dış Servis ve Taşıyıcı
Künye servisi burada yerel bir süreçle temsil ediliyor; gerçekte ağın öbür ucundadır. İki sürümü vardır ve aradaki fark bir alan adıdır — bu fark dersin sonunda ölçülecek.
// kunye-servisi.mjs — baska bir ekibin kunye servisinin yerine gecen yerel surec import { createServer } from 'node:http'; const KUNYELER = { 'K-903': { yazar: 'Sabahattin Ali', yil: 1943 }, 'K-904': { yazar: 'Halide Edip', yil: 1922 }, }; export function servisKur(bagl, surum) { const sunucu = createServer((istek, yanit) => { const adres = new URL(istek.url, 'http://yerel'); const no = adres.searchParams.get('no'); const kunye = KUNYELER[no]; if (adres.pathname !== '/kunye' || kunye === undefined) { yanit.writeHead(404, { 'content-type': 'application/json' }); return yanit.end(`{"hata":"kunye yok: ${no}"}`); } // v2, baslik alanini ad olarak yeniden adlandirdi const govde = surum === 'v2' ? { no, ad: `kitap ${no}`, yazar: kunye.yazar, yil: kunye.yil } : { no, baslik: `kitap ${no}`, yazar: kunye.yazar, yil: kunye.yil }; yanit.writeHead(200, { 'content-type': 'application/json' }); yanit.end(JSON.stringify(govde)); }); sunucu.listen(bagl, '127.0.0.1'); return sunucu; } if (process.argv[2] !== undefined) servisKur(Number(process.argv[2]), process.argv[3] ?? 'v1');
Taşıyıcı iki kipte çalışır. Kayıt kipinde gerçek servise gider ve her etkileşimi kasete ekler; oynatma kipinde ağa hiç çıkmaz, isteği kasette arar. Anahtar, yöntem ile tam yoldan oluşur — sorgu alanları dahil.
// kaset.mjs — kayit ve tekrar oynatma: kayit kipi gercek servise gider, oynatma kipi dosyadan okur import { existsSync, readFileSync, writeFileSync } from 'node:fs'; export function kasetIstemcisi({ kip, kasetYolu, taban }) { const kaset = existsSync(kasetYolu) ? JSON.parse(readFileSync(kasetYolu, 'utf8')) : []; let agCagrisi = 0; return { async iste(yol) { const anahtar = `GET ${yol}`; if (kip === 'oynat') { const kayit = kaset.find((k) => k.anahtar === anahtar); if (kayit === undefined) throw new Error(`kasette karsilik yok: ${anahtar}`); return { durum: kayit.durum, govde: kayit.govde }; } const yanit = await fetch(`${taban}${yol}`); agCagrisi += 1; const govde = await yanit.json(); kaset.push({ anahtar, durum: yanit.status, govde }); writeFileSync(kasetYolu, `${JSON.stringify(kaset, null, 1)}\n`); return { durum: yanit.status, govde }; }, olcu: () => ({ agCagrisi, etkilesim: kaset.length }), }; }
// hazir.mjs — servisin dinlemeye baslamasini bekler export async function hazirBekle(taban, enCok = 100) { for (let i = 0; i < enCok; i += 1) { try { await (await fetch(`${taban}/kunye?no=K-903`)).text(); return; } catch { await new Promise((c) => setTimeout(c, 20)); } } throw new Error('servis hazir olmadi'); }
Künye istemcisi sorgu alanının adını bir değişkenden alır; kusurlu sürüm bu adı değiştirecek.
// istemci.mjs — kunye istemcisi; ALAN_ADI degiskeni sorgu alaninin adini secer export const SORGU_ALANI = process.env.ALAN_ADI ?? 'no'; export function kunyeIstemcisi(tasiyici) { return { async kunyeGetir(kitapNo) { const { durum, govde } = await tasiyici.iste(`/kunye?${SORGU_ALANI}=${kitapNo}`); if (durum !== 200) throw new Error(`kunye alinamadi: ${durum}`); return govde; }, }; }
// kunye.test.mjs — ayni test uc kipte: kayit, tekrar oynatma ve canli servis import test, { before } from 'node:test'; import assert from 'node:assert/strict'; import { kasetIstemcisi } from './kaset.mjs'; import { kunyeIstemcisi } from './istemci.mjs'; import { hazirBekle } from './hazir.mjs'; const kip = process.env.KIP ?? 'oynat'; const kasetYolu = process.env.KASET ?? 'kaset.json'; const taban = `http://127.0.0.1:${process.env.BAGL ?? 8941}`; const istemciKur = () => kunyeIstemcisi(kasetIstemcisi({ kip, kasetYolu, taban })); before(async () => { if (kip === 'kayit') await hazirBekle(taban); }); test('K-903 kunyesinde baslik ve yazar bulunur', async () => { const kunye = await istemciKur().kunyeGetir('K-903'); assert.equal(kunye.baslik, 'kitap K-903'); assert.equal(kunye.yazar, 'Sabahattin Ali'); }); test('K-904 kunyesinde yil bulunur', async () => { const kunye = await istemciKur().kunyeGetir('K-904'); assert.equal(kunye.yil, 1922); });
Kayıt ve Oynatma
Kayıt bir kez yapılır ve gerçek servisi gerektirir.
node kunye-servisi.mjs 8941 v1 & SERVIS=$! KIP=kayit node --test --test-reporter=tap kunye.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))' kill $SERVIS echo "kasetteki etkilesim: $(node -e "console.log(JSON.parse(require('fs').readFileSync('kaset.json')).length)")"
ok 1 - K-903 kunyesinde baslik ve yazar bulunur ok 2 - K-904 kunyesinde yil bulunur # tests 2 # pass 2 # fail 0 kasetteki etkilesim: 2
Sonraki koşumlar servisi hiç istemez.
node --test --test-reporter=tap kunye.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))' echo "kosum sirasinda ayakta olan kunye servisi: $(pgrep -f 'kunye-servisi.mjs' | wc -l | tr -d ' ')"
ok 1 - K-903 kunyesinde baslik ve yazar bulunur ok 2 - K-904 kunyesinde yil bulunur # tests 2 # pass 2 # fail 0 kosum sirasinda ayakta olan kunye servisi: 0
İkinci ve üçüncü derste ödenen bedel burada ödenmiyor: ayağa kalkan süreç yok, hazır olma yoklaması yok, yıkım adımı yok. Karşılığında kasete bir bağımlılık doğdu ve kasetin geçerliliği bir varsayımdır.
Yakalanan Sınıf: İstek Biçimi Kayması
Künye istemcisinin sorgu alanı no yerine kitap olarak değiştirilsin. Gerçek servis
buna 404 döndürürdü; kaset karşılık bulamıyor.
ALAN_ADI=kitap node --test --test-reporter=tap kunye.test.mjs \ | grep -E "^ *(not ok|error:|# fail)" | sed 's/^ *//;s/^/kusurlu /' ALAN_ADI=no node --test --test-reporter=tap kunye.test.mjs \ | grep -E '^ *(ok|# (pass|fail))' | sed 's/^ *//;s/^/duzeltilmis /'
kusurlu not ok 1 - K-903 kunyesinde baslik ve yazar bulunur kusurlu error: 'kasette karsilik yok: GET /kunye?kitap=K-903' kusurlu not ok 2 - K-904 kunyesinde yil bulunur kusurlu error: 'kasette karsilik yok: GET /kunye?kitap=K-904' kusurlu # fail 2 duzeltilmis ok 1 - K-903 kunyesinde baslik ve yazar bulunur duzeltilmis ok 2 - K-904 kunyesinde yil bulunur duzeltilmis # pass 2 duzeltilmis # fail 0
Yakalanan kusur sınıfının adı istek biçimi kaymasıdır. Kaset yalnız yanıtları değil, o yanıtları getiren istekleri de sabitliyor; anahtarın içinde yol ve sorgu alanları bulunduğu için istek tarafındaki her değişiklik karşılıksız kalıyor. Bu, kasetin en güçlü yanıdır ve aynı zamanda en çok gürültü üreten yanıdır: kodun dış servise yaptığı çağrı gerçekten değiştiğinde de aynı hata çıkar ve kaset yeniden kaydedilmek zorunda kalır — yeniden kayıt ise gerçek servisi yeniden gerektirir.
Kaçan Sınıf: Dondurulmuş Yanıt
Kaset, servisin kayıt anındaki davranışını dondurur. Servis sahibi ekip yanıt gövdesini değiştirdiğinde kasetin bundan haberi olmaz.
node --test --test-reporter=tap kunye.test.mjs | grep -E '^ *(ok|not ok|# (pass|fail))' | sed 's/^/oynatma /' node kunye-servisi.mjs 8942 v2 & SERVIS=$! KIP=kayit KASET=kaset-v2.json BAGL=8942 node --test --test-reporter=tap kunye.test.mjs \ | grep -E '^ *(ok|not ok|# (pass|fail))' | sed 's/^/canli v2 /' kill $SERVIS rm -f kaset-v2.json
oynatma ok 1 - K-903 kunyesinde baslik ve yazar bulunur oynatma ok 2 - K-904 kunyesinde yil bulunur oynatma # pass 2 oynatma # fail 0 canli v2 not ok 1 - K-903 kunyesinde baslik ve yazar bulunur canli v2 ok 2 - K-904 kunyesinde yil bulunur canli v2 # pass 1 canli v2 # fail 1
Aynı test, aynı kod, iki farklı yanıt. Servisin ikinci sürümü baslik alanını ad olarak
yeniden adlandırdı; canlı koşum bunu görüyor, kasetten beslenen koşum görmüyor. Kaçan kusur
sınıfının adı dondurulmuş yanıttır: kaset dünkü davranışı taşıyor ve o davranış artık
doğru değil.
Bu sınıfın ağırlığı, sessiz olmasından gelir. Şema uyuşmazlığı bir kısıt ihlaliyle, göç kusuru bir iddia düşmesiyle, istek kayması bir eksik karşılıkla kendini duyurdu. Dondurulmuş yanıtta düşen hiçbir şey yoktur — takım yeşildir ve yeşil olduğu için kimse bakmaz.
Bedel
// bedel.mjs — kayit ve oynatma kiplerinin bedeli: surec, ag cagrisi, dosya ve goreli sure import { spawn } from 'node:child_process'; import { statSync, rmSync } from 'node:fs'; import { kasetIstemcisi } from './kaset.mjs'; import { kunyeIstemcisi } from './istemci.mjs'; import { hazirBekle } from './hazir.mjs'; const N = 100; const taban = 'http://127.0.0.1:8943'; const surec = spawn(process.execPath, ['kunye-servisi.mjs', '8943', 'v1'], { stdio: 'ignore' }); await hazirBekle(taban); async function olc(kip, kasetYolu) { const tasiyici = kasetIstemcisi({ kip, kasetYolu, taban }); const istemci = kunyeIstemcisi(tasiyici); await istemci.kunyeGetir('K-903'); const baslangic = performance.now(); for (let i = 0; i < N; i += 1) await istemci.kunyeGetir('K-903'); return { sure: performance.now() - baslangic, ...tasiyici.olcu() }; } const canli = await olc('kayit', 'kaset-olcum.json'); const oynatma = await olc('oynat', 'kaset.json'); surec.kill('SIGTERM'); const boyut = statSync('kaset.json').size; rmSync('kaset-olcum.json', { force: true }); console.log(`kayit kipi : 1 surec, ${canli.agCagrisi} ag cagrisi (${N} istek + 1 isinma)`); console.log(`oynatma kipi : 0 surec, ${oynatma.agCagrisi} ag cagrisi, 1 dosya okumasi`); console.log(`kaset : ${oynatma.etkilesim} etkilesim, ${boyut} bayt`); console.log(`oynatma daha kisa surdu : ${oynatma.sure < canli.sure}`); console.log(`sure farki en az yirmi kat : ${canli.sure / oynatma.sure >= 20}`);
kayit kipi : 1 surec, 101 ag cagrisi (100 istek + 1 isinma) oynatma kipi : 0 surec, 0 ag cagrisi, 1 dosya okumasi kaset : 2 etkilesim, 334 bayt oynatma daha kisa surdu : true sure farki en az yirmi kat : true
Oynatma kipinin koşum bedeli üç dersin en düşüğüdür: sıfır süreç, sıfır ağ çağrısı, tek dosya okuması. Bedel koşumdan çıkıp başka bir yere taşınmıştır — üç yüz otuz dört baytlık kasetin tazeliğine. Bu bedel her koşumda değil, servis değiştiğinde ödenir ve ödenmediği zaman görünmez. Kaset büyüdükçe de büyür: iki etkileşim gözle denetlenebilir, iki yüz etkileşim denetlenemez.
Özet
- Kayıt ve tekrar oynatma, gerçek yanıtları bir kez kaydedip sonraki koşumları kasetten besler; oynatma kipinde süreç ve ağ çağrısı sıfırdır.
- Kaset yalnız yanıtları değil istekleri de sabitler; sorgu alanının adı değiştiğinde iki test de kasette karşılık bulamadı.
- Kaydın dondurduğu yanıt eskiyebilir: servisin ikinci sürümü bir alanı yeniden adlandırdığında canlı koşum kırmızıya döndü, kasetten beslenen koşum yeşil kaldı.
- Dondurulmuş yanıt sessiz bir kusur sınıfıdır; düşen bir test olmadığı için bakılmaz.
- Bedel koşumdan kasetin tazeliğine taşınır ve kaset büyüdükçe elle denetlenemez hâle gelir.
Sonraki Adım
Bu konudaki beş ders aynı soruyu farklı sınırlarda sordu: hangi bağımlılık gerçek çalışır ve bunun karşılığı nedir. Kaset, sorunun sınırına geldi. Kayıt, dış servisin dünkü davranışını donduruyor; servis sahibi ekip sözleşmesini değiştirdiğinde kayıt hâlâ yeşil kalıyor ve testler yalan söylemeye başlıyor. Kaseti tazelemek bir çözüm değildir, çünkü tazelemenin ne zaman gerektiğini söyleyen hiçbir sinyal yoktur.
Eksik olan şey, iki tarafın üzerinde anlaştığı sözleşmenin kendisinin sınanmasıdır: tüketicinin beklediği alanlar sağlayıcının verdiği alanlarla örtüşüyor mu, bir alan adı değiştiğinde bunu kim ve ne zaman fark eder. Sonraki konu bu soruyla açılır ve isteğin ile yanıtın doğrulanmasını, beklentinin sağlayıcıya taşınmasını ve kırıcı değişikliğin denetimini ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.