İçeriğe geç
academia.sh

Ders 11 / 18

Yayımla–Abone Ol

Ücret kesinleştiğinde koşan üç işin doğrudan çağrıyla ve olay veriyolu üzerinden bağlanmasının karşılaştırılması: yayımcının tanıdığı modül ve ad sayısının, ithal kapanışının, dördüncü bir tüketici eklendiğinde yayımcıda düzenlenen satır sayısının ve işleyicileri bulmak için okunması gereken dosya sayısının ölçülmesi.

İçindekiler

Boru hattı sabit ve doğrusal bir zincirdi: her kayıt her adımdan geçiyor, her adımın çıktısı bir sonrakinin girdisi oluyordu. Kitaplıkta bu kalıba uymayan bir iş kümesi var. Bir gönderinin ücreti kesinleştiğinde teslimat operasyonu bağlamında kayıt açılıp rota atanmalı, sözleşmeli müşterinin hacim sayacı artmalı, hangi tarife sürümüyle fiyatlandığı arşive geçmeli. Bu üç iş birbirini beslemiyor, kaydı dönüştürmüyor ve aralarında bir sıra yok; sayıları da zamanla değişiyor. Ücreti kesinleştiren modül bunları kendisi çağırırsa üçünü de ithal etmek zorunda kalır.

Yayımla–abone ol (publish–subscribe) düzeni Önbellekleme, Kuyruklar ve Eşzamansız İşleme kursunun Mesajlaşma konusunda kurulmuş ve ölçülmüştü; konu, abonelik, üretici ve tüketici orada tanımlandı, teslim anlamları ile yeniden deneme de oraya aittir ve burada yeniden ele alınmaz. Tasarım Kalıpları kursundaki Gözlemci kalıbı aynı fikri nesne düzeyinde kurar: haber veren nesne abone listesini kendisi taşır. Buradaki biçemin ayırt edici yüzü bir dosyada görünür — abone listesi ne yayımcıda ne tüketicidedir. Araya adlandırılmış bir konu (topic) ile bir olay veriyolu (event bus) girer; iki taraf birbirini değil veriyolunu tanır.

Bağlandığı kalite niteliği sürdürülebilirliktir ve nitelik sorusu şudur: kesinleşen ücrete tepki veren dördüncü bir iş eklendiğinde ücreti kesinleştiren dosyada kaç satır düzenlenir. Aynı sorunun ters yüzü de ölçülür: yayımcı işleyicileri tanımadığında, “bu olayı kim işliyor” sorusunu yanıtlamak için kaç dosya okunur.

Üç İş

Üç iş her iki düzende birebir aynı dosyalardır; değişen tek şey çağrılma yollarıdır. Her biri kendi durumunu tutar ve dışarıya bir sayaç açar, böylece iki düzenin aynı etkiyi ürettiği sayılabilir.

mkdir -p isler dogrudan veriyolu
// isler/rota.mjs — teslimat operasyonu: teslimat kaydi acar ve rota atar
const AGAC = { yakin: ["34"], orta: ["34", "06"], uzak: ["34", "41", "65"] };
const acilan = [];
export const rotaAta = (olay) => { acilan.push({ kod: olay.kod, rota: AGAC[olay.bolge] }); };
export const acilanKayit = () => acilan.length;
// isler/hacim.mjs — ucretlendirme: sozlesmeli musterinin hacim sayaci
const sayac = new Map();
export const hacimArtir = (olay) => { sayac.set(olay.musteri, (sayac.get(olay.musteri) ?? 0) + 1); };
export const hacim = (musteri) => sayac.get(musteri) ?? 0;
// isler/arsiv.mjs — kesinlesen tutari tarife surumuyle birlikte arsive yazar
const satir = [];
export const arsivle = (olay) => { satir.push(`${olay.kod} ${olay.tarifeSurumu} ${olay.net}`); };
export const arsivSatiri = () => satir.length;

Doğrudan Çağrı

Birinci düzende ücreti kesinleştiren modül üç işi sırayla çağırır. Üç ithal satırı ve üç çağrı, yayımcının bilme yükümlülüğünün tamamıdır.

// dogrudan/kesinlestir.mjs — ucreti kesinlestiren modul uc isi kendisi cagirir
import { rotaAta } from "../isler/rota.mjs";
import { hacimArtir } from "../isler/hacim.mjs";
import { arsivle } from "../isler/arsiv.mjs";

export function kesinlestir(olay) {
  rotaAta(olay);
  hacimArtir(olay);
  arsivle(olay);
  return olay.kod;
}

Olay Veriyolu

İkinci düzende araya bir modül girer. Veriyolu iki işlem açar: abone bir konuya işleyici bağlar, yayimla o konuya gelen olayı bağlı bütün işleyicilere verir. Her işleyici çağrısı yalıtılır ve sayaçla kaydedilir. Bir tüketici hata verdiğinde ne olduğu ve olay sistem sınırını geçtiğinde neyin değiştiği bu kursun Olay Güdümlü Mimari dersinde ölçülür; buradaki veriyolu tek süreçtedir ve yayım eşzamanlıdır.

// veriyolu/veriyolu.mjs — konu adiyla yayim: iki taraf birbirini degil veriyolunu tanir
const abonelik = new Map();
let yayim = 0, teslim = 0, hata = 0;

export function abone(konu, isleyici) {
  if (abonelik.has(konu) === false) abonelik.set(konu, []);
  abonelik.get(konu).push(isleyici);
}

export function yayimla(konu, olay) {
  yayim += 1;
  for (const isleyici of abonelik.get(konu) ?? []) {
    try { isleyici(olay); teslim += 1; } catch { hata += 1; }
  }
  return (abonelik.get(konu) ?? []).length;
}

export const sayac = () => ({ yayim, teslim, hata });

Yayımcı bu iki işlemden yalnız birini tanır. Tanıdığı öteki şey bir dizedir: konunun adı.

// veriyolu/kesinlestir.mjs — yalnizca konu adini ve olay bicimini taniyor
import { yayimla } from "./veriyolu.mjs";

export function kesinlestir(olay) {
  yayimla("ucret-kesinlesti", olay);
  return olay.kod;
}

Abonelikler üçüncü bir dosyada kurulur. Bu dosya bileşim kökünün işini yapar: hem veriyolunu hem üç işi tanır, ama kendisini kimse tanımaz — yayımcı onu ithal etmez.

// veriyolu/kayit.mjs — abonelikler burada kurulur; yayimci bu dosyayi tanimaz
import { abone } from "./veriyolu.mjs";
import { rotaAta } from "../isler/rota.mjs";
import { hacimArtir } from "../isler/hacim.mjs";
import { arsivle } from "../isler/arsiv.mjs";

abone("ucret-kesinlesti", rotaAta);
abone("ucret-kesinlesti", hacimArtir);
abone("ucret-kesinlesti", arsivle);

Ölçüm

Betik üç olayı iki düzende koşturur ve işlerin sayaçlarındaki artışı karşılaştırır. Sonra iki yayımcı dosyasının ithal satırlarını okuyup bilinen modül ile ad sayısını çıkarır, ithal kapanışını hesaplar ve son bölümde “bu olayı kim işliyor” sorusunu iki düzende yanıtlamaya çalışır: doğrudan düzende yayımcı dosyası, veriyolu düzeninde kaynak ağacındaki bütün modüller taranır.

// bag-say.mjs — iki duzenin ayni etkiyi uretmesi, yayimcinin tanidigi ad ve isleyicileri bulmak icin okunan dosya
import { readdirSync, readFileSync } from "node:fs";
import { dirname, join, normalize } from "node:path";
import { kesinlestir as dogrudan } from "./dogrudan/kesinlestir.mjs";
import { kesinlestir as veriyolu } from "./veriyolu/kesinlestir.mjs";
import { sayac } from "./veriyolu/veriyolu.mjs";
import { acilanKayit } from "./isler/rota.mjs";
import { hacim } from "./isler/hacim.mjs";
import { arsivSatiri } from "./isler/arsiv.mjs";
import "./veriyolu/kayit.mjs";

const OLAY = [
  { kod: "G-1", bolge: "orta", musteri: "MUS-1", net: 89.25, tarifeSurumu: "T2" },
  { kod: "G-2", bolge: "uzak", musteri: "MUS-2", net: 119.7, tarifeSurumu: "T2" },
  { kod: "G-3", bolge: "yakin", musteri: "MUS-1", net: 27, tarifeSurumu: "T2" },
];
const durum = () => ({ rota: acilanKayit(), arsiv: arsivSatiri(), hacim: hacim("MUS-1") });
const fark = (a, b) => `rota kaydi +${b.rota - a.rota}, arsiv satiri +${b.arsiv - a.arsiv}, MUS-1 hacim +${b.hacim - a.hacim}`;

const d0 = durum();
for (const o of OLAY) dogrudan(o);
const d1 = durum();
for (const o of OLAY) veriyolu(o);
const d2 = durum();
console.log(`dogrudan duzen: ${fark(d0, d1)}`);
console.log(`veriyolu duzen: ${fark(d1, d2)}`);
console.log(`veriyolu sayaci: ${JSON.stringify(sayac())}`);

const ITHAL = /import\s*\{([^}]*)\}\s*from\s*"(\.[^"]+)"/g;
const kapanis = (kok) => {
  const gorulen = new Set([kok]), yigit = [kok];
  while (yigit.length > 0) {
    const d = yigit.pop();
    for (const [, , y] of readFileSync(d, "utf8").matchAll(ITHAL)) {
      const h = normalize(join(dirname(d), y));
      if (gorulen.has(h) === false) { gorulen.add(h); yigit.push(h); }
    }
  }
  return gorulen.size;
};

console.log("\nyayimci                    bilinen modul  bilinen ad  ithal kapanisi");
for (const yol of ["dogrudan/kesinlestir.mjs", "veriyolu/kesinlestir.mjs"]) {
  const bag = [...readFileSync(yol, "utf8").matchAll(ITHAL)];
  const ad = bag.flatMap((m) => m[1].split(",").map((s) => s.trim()));
  console.log(
    `${yol.padEnd(27)}${String(bag.length).padStart(13)}${String(ad.length).padStart(12)}` +
      `${String(kapanis(yol)).padStart(16)}  [${ad.join(" ")}]`,
  );
}

const mjs = readdirSync(".", { recursive: true }).filter((y) => y.endsWith(".mjs") && y.includes("/"));
const dogrudanIsleyici = (readFileSync("dogrudan/kesinlestir.mjs", "utf8").match(/^ {2}\w+\(olay\);$/gm) ?? []).length;
const aboneDosyasi = mjs.filter((y) => readFileSync(y, "utf8").includes('abone("ucret-kesinlesti"'));
const veriyoluIsleyici = aboneDosyasi.reduce(
  (t, y) => t + (readFileSync(y, "utf8").match(/abone\("ucret-kesinlesti", \w+\)/g) ?? []).length, 0);
console.log(`\n"ucret-kesinlesti olayini kim isliyor" sorusu`);
console.log(`  dogrudan: 1 dosya okunur, ${dogrudanIsleyici} isleyici gorunur`);
console.log(`  veriyolu: yayimcida 0 gorunur, ${mjs.length} dosya taranir, ${aboneDosyasi.length} dosyada ${veriyoluIsleyici} isleyici bulunur`);
node bag-say.mjs
dogrudan duzen: rota kaydi +3, arsiv satiri +3, MUS-1 hacim +2
veriyolu duzen: rota kaydi +3, arsiv satiri +3, MUS-1 hacim +2
veriyolu sayaci: {"yayim":3,"teslim":9,"hata":0}

yayimci                    bilinen modul  bilinen ad  ithal kapanisi
dogrudan/kesinlestir.mjs               3           3               4  [rotaAta hacimArtir arsivle]
veriyolu/kesinlestir.mjs               1           1               2  [yayimla]

"ucret-kesinlesti olayini kim isliyor" sorusu
  dogrudan: 1 dosya okunur, 3 isleyici gorunur
  veriyolu: yayimcida 0 gorunur, 7 dosya taranir, 1 dosyada 3 isleyici bulunur

Sayıların Okunması

İlk iki satır ölçümün geçerlilik koşuludur: üç olay iki düzende de üç rota kaydı, üç arşiv satırı ve MUS-1 için iki hacim artışı üretti. İki düzen aynı işi yapıyor.

Üçüncü satır yayımın karşılığını verir. Üç yayım çağrısı dokuz işleyici çağrısına dönüştü; yayımcının yaptığı çağrı sayısı 3, gerçekleşen iş sayısı 9. Aradaki çarpan yayımcının bilmediği sayıdır ve hata alanı sıfır olduğu için dokuzunun dokuzu tamamlandı.

Tablo bilme yükümlülüğünü sayar. Doğrudan düzende yayımcı üç modül ve üç ad tanıyor, ithal kapanışı 4 dosya: yayımcıyı yüklemek üç işi de yüklemektir. Veriyolu düzeninde bir modül ve bir ad tanınıyor, kapanış 2 dosya. Üç iş, yayımcının ithal kapanışının dışına çıktı; kapanışın içinde kalan tek yeni şey veriyolunun kendisidir. Konu adı "ucret-kesinlesti" bir ithal değil bir dizedir, yani derleme anında denetlenmez — bu, bedelin ilk yarısıdır.

Son üç satır bedelin ikinci yarısıdır. Doğrudan düzende soruyu bir dosya yanıtlıyor: üç çağrı orada, sırasıyla okunuyor. Veriyolu düzeninde yayımcı dosyası hiçbir işleyici göstermiyor; yanıtı bulmak için ağaçtaki yedi modülün taranması ve abonelik satırlarının aranması gerekti. Bulundu — bir dosyada üç işleyici — ama arama maliyeti dosya sayısıyla büyür. Gevşek bağlanmanın ölçüsü budur: yayımcının bildiği ad sayısı 3’ten 1’e inerken, işleyicileri bulmak için okunan dosya sayısı 1’den 7’ye çıktı.

Dördüncü Tüketici

Nitelik sorusu dördüncü bir işle sınanır: kesinleşen ücret taşıyıcıya bildirilecek.

// isler/tasiyici.mjs — dorduncu is: kesinlesen ucreti tasiyiciya bildirir
const bildirim = [];
export const tasiyiciBildir = (olay) => { bildirim.push(`${olay.kod} ${olay.net}`); };
export const bildirimSayisi = () => bildirim.length;

Betik iki düzenin kopyasını çıkarır, dördüncü işi ikisine de bağlar, hangi dosyada kaç satırın değiştiğini sayar ve ayrıca yayımcı dosyasında değişen satırı ayrı bir sütunda gösterir. Sonra iki düzeni yeni ağaçta koşturarak dördüncü işin gerçekten çalıştığını doğrular.

// uzanti.mjs — dorduncu tuketici eklendiginde degisen satir, yayimcida degisen satir ve teslim sayisi
import { cpSync, readFileSync, writeFileSync } from "node:fs";

const duzenle = (yol, don) => writeFileSync(yol, don(readFileSync(yol, "utf8")));
const degisenSatir = (eski, yeni) => {
  const say = (m) => m.split("\n").reduce((h, s) => h.set(s, (h.get(s) ?? 0) + 1), new Map());
  const a = say(eski), b = say(yeni);
  const eksik = (x, y) => [...x].reduce((t, [s, n]) => t + Math.max(0, n - (y.get(s) ?? 0)), 0);
  return eksik(b, a) + eksik(a, b);
};
const ARSIV = 'import { arsivle } from "../isler/arsiv.mjs";';
const TASIYICI = 'import { tasiyiciBildir } from "../isler/tasiyici.mjs";';

for (const d of ["isler", "dogrudan", "veriyolu"]) cpSync(d, `yeni/${d}`, { recursive: true });
duzenle("yeni/dogrudan/kesinlestir.mjs", (m) =>
  m.replace(ARSIV, `${ARSIV}\n${TASIYICI}`).replace("  arsivle(olay);", "  arsivle(olay);\n  tasiyiciBildir(olay);"));
duzenle("yeni/veriyolu/kayit.mjs", (m) =>
  m.replace(ARSIV, `${ARSIV}\n${TASIYICI}`)
    .replace('abone("ucret-kesinlesti", arsivle);', 'abone("ucret-kesinlesti", arsivle);\nabone("ucret-kesinlesti", tasiyiciBildir);'));

const YAYIMCI = { dogrudan: "dogrudan/kesinlestir.mjs", veriyolu: "veriyolu/kesinlestir.mjs" };
const DOSYA = {
  dogrudan: ["dogrudan/kesinlestir.mjs"],
  veriyolu: ["veriyolu/kesinlestir.mjs", "veriyolu/veriyolu.mjs", "veriyolu/kayit.mjs"],
};
console.log("duzen     duzenlenen dosya          degisen satir  yayimcida degisen satir");
for (const [duzen, dosyalar] of Object.entries(DOSYA)) {
  const satirFarki = (y) => degisenSatir(readFileSync(y, "utf8"), readFileSync(`yeni/${y}`, "utf8"));
  const degisen = dosyalar.filter((y) => satirFarki(y) > 0);
  const toplam = degisen.reduce((t, y) => t + satirFarki(y), 0);
  const yayimcida = degisen.includes(YAYIMCI[duzen]) ? satirFarki(YAYIMCI[duzen]) : 0;
  console.log(`${duzen.padEnd(10)}${degisen.join(" ").padEnd(26)}${String(toplam).padStart(13)}${String(yayimcida).padStart(25)}`);
}

const OLAY = [
  { kod: "G-4", bolge: "orta", musteri: "MUS-1", net: 89.25, tarifeSurumu: "T2" },
  { kod: "G-5", bolge: "uzak", musteri: "MUS-2", net: 119.7, tarifeSurumu: "T2" },
];
const { bildirimSayisi } = await import("./yeni/isler/tasiyici.mjs");
const { kesinlestir: dogrudan } = await import("./yeni/dogrudan/kesinlestir.mjs");
const { kesinlestir: veriyolu } = await import("./yeni/veriyolu/kesinlestir.mjs");
const { sayac } = await import("./yeni/veriyolu/veriyolu.mjs");
await import("./yeni/veriyolu/kayit.mjs");

for (const o of OLAY) dogrudan(o);
const dogrudanBildirim = bildirimSayisi();
for (const o of OLAY) veriyolu(o);
console.log(`\ndorduncu is kostu mu: dogrudan ${dogrudanBildirim}/${OLAY.length}, veriyolu ${bildirimSayisi() - dogrudanBildirim}/${OLAY.length}`);
console.log(`veriyolu sayaci: ${JSON.stringify(sayac())}, olay basina teslim = ${sayac().teslim / sayac().yayim}`);
node uzanti.mjs
duzen     duzenlenen dosya          degisen satir  yayimcida degisen satir
dogrudan  dogrudan/kesinlestir.mjs              2                        2
veriyolu  veriyolu/kayit.mjs                    2                        0

dorduncu is kostu mu: dogrudan 2/2, veriyolu 2/2
veriyolu sayaci: {"yayim":2,"teslim":8,"hata":0}, olay basina teslim = 4

Değişen satır sayısı iki düzende de 2’dir: bir ithal satırı ve bir bağlama satırı. Fark son sütunda: doğrudan düzende bu iki satır ücreti kesinleştiren dosyanın içindedir, veriyolu düzeninde o dosyaya dokunulmadı. Yayımcı dosyası değişmeden olay başına teslim sayısı 3’ten 4’e çıktı.

Bu, kazancın nereye yerleştiğini gösterir: değişiklik miktarı aynı kaldı, değişikliğin yeri değişti. Yayımcı ücretlendirme bağlamının içindedir; veriyolu/kayit.mjs ise hiçbir alan kuralı taşımayan bir bağlama dosyasıdır. On tüketicili bir düzende her yeni iş yine iki satırdır, ama ücret hesabının durduğu dosya hiç açılmaz.

Özet

  • Yayımla–abone ol biçeminde abone listesi ne yayımcıda ne tüketicidedir; araya bir konu adı ile bir olay veriyolu girer ve iki taraf birbirini değil veriyolunu tanır.
  • Üç olay iki düzende de aynı etkiyi üretti; üç yayım çağrısı dokuz işleyici çağrısına dönüştü ve yayımcı bu çarpanı bilmiyor.
  • Yayımcının bildiği modül sayısı 3’ten 1’e, ad sayısı 3’ten 1’e, ithal kapanışı 4 dosyadan 2 dosyaya indi; konu adı bir ithal değil bir dize olduğu için derleme anında denetlenmez.
  • Bedel izlenebilirliktir: “bu olayı kim işliyor” sorusu doğrudan düzende 1 dosya okunarak, veriyolu düzeninde 7 modül taranarak yanıtlandı.
  • Dördüncü tüketici iki düzende de 2 satır değiştirdi, ama doğrudan düzende bu satırlar yayımcı dosyasına, veriyolu düzeninde bağlama dosyasına düştü; yayımcı dosyası değişmeden olay başına teslim 3’ten 4’e çıktı.

Sonraki Adım

Bu veriyolunun ölçülmemiş bir özelliği var: yayimla çağrısı, bağlı bütün işleyiciler bitmeden geri dönmüyor. Dört işleyici aynı çağrı yığıtında sırayla koşuyor, dolayısıyla ücreti kesinleştiren modülün işi en yavaş işleyicinin işi kadar sürüyor. Arşiv yazımı yavaşlarsa ücret kesinleştirme de yavaşlar; oysa arşivin ne zaman yazıldığı ücretlendirme için önemli değildir. İşleyiciyi araya bir depo koyarak beklemekten çıkarmak, yani olayı bir yere bırakıp dönmek mümkün. Sonraki ders bu depoyu kurar ve ölçer: yayımcı olayı bıraktığında kaç tüketici adımı koşmuş oluyor, tüketici üreticiden yavaş olduğunda beklemede kaç iş birikiyor, iki tüketici aynı deponun ucunda çalıştığında sıra korunuyor mu ve aynı ileti iki kez işlendiğinde teslimat kaydı kaç kez açılıyor.

İ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