Bu sayfa İngilizceden çevrilmiştir. İngilizce sayfayı okuyun
Bir Zeyn uygulamasını üretimde çalıştırma: saat dilimi verisi, imzalı lisanslar, birden fazla sunucu için paylaşılan durum, loglar ve izler, PDF belgeleri ve e-posta.
Saat dilimleri
TimeZone::named her IANA bölgesini (Europe/Istanbul, America/New_York, Australia/Sydney, büyük küçük harf fark etmez) ve sabit ofsetleri (UTC+03:00, +05:30) kabul eder.
- Bölge verisi,
jiffaracılığıyla ikili dosyanın içine gömülü IANA saat dilimi veritabanıdır (yaklaşık 200 KB). Cevaplar Windows dahil her platformda aynıdır ve sistemin zoneinfo verisi okunmaz. - Geçmiş de modellenir: Türkiye'nin 2016'ya kadar yaz saati uygulaması ve o tarihten beri kalıcı
+03ofseti, Amerika Birleşik Devletleri'nin 2007 öncesi kuralları ve kayıtlı diğer tüm değişiklikler. - Var olmayan bir duvar saati (saatlerin ileri alındığı an)
LocalTimeError::Skippedile reddedilir. İki kez yaşanan bir saat (saatlerin geri alındığı an) önceki anı ifade eder. - Bir bölge, adı ilk kullanıldığında pakete gömülü veriden okunur ve sürecin ömrü boyunca tutulur; her biri birkaç kilobayttır. Bilinmeyen adlar hiçbir zaman tutulmaz, bu yüzden istemciler önbelleği büyütemez.
TimeZone::names(), kabuğun (shell) bölge seçicisi için yaygın bölgelerden oluşan kısa bir listedir. Listede olmayan bir kullanıcı bölgesi, o kullanıcı için seçiciye eklenir.- Yeni bölge kuralları daha yeni bir
jiffsürümüyle gelir (cargo update -p jiff-tzdb). Bir ülke kurallarını değiştirdiğinde yeniden derleyin ve dağıtın.
İmzalı lisanslar
Bir lisans dosyası, yalnızca satıcının elinde bulunan bir ed25519 anahtarıyla imzalanır. Uygulama genel anahtarı gömülü taşır; böylece müşteri lisansı okuyabilir ama değiştiremez ya da yenisini üretemez. signed-licenses Cargo feature'ı ile açılır.
zeyn license keygen # writes zeyn-license.key, prints the public key
zeyn license sign acme.json --out license.jsonacme.json lisans alanlarını içerir:
{ "holder": "Acme", "valid_until": "2027-12-31", "modules": ["pos", "stock"], "max_users": 50 }const LICENSE_KEY: &str = "f414a89e9dae57598d60141df96dd1c9fb655fdcf49f5eaf24e093fda7d332e3";
App::new("Shop")
.license(SignedLicenseFile::new("license.json", LICENSE_KEY), Duration::from_secs(3600))
.feature(Feature::new("pos").guard(Licensed::module("pos")).screen(pos))- Dosya JSON'dur: lisans
payloadiçinde metin olarak, imzasignatureiçinde durur. İmzalanan şeyzeyn-license-v1\nifadesi ve ardından gelen payload metnidir; yani imzalanan baytlar saklanan baytlarla birebir aynıdır. - Özel anahtar imzalama makinesinde kalır.
keygenvar olan bir anahtar dosyasının üzerine asla yazmaz ve*.keyyeni projelerin.gitignoredosyasında yer alır.signanahtarı--keyseçeneğinden,ZEYN_LICENSE_KEYdeğişkeninden ya dazeyn-license.keydosyasından okur;holderalanı olmayan veyavalid_untildeğeriYYYY-MM-DDbiçiminde olmayan bir lisansı reddeder. - Değiştirilmiş bir dosya, başka bir anahtarla imzalanmış bir dosya veya lisans olmayan bir dosya reddedilir:
License::from_signedLicenseErrordöndürür (BadSignature,InvalidPublicKey,Malformed), sunucu nedenini loglar ve herLicensedguard "Your license does not cover this." mesajıyla erişimi reddeder. Okunamayan bir dosya, başarısız olan her lisans kaynağında olduğu gibi son lisansı korur. - Süre sonu, guard tarafından her istekte kullanıcının bugününe göre denetlenir; bu yüzden süresi dolmuş bir lisans, yeniden başlatma gerekmeden uygulamayı "Your license has expired." mesajıyla kilitler. Yenilenmiş bir dosya sonraki yenilemede okunur.
- ed25519, değiştirilmiş bir dosyayı başka bir anahtarla imzalanmış bir dosyadan ayırt edemez; ikisi de eşleşmeyen bir imzadır ve mesaj bunu söyler.
max_userstaşınır ve imzalanır ama framework tarafından uygulanmaz; neyin kullanıcı sayılacağına uygulama karar verir.
Birden fazla sunucu: Redis'te paylaşılan durum
redis Cargo feature'ı ile RedisStore, bir yük dengeleyicinin arkasındaki her örnekte aynı olması gereken durumu tutar:
let redis = RedisStore::new(config.require("redis.url")?, "billing").map_err(Error::Internal)?;
App::new("Billing")
.rate_limit(RateLimit::per_ip(300, Duration::from_secs(60)))
.rate_store(redis.clone())
.datagrid_views(redis)Sunucunun kullanıcı başına neyi nerede tuttuğu:
| Durum | Nerede | Paylaşılan |
|---|---|---|
| Hız sınırı sayaçları | RateStore | RedisStore |
| Kaydedilmiş grid görünümleri ve dock yerleşimleri | DataGridViewStore | RedisStore |
| Boşta kalma zaman aşımı | tarayıcı (uygulamanın tüm sekmeleri) | sunucuda hiçbir şey |
| Oturum açma ve yeniden kimlik doğrulama | tek oturum açma (single sign on) proxy'si | sunucuda hiçbir şey |
| Hassas bir ekran için gerekçe | bir HttpOnly cookie | sunucuda hiçbir şey |
| Form taslakları | sessionStorage | sunucuda hiçbir şey |
| Lisans önbelleği | süreç başına, kaynaktan yeniden yüklenir | her sunucu kendi önbelleğini yükler |
| Arka plan işleri | süreç başına | paylaşılmaz; bkz. enterprise.md içindeki işler (jobs) bölümü |
- Hız sınırları tek bir Lua betiğinde token bucket olarak çalışır; bu yüzden farklı sunuculardaki eşzamanlı istekler sınırı asla aşmaz. Zaman uygulama sunucularından değil, Redis'ten gelir. Sayaçlar son kullanımlarından bir pencere sonra sona erer.
- Görünümler
zeyn:<prefix>:view:<user length>:<user>:<grid>anahtarı altında JSON olarak durur ve asla sona ermez. - Bağlantı ilk kullanımda kurulur ve kendiliğinden geri gelir. Çağrılar 500 ms sonra zaman aşımına uğrar. Redis kapalıyken hız sınırları istekleri geçirir, görünümler ne yüklenir ne kaydedilir (grid kendi yerleşimine, dock
localStorage'a döner), kesinti ve düzelme ayrı ayrı birer kez loglanır ve her hatadan sonra Redis beş saniye rahat bırakılır; böylece istekler tek tek zaman aşımını beklemez. RateStore::takebir future döndürür; böylece depolar bir ağ ile konuşabilir.MemoryRateStore::takedoğrudan kullanım için eşzamanlı (synchronous) kalır.- TLS (
rediss://) yerleşik değildir. ZEYN_TEST_REDISayarlıysa testler gerçek bir sunucuya karşı çalışır:
docker run -d --rm --name zeyn-redis -p 6379:6379 redis:7-alpine
ZEYN_TEST_REDIS=redis://127.0.0.1:6379 cargo test -p zeyn --features redis --test redis
docker stop zeyn-redisLoglar ve izler
Framework tracing üzerinden raporlar. Kendiliğinden hiçbir şey yazdırmaz.
App::serveveApp::serve_until, uygulama önceden bir tane kurmadıysa bir subscriber kurar: olaylar standart hataya metin satırları olarak gider velog.levelile filtrelenir (varsayılaninfo;warn,zeyn=debuggibi herhangi birtracinghedef filtresi). JSON satırları ya da başka bir çıkış isteyen bir uygulama, servis vermeden önce kendi subscriber'ını kurar ve Zeyn ona dokunmaz.infodüzeyinde çıktı her zamanki gibidir: dinlenen adres, başlangıç uyarıları (eksik çeviriler,DevAuth,dev_latency), başarısız lisans kaynakları ve iç hatalar.debugdüzeyinde her çağrı; tür, ekran, action, kullanıcı kimliği, tenant kimliği, sonuç, hata kodu ve milisaniye içeren bircall finishedsatırı ekler.- İz arka uçları için span'ler:
| Span | Düzey | Alanlar |
|---|---|---|
request | info | method, path (sorgu dizesi olmadan), status, ms ve bilindiğinde: kind, screen, action, user, tenant, ok, error |
run | info | uygulama kodu: bir action, bir veri kaynağı (grid fetch, satır sorgusu, görünüm deposu), bir screen builder |
interceptor | debug | name (tür adı veya Interceptor::name), stage (before ya da after) |
otelCargo feature'ı ile,Configiçindeotel.endpointayarlıysa (http://collector:4318; span'ler/v1/tracesadresine gider) span'ler OTLP/HTTP üzerinden dışa aktarılır.otel.service_nameservisi adlandırır; varsayılan uygulamanın adıdır. Span'ler bir arka plan thread'inde gruplanır veserve_untildurduğunda boşaltılır.- Dışa aktarma yoksa standart hata yalnızca olayları alır; bu yüzden hiçbir span oluşturulmaz ve span'ler başına önbelleğe alınmış bir denetim kadar maliyet getirir.
App::routerile başka bir sunucuya bağlanan uygulamalar ve testler de aynı bedeli öder. - Hiçbir zaman kaydedilmeyenler: alan değerleri (maskeli olsun olmasın), form ve API girdisi, sorgu dizeleri, cookie'ler, header'lar ve gizli bilgiler. Bir test, grid'leri, form kayıtlarını, API çağrılarını ve açığa çıkarmaları (reveal) trace düzeyinde bir subscriber altında çalıştırır ve hiçbir maskeli değerin görünmediğini denetler. İki şey uygulamada kalır: path'ler kaydedilir, bu yüzden route'lara kişisel veri koymayın; ve
Error::Internalmesajları yazıldığı gibi loglanır.
PDF belgeleri
pdf Cargo feature'ı, Typst şablonlarından PDF belgeleri üretir. Typst büyük olduğu için varsayılan olarak kapalıdır (aşağıdaki rakamlara bakın).
const INVOICE: PdfTemplate = PdfTemplate::new("invoice", include_str!("../templates/invoice.typ"));
#[action(permission = "sales.order.read")]
pub async fn invoice_data(p: OrderParams, store: Orders, ctx: Ctx) -> Result<InvoiceData> { ... }
Screen::window("Order").body(PdfLink::new("Print invoice", INVOICE, invoice_data, params))
App::new("Shop").service(Pdf::from_config(&config)?)PdfLink, bir screen'e indirme düğmesi koyar.GET /_zeyn/pdfscreen'i erişim denetimleriyle açar, action'ı kullanıcı için çalıştırır (action'ın izni ve interceptor'ları geçerlidir, çağrı türüDocument'tır) ve şablonu action'ın döndürdüğü veriyle işler. Action'ı çalıştırma izni olmayan kullanıcılar düğmeyi görmez.exportable()olan grid'ler, araç çubuğununExport::Pdfseçeneğiyle bir liste raporu da yazdırır (GET /_zeyn/grid/export?format=pdf): kullanıcının sütunları, etiketleri, filtreleri ve sıralaması, sağa hizalı sayılar, tekrarlanan başlık satırı ve sayfa numaraları. Bir rapor en fazla 2.000 satır içerir (MAX_PDF_ROWS, ya da daha küçükse grid'in dışa aktarma sınırı) ve satır dışarıda kaldığında bunu belirtir.- Veri hiçbir zaman Typst koduna dönüşmez. Action'ın çıktısı JSON'a serileştirilir ve şablon onu
dataolarak okur (işleyici, şablondan önce#let data = json("/data.json")ifadesini bağlar).#panic(),]veya$x$gibi değerler metin olarak yazdırılır; bir test bunu denetler. Şablonlar başka hiçbir şeyi okuyamaz: dosya yok, paket yok. - İşleme (rendering) bir engelleyen thread'de çalışır. Aynı anda en fazla
pdf.max_concurrentbelge işlenir (1 ile 64 arası; varsayılan iki, tek CPU'da bir); geri kalanlar bir yuva için bekler. Typst'in memoization önbelleği her belgeden sonra boşaltılır, böylece bellek yeniden düşer.
| Anahtar | Anlamı |
|---|---|
pdf.fonts | Başlangıçta bir kez okunan, ek .ttf, .otf, .ttc veya .otc yazı tiplerinin dizini |
pdf.max_concurrent | Aynı anda işlenen belge sayısı |
Yazı tipleri: Libertinus Serif (normal, kalın, italik, kalın italik; SIL Open Font License) gömülüdür ve Türkçeyi kapsar; DejaVu Sans Mono, ₺ gibi Libertinus'ta bulunmayan işaretler için yedek olarak gömülüdür. Lisansları crates/zeyn/assets/fonts içindedir. Matematik yazı tipi yoktur; şablonlar denklem kullanıyorsa pdf.fonts ile bir tane ekleyin.
Windows'ta ölçüldü (release derlemesi, x86_64):
| Sonuç | |
|---|---|
| İkili dosya boyutu | pdf olmadan 5,5 MB, ile yaklaşık 46 MB (Typst yaklaşık 39 MB, yazı tipleri 1,6 MB ekler) |
| Tek sayfalık bir belge | 4 ile 7 ms, işleme sırasında 1 MB'ın altında heap, 10 KB dosya |
| 1.000 satırlık bir liste raporu (25 sayfa) | 0,54 ile 0,74 sn, işleme sırasında 51 MB tepe heap, sonrasında 0 kalır, 0,87 MB dosya |
| Başlangıçtan sonraki ilk belge | Typst'in tembel biçimde oluşturulan kendi tabloları için yaklaşık 42 MB heap kalır |
| Yazı tipleri | Yazı tipi dizini için 0,25 MB heap |
Debug derlemesi yaklaşık on kat daha yavaştır (showcase'de 2.000 satırlık bir rapor 71 sn sürdü).
E-posta
email Cargo feature'ı, bildirimleri lettre ile gönderir.
let mailer = Mailer::from_config(&config)?;
App::new("People").mailer(mailer)
#[action(permission = "people.leave.request", audit)]
pub async fn move_leave(r: TransitionRequest<LeaveRequestId>, store: Leaves, mailer: Mailer, ctx: Ctx) -> Result<LeaveState> {
let state = leave_flow().apply(&ctx, current, &r.transition, r.comment.as_deref())?;
mailer.send(&ctx, Mail::transition(&ctx, &manager_email, &employee)?)?;
Ok(state)
}Mailer::sendiletiyi işler ve bellekteki bir kuyruğa koyar (en fazla 10.000 ileti), sonra döner.zeyn.mailarka plan işi kuyruğu hemen gönderir, başarısız iletileri geri çekilme (backoff) ile yeniden dener (Mailer::retrybaşka bir şey söylemedikçe on saniyeden başlayarak beş kez) ve kuyruğa her dakika bakar. Bir ileti, sekiz başarısız denemeden sonra logda bir hata ile bırakılır. Her iş gibi, süreç durduğunda gönderilmemiş bir kuyruk kaybolur; kalıcı teslimat için veri katmanının veritabanı destekli kuyruğu gerekir.- İletiler i18n kataloglarından gelir:
<template>.subject,<template>.textve isteğe bağlı olarak<template>.html,{name}argümanlarıyla.Mail::localeayarlamadıkça alıcının dili göndericinin dilidir. Her iletinin düz metin ve HTML bölümü vardır; HTML bölümünde her argüman kaçırılır (escape) ve.htmlgirdisi yoksa HTML, kaçırılmış metindir. Konulardan satır sonları kaldırılır ve adresler ayrıştırılır; böylece veri header veya alıcı ekleyemez. Mail::transition, çağrının az önceWorkflow::applyile yaptığı geçişi (kayıt, önceki durum, yeni durum, kim ve yorum) yerleşikzeyn.mail.transitionşablonuyla yazar. Showcase, bir izin talebi ilerlediğinde yöneticiye haber verir.
| Anahtar | Anlamı |
|---|---|
mail.from | Northwind <no-reply@example.com> gibi gönderici (zorunlu) |
mail.transport | smtp, file (ileti başına bir .eml dosyası, geliştirme ve testler için) veya log (alıcı sayısını ve boyutu içeren, asla adres ya da içerik içermeyen bir log satırı; varsayılan) |
mail.smtp.host, mail.smtp.port | SMTP sunucusu |
mail.smtp.tls | starttls (varsayılan), tls (örtük TLS, genellikle 465 portu) veya none (yalnızca yerel röleler) |
mail.smtp.user, mail.smtp.password | Kimlik bilgileri; parolayı ortam gibi bir gizli kaynaktan okuyun |
mail.file.dir | file taşıyıcısının dizini |
TLS, Mozilla kök sertifikalarıyla (webpki-roots) rustls kullanır.