omeryanbas.com

Ömer Yanbaş

Genel Müdür, Ticofab Yazılım

MesajlaşmaYöntem

Her mesajı ikiye bölen görünmez sonek

İki ayrı özellik, uzunluk hesabı yapıldıktan sonra beşer karakter ekledi. Her mesaj parça sınırını aştı ve maliyet sessizce ikiye katlandı.

Yüz bin mesajlık bir kampanya, tahmin edilenin iki katına mal oldu. Arayüz mesaj başına tek parça göstermişti, taslaklar sınırın rahatça altındaydı ve metne kimse dokunmamıştı. Süreçten çıkan mesajlar, ölçülen mesajlardan on karakter uzundu; çünkü iki ayrı özellik, ölçüm çoktan yapıldıktan sonra beşer karakter ekliyordu.

Gerçekte ne oluyor

İki özellik de tek başına makuldü ve aylar arayla yazılmıştı.

Birincisi tekrar koruması. Bazı rotalar az önce taşıdığının aynısı olan bir mesajı düşürür, bu yüzden gönderen her gövdeye kısa bir rastgele katar ekliyordu. Beş karakter, gönderim anında ekleniyor, dolu bir ekranda alıcıya görünmüyor.

İkincisi, raporlarda bir grubu diğerinden ayırmak için kullanılan dönen bir etiketti. Yine beş karakter, yine gönderim anında ekleniyor ve yine birincinin varlığından haberi olmayan bir kod tarafından.

Metni kuran katman işini doğru yaptı, ama yanlış anda:

/* paraya mal olan sıra */
const draft = renderTemplate(template, contact);   /* 155 karakter */
const quote = analyse(draft);                      /* GSM7, 155 birim, 1 parça */
showToUser(quote);

/* ... sonra, gönderenin derinlerinde ... */
const body = draft + " " + nonce() + " " + rotatingTag();  /* 165 karakter */
await provider.submit({ to, body });                       /* 2 parça */

Kullanıcının gördüğü her sayı draft değişkeninden geliyordu. Sağlayıcının faturaladığı her sayı body değişkeninden. İki katar hiçbir yerde birbiriyle karşılaştırılmıyordu, yani aradaki boşluk, biri oturup faturayı toplayana kadar orada durabilirdi.

Fark edilmesini zorlaştıran şey, hiçbir noktada bir şeyin bozuk görünmemesi. Ekrandaki metin sınırın altında. Telefondaki metin normal görünüyor, çünkü iki parçalı mesaj gösterilmeden önce birleştiriliyor. Log'larda kurulan taslak var, çünkü log yazılırken ilgi çekici olan oydu. Gerçeği içinde taşıyan tek kayıt fatura ve fatura da haftalar sonra tek bir sayı olarak geliyor.

Hesabı sonradan yaptığımızda tablo netleşti: gövdesi 150 ile 155 karakter arasında olan her mesaj iki parça olarak gitmişti, 145 karakterin altındakiler ise tek parça. Yani hata rastgele değildi, şablonu sonuna kadar kullanan metinleri seçiyordu ve bunlar da tam olarak en çok gönderilen metinlerdi. Ucuz mesajlar doğru, pahalı mesajlar yanlış ölçülüyordu.

Bu bir mesajlaşma sorunu değil, genel bir biçim. Bir değerin doğrulanıp sonra değiştirildiği her yerde çıkar:

  • Mevzuat katmanı gövdeye zorunlu bir çıkış satırı ekler.
  • Link yeniden yazıcı kısa bir adresi daha uzun bir takip adresiyle değiştirir.
  • Şablon motoru bir yer tutucuyu, yer tutucudan uzun bir değerle doldurur.
  • Biçimlendirici sona bir satır sonu koyar ya da normalizasyon bir karakteri ikiye çevirir.
  • Harf sadeleştirme adımı kodlamayı değiştirir, kodlama da parça boyunu 67'den 153'e ya da tersine taşır.

Bunların her biri uzunluk hesabından sonra uygulanan bir müdahaledir ve her biri, kullanıcıya rahatça içinde olduğu söylenmiş bir sınırı aşabilir.

Nasıl görülür

Bunu kafadan çözmeye çalışmayın. İki katarı da log'layın ve gerçek trafikte karşılaştırın. Ölçüm anında iki alan, gönderim anında iki alan yeter:

log.info("quote",  { id, len: draft.length, seg: analyse(draft).segments });
log.info("submit", { id, len: body.length,  seg: analyse(body).segments });

Sonra bir günlüğünü eşleştirin:

grep -h '"quote"\|"submit"' /var/log/app/send.log \
  | node scripts/pair-by-id.js \
  | awk '$2 != $4 { print }' \
  | head -20
# id=01J1... quote_len=155 submit_len=165  quote_seg=1 submit_seg=2
# id=01J1... quote_len=148 submit_len=158  quote_seg=1 submit_seg=1

Birinci satır hatanın kendisi. İkinci satır aynı müdahalenin daha kısa bir gövdedeki hâli: orada hiçbir maliyeti yok, bu yüzden kimse şikâyet etmiyor. Hatanın hayatta kalma sebebi budur. Müdahale uzunluk aralığının çoğunda zararsız, sınıra yakın dar bir bantta pahalıdır ve o banda düşen mesajlar tam olarak parçayı sonuna kadar kullanmak için yazılmış olanlardır.

Gönderdiğiniz veriyi zaten saklıyorsanız log'a gerek kalmaz, doğrudan veritabanına sorabilirsiniz. Saklamıyorsanız, bu yazının geri kalanı saklamaya başlamanız için bir gerekçe.

Çözüm

Kural tek cümle: göndermek üzere olduğunuz katarı ölçün, değiştirmek üzere olduğunuz katarı değil.

Bütün müdahaleleri tek bir yerde, sıralı ve isimli tutun:

type Step = { name: string; apply(text: string, ctx: Ctx): string };

const PIPELINE: Step[] = [
  renderTemplate,
  transliterate,
  appendOptOut,
  appendNonce,
  appendRotatingTag,
];

export function buildPayload(draft: string, ctx: Ctx) {
  const body = PIPELINE.reduce((t, step) => step.apply(t, ctx), draft);
  return { body, ...analyse(body) };   /* her şeyden sonra ölçülüyor */
}

Artık veriyi üreten tek şey buildPayload, sayıyı üreten tek şey de o. Arayüz onu temsili bir bağlamla çağırır, böylece ekrandaki sayaç gerçek sonucu gösterir. Gönderen onu çağırır ve body değerine dokunmadan gönderir. Ücret tahmini onu çağırır ve çarpar. Aşağıdaki hiçbir katmanın ekleme yapma izni yoktur.

İki ayrıntı bunu kalıcı kılıyor.

Birincisi, arayüzün ek yükü dürüstçe göstermesi. Pipeline on karakter ekleyecekse sayaç 155 değil 145 kalan yazmalı. İnsanlar önlerindeki sayıya göre yazar ve on karakter yalan söyleyen bir sayaç, sınırın tam yanlış tarafında duran mesajlar üretir.

İkincisi, iki müdahaleye birden gerçekten ihtiyacınız olup olmadığına sert bakmak. Bu örnekte tekrar koruması ile dönen etiket neredeyse aynı gerekçeyle vardı ve biri yeterliydi. Bir özelliği hesaba katmak yerine kaldırmak daha iyi bir çözümdür ve mesajı pahalı değil kısa yapan tek çözümdür. Kalanı ise sayın: pipeline beş karakter ekliyorsa, parça hesabı bu beş karakteri her yerde içermeli; karakter seti ve parça matematiği de dahil, çünkü 153'lük mü yoksa 67'lik birimlerle mi çalıştığınıza o karar veriyor.

Bunun çözmediği şey, süreç dışında uygulanan bir müdahaledir. Rotanın kendisi gövdenin başına bir şey ekliyorsa hiçbir iç disiplin bunu göstermez. Onun için gerçek bir teslimat, gerçek bir telefonda okunmalı ve gönderdiğinizle karşılaştırılmalı.

Çalıştığını nasıl doğrularsınız

Bunu kalıcı kılan test pipeline'ı test etmez. Gösterdiğiniz sayı ile gönderdiğiniz baytların aynı şey olduğunu, transport sınırında yakalayarak test eder:

it("gösterdiği veriyi aynen gönderir", async () => {
  const sent: string[] = [];
  const provider = { submit: async (m: any) => { sent.push(m.body); return { id: "x" }; } };

  for (const len of [1, 69, 70, 71, 142, 143, 144, 152, 153, 154, 160, 161]) {
    sent.length = 0;
    const draft = "x".repeat(len);
    const quote = buildPayload(draft, ctx);

    await send({ draft, to: "0000", provider, ctx });

    expect(sent).toHaveLength(1);
    expect(sent[0]).toBe(quote.body);                     /* bayt bayt */
    expect(analyse(sent[0]).segments).toBe(quote.segments);
  }
});

Listedeki uzunluklar özellikle seçildi: her biri şu ya da bu kodlamada bir sınırın üstünde veya hemen yanında duruyor. Veriye bir karakter ekleyen bir regresyon bunlardan en az ikisinde teste takılır. İstenen davranış budur, çünkü bir karakter ekleyen regresyon bunun dışında tamamen görünmezdir.

Dağıtımdan sonraki gözlemlenebilir sonuç test takımında değil veride:

SELECT sum(predicted_segments) AS predicted,
       sum(billed_segments)    AS billed
FROM messages
WHERE created_at > now() - interval '1 day';

Bu iki sayı eşit olmalı ya da raporu hiç dönmemiş birkaç satır kadar ayrışmalı. Sistematik her fark, henüz bulmadığınız başka bir müdahaledir.

Nelere dikkat etmeli

  • Yalnızca bazen uygulanan bir müdahale, her zaman uygulanandan kötüdür. Ayın ilk mesajına eklenen bir çıkış satırı, salı günü koşan her testten geçer.
  • Veritabanında gönderilen veriyi değil taslağı saklamak, bu hatayı sonradan kanıtlanamaz yapar. Gönderdiğinizi saklayın ya da en azından uzunluğunu ve bir hash'ini.
  • Arayüz sayacı ile gönderen aynı fonksiyonu kullanmalı. Tarayıcı için yazılmış ikinci bir uygulama bir sürüm içinde yanlışa düşer ve daha az parça vaat etme yönünde yanlışa düşer.
  • Kodlama sınırı değiştirir, yani eklenen bir karakter kodlamayı da değiştirebilir. İçinde tek bir aksanlı harf olan bir sonek, 150 karakterlik mesajı tek parçadan ikiye değil üçe taşır.
  • Pipeline'a yeni bir adım eklerken testi değil kuralı hatırlayın: adımın yerini listede tanımlayın ve ölçümün her zaman listenin sonunda kaldığından emin olun. Sıraya sonradan giren bir adım, kendisinden sonra ölçüm yapılmadığı sürece aynı hatayı sıfırdan üretir.

Buradan alınacak alışkanlık, bir söz ile bir eylem arasına giren her adıma şüpheyle bakmaktır. Bir doğrulayıcı, bir sayaç, bir tahmin ve bir onay kutusu, hepsi bir değer hakkında verilmiş sözlerdir ve her biri yalnızca o değere bir sonraki fonksiyon dokunana kadar doğrudur. Sözü mümkün olan en son ana taşıyın, dönüşümleri tek bir görünür listede tutun ve testi süreç sınırını gerçekten geçen şeye karşı yazın. Aynı disiplin, asenkron bir bildirimin ait olduğu kayıtla eşleşmesini sağlayan şeydir: süreçten çıkan ya da içeri giren kayda güvenin, o konuda ne söyleyeceğinize karar verirken elinizde tuttuğunuza değil.

Sorular ve cevaplar

Sayaç tek parça derken fatura neden iki parça diyor?
Çünkü sayaç gönderilen metinden başka bir metni ölçtü. Sayaç çalıştıktan sonra eklenen, genişletilen veya yeniden yazılan her şey ona görünmez; parça sınırının hemen altında duran bir gövde, ekrandaki hiçbir sayı değişmeden sınırı aşar. Güvenilir tek ölçüm noktası, transport'a verilmeden hemen önceki nihai veridir.
Mevcut bir gönderim yolundaki gizli müdahaleleri nasıl bulurum?
Gövdenin uzunluğunu ve kısa bir hash'ini hem sayacın çalıştığı noktada hem de gönderim noktasında log'layın, sonra bir günlük trafikte ikisini karşılaştırın. Uzunlukların ayrıştığı her satır, kimsenin hesaba katmadığı bir müdahaleyi işaret eder. Bu yöntem doğru ama beyan edilmemiş müdahaleleri de yakalar; zorunlu çıkış satırı gibi.
Her mesaja rastgele sonek eklemek maliyetine değer mi?
Bazen. Mesajı benzersiz kılmak, rotadaki agresif tekrar filtrelerini aşmaya yarayabilir ve bu birkaç karaktere değen gerçek bir sorundur. Asla değmeyen şey, aynı işi yapan iki özelliğin aynı anda çalışmasıdır; beş karakterlik yükün sessizce ona çıkması tam olarak böyle olur.
Parça hesabı kodda nerede durmalı?
Nihai veriyi alıp kodlamayı ve parça sayısını dönen tek bir fonksiyonda; arayüz, API, worker ve ücret tahmini bu fonksiyonu kullanmalı. Arayüzün taslak üzerinde çalışan kendi kopyası varsa, haftalar içinde gönderenden ayrışır ve her zaman iyimser yönde ayrışır.