İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat