यह पृष्ठ अंग्रेज़ी से अनुवादित है। अंग्रेज़ी पृष्ठ पढ़ें
प्रोडक्शन में Zeyn ऐप चलाना: समय क्षेत्र डेटा, हस्ताक्षरित लाइसेंस, कई सर्वर के लिए साझा स्थिति, लॉग और ट्रेस, PDF दस्तावेज़ और ईमेल।
समय क्षेत्र
TimeZone::named हर IANA क्षेत्र (Europe/Istanbul, America/New_York, Australia/Sydney, किसी भी अक्षर रूप में) और निश्चित ऑफ़सेट (UTC+03:00, +05:30) स्वीकार करता है।
- क्षेत्र डेटा IANA समय क्षेत्र डेटाबेस है जो
jiffके ज़रिए बाइनरी में बना है (लगभग 200 KB)। उत्तर हर प्लेटफ़ॉर्म पर, Windows समेत, एक जैसे हैं, और कोई सिस्टम zoneinfo नहीं पढ़ा जाता। - इतिहास मॉडल किया गया है: तुर्की का 2016 तक डेलाइट सेविंग और तब से स्थायी
+03, 2007 से पहले के संयुक्त राज्य के नियम, और हर अन्य दर्ज बदलाव। - दीवार घड़ी का समय जो मौजूद नहीं है (घड़ियाँ आगे बढ़ीं)
LocalTimeError::Skippedसे अस्वीकार होता है। जो दो बार होता है (घड़ियाँ पीछे गईं) उसका अर्थ पहला क्षण है। - क्षेत्र को नाम पहली बार इस्तेमाल होने पर बंडल से पढ़ा जाता है और प्रक्रिया के जीवन भर रखा जाता है, प्रत्येक कुछ किलोबाइट। अज्ञात नाम कभी नहीं रखे जाते, इसलिए क्लाइंट कैश नहीं बढ़ा सकते।
TimeZone::names()शेल के क्षेत्र चयनकर्ता के लिए आम क्षेत्रों की छोटी सूची है। उपयोगकर्ता का क्षेत्र जो सूची में नहीं है, उस उपयोगकर्ता के लिए चयनकर्ता में जोड़ दिया जाता है।- नए क्षेत्र नियम नए
jiffरिलीज़ के साथ आते हैं (cargo update -p jiff-tzdb)। जब कोई देश अपने नियम बदले तो रीबिल्ड और डिप्लॉय करें।
हस्ताक्षरित लाइसेंस
लाइसेंस फ़ाइल ed25519 कुंजी से हस्ताक्षरित होती है जो केवल विक्रेता के पास होती है। ऐप सार्वजनिक कुंजी एम्बेड करता है, इसलिए ग्राहक लाइसेंस पढ़ सकता है लेकिन उसे बदल या नया नहीं बना सकता। इसे signed-licenses Cargo feature से चालू करें।
zeyn license keygen # writes zeyn-license.key, prints the public key
zeyn license sign acme.json --out license.jsonacme.json लाइसेंस फ़ील्ड रखता है:
{ "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))- फ़ाइल JSON है जिसमें
payloadमें टेक्स्ट के रूप में लाइसेंस औरsignatureमें हस्ताक्षर है। जो हस्ताक्षरित है वहzeyn-license-v1\nके बाद payload टेक्स्ट है, इसलिए हस्ताक्षरित बाइट ठीक सहेजे हुए बाइट हैं। - निजी कुंजी हस्ताक्षर करने वाली मशीन पर रहती है।
keygenकभी कुंजी फ़ाइल ओवरराइट नहीं करता, और*.keyनए प्रोजेक्ट की.gitignoreमें है।signकुंजी--keyसे,ZEYN_LICENSE_KEYवेरिएबल से याzeyn-license.keyसे पढ़ता है, औरholderके बिना याYYYY-MM-DDन होने वालेvalid_untilके साथ लाइसेंस अस्वीकार करता है। - बदली गई फ़ाइल, दूसरी कुंजी से हस्ताक्षरित फ़ाइल या ऐसी फ़ाइल जो लाइसेंस नहीं है अस्वीकार होती है:
License::from_signedLicenseError(BadSignature,InvalidPublicKey,Malformed) लौटाता है, सर्वर कारण लॉग करता है, और हरLicensedगार्ड "Your license does not cover this." के साथ अस्वीकार करता है। जो फ़ाइल पढ़ी न जा सके वह किसी भी विफल लाइसेंस स्रोत की तरह आखिरी लाइसेंस रखती है। - समाप्ति गार्ड हर अनुरोध पर उपयोगकर्ता के आज के विरुद्ध जाँचता है, इसलिए समाप्त लाइसेंस बिना रीस्टार्ट के ऐप को "Your license has expired." के साथ लॉक करता है। नवीनीकृत फ़ाइल अगले रिफ़्रेश पर पढ़ी जाती है।
- ed25519 बदली फ़ाइल और दूसरी कुंजी से हस्ताक्षरित फ़ाइल में फ़र्क नहीं कर सकता; दोनों बेमेल हस्ताक्षर हैं, और संदेश यही बताता है।
max_usersसाथ रखा और हस्ताक्षरित है लेकिन फ्रेमवर्क द्वारा लागू नहीं किया जाता; ऐप तय करता है कि वह किसे उपयोगकर्ता गिनता है।
कई सर्वर: Redis में साझा स्थिति
redis Cargo feature के साथ RedisStore वह स्थिति रखता है जो लोड बैलेंसर के पीछे हर इंस्टेंस पर एक जैसी होनी चाहिए:
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)सर्वर प्रति उपयोगकर्ता क्या रखता है, और कहाँ:
| स्थिति | कहाँ | साझा |
|---|---|---|
| दर सीमा काउंटर | RateStore | RedisStore |
| सहेजे गए ग्रिड व्यू और डॉक लेआउट | DataGridViewStore | RedisStore |
| निष्क्रियता टाइमआउट | ब्राउज़र (ऐप के सभी टैब) | सर्वर पर कुछ नहीं |
| साइन इन और पुनः प्रमाणीकरण | सिंगल साइन ऑन प्रॉक्सी | सर्वर पर कुछ नहीं |
| संवेदनशील Screen का कारण | HttpOnly कुकी | सर्वर पर कुछ नहीं |
| फ़ॉर्म ड्राफ़्ट | sessionStorage | सर्वर पर कुछ नहीं |
| लाइसेंस कैश | प्रति प्रक्रिया, स्रोत से फिर लोड | हर सर्वर अपना लोड करता है |
| बैकग्राउंड जॉब | प्रति प्रक्रिया | साझा नहीं; देखें enterprise.md का जॉब खंड |
- दर सीमाएँ एक Lua स्क्रिप्ट में टोकन बकेट हैं, इसलिए अलग सर्वर पर समवर्ती अनुरोध कभी सीमा से अधिक नहीं होते। समय Redis से आता है, ऐप सर्वर से नहीं। काउंटर अपने आखिरी उपयोग के एक विंडो बाद समाप्त होते हैं।
- व्यू
zeyn:<prefix>:view:<user length>:<user>:<grid>के नीचे JSON हैं और कभी समाप्त नहीं होते। - कनेक्शन पहले इस्तेमाल पर बनता है और अपने आप लौट आता है। कॉल 500 ms के बाद टाइमआउट होती हैं। जब Redis बंद हो, दर सीमाएँ अनुरोध गुज़रने देती हैं, व्यू न लोड होते हैं न सहेजे जाते हैं (ग्रिड अपने लेआउट पर लौटता है, डॉक
localStorageपर), आउटेज और रिकवरी हर एक एक बार लॉग होती है, और हर विफलता के बाद पाँच सेकंड Redis को अकेला छोड़ा जाता है ताकि अनुरोध हर एक टाइमआउट का इंतज़ार न करें। RateStore::takeएक future लौटाता है ताकि स्टोर नेटवर्क से बात कर सकें।MemoryRateStore::takeसीधे इस्तेमाल के लिए synchronous रहता है।- TLS (
rediss://) बिल्ट इन नहीं है। - जब
ZEYN_TEST_REDISसेट हो तो टेस्ट असली सर्वर के विरुद्ध चलते हैं:
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-redisलॉग और ट्रेस
फ्रेमवर्क tracing के ज़रिए रिपोर्ट करता है। यह खुद कभी कुछ नहीं छापता।
App::serveऔरApp::serve_untilएक subscriber इंस्टॉल करते हैं जब तक ऐप ने पहले कोई इंस्टॉल न किया हो: घटनाएँ टेक्स्ट पंक्तियों के रूप में standard error पर जाती हैं,log.level(डिफ़ॉल्टinfo; कोई भीtracingटारगेट फ़िल्टर जैसेwarn,zeyn=debug) से फ़िल्टर होकर। जिस ऐप को JSON पंक्तियाँ या कोई और सिंक चाहिए वह परोसने से पहले अपना subscriber इंस्टॉल करता है, और Zeyn उसे अकेला छोड़ देता है।infoपर आउटपुट वही है जो हमेशा था: सुनने का पता, स्टार्टअप चेतावनियाँ (छूटे अनुवाद,DevAuth,dev_latency), विफल लाइसेंस स्रोत और आंतरिक त्रुटियाँ।debugपर हर कॉल kind, Screen, action, उपयोगकर्ता आईडी, टेनेंट आईडी, परिणाम, त्रुटि कोड और मिलीसेकंड के साथcall finishedपंक्ति जोड़ती है।- स्पैन, ट्रेसिंग बैकएंड के लिए:
| स्पैन | स्तर | फ़ील्ड |
|---|---|---|
request | info | मेथड, पथ (क्वेरी स्ट्रिंग के बिना), स्टेटस, ms, और पता चलने पर: kind, Screen, action, उपयोगकर्ता, टेनेंट, ok, त्रुटि |
run | info | एप्लिकेशन कोड: एक action, एक डेटा स्रोत (ग्रिड fetch, पंक्ति खोज, व्यू स्टोर), एक Screen बिल्डर |
interceptor | debug | नाम (टाइप का नाम, या Interceptor::name), चरण (before या after) |
otelCargo feature के साथ, जबConfigमेंotel.endpointसेट हो (http://collector:4318; स्पैन/v1/tracesपर जाते हैं) तो स्पैन OTLP/HTTP पर निर्यात होते हैं।otel.service_nameसेवा को नाम देता है, डिफ़ॉल्ट रूप से ऐप का नाम। स्पैन बैकग्राउंड थ्रेड पर बैच होते हैं औरserve_untilरुकने पर फ़्लश होते हैं।- निर्यात के बिना standard error केवल घटनाएँ पाता है, इसलिए कोई स्पैन नहीं बनता और स्पैन एक कैश की गई जाँच की लागत रखते हैं। दूसरे सर्वर में
App::routerसे माउंट किए ऐप, और टेस्ट, वही चुकाते हैं। - जो कभी दर्ज नहीं होता: फ़ील्ड मान (मास्क किए हों या नहीं), फ़ॉर्म और API इनपुट, क्वेरी स्ट्रिंग, कुकी, हेडर और सीक्रेट। एक टेस्ट trace स्तर के subscriber के तहत ग्रिड, फ़ॉर्म सेव, API कॉल और reveal चलाता है और जाँचता है कि कोई मास्क किया मान नहीं दिखता। दो चीज़ें ऐप के पास रहती हैं: पथ दर्ज होते हैं, इसलिए रूट में व्यक्तिगत डेटा न डालें, और
Error::Internalसंदेश जैसे लिखे गए वैसे लॉग होते हैं।
PDF दस्तावेज़
pdf Cargo feature Typst टेम्पलेट से PDF दस्तावेज़ रेंडर करता है। यह डिफ़ॉल्ट रूप से बंद है क्योंकि Typst बड़ा है (नीचे संख्याएँ देखें)।
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)?)PdfLinkScreen पर डाउनलोड बटन रखता है।GET /_zeyn/pdfScreen को उसकी एक्सेस जाँचों के साथ खोलता है, उपयोगकर्ता के लिए action चलाता है (उसकी अनुमति और इंटरसेप्टर लागू होते हैं, कॉल का kindDocumentहै) और action जो लौटाती है उससे टेम्पलेट रेंडर करता है। जो उपयोगकर्ता action नहीं चला सकते उन्हें बटन नहीं दिखता।exportable()वाले ग्रिड टूलबार केExport::Pdf(GET /_zeyn/grid/export?format=pdf) से सूची रिपोर्ट भी छापते हैं: उपयोगकर्ता के कॉलम, लेबल, फ़िल्टर और क्रम, संख्याएँ दाईं ओर संरेखित, दोहराई गई शीर्षक पंक्ति और पृष्ठ संख्याएँ। रिपोर्ट में अधिकतम 2,000 पंक्तियाँ होती हैं (MAX_PDF_ROWS, या जब छोटी हो तो ग्रिड की निर्यात सीमा) और पंक्तियाँ छूटने पर यह बताती है।- डेटा कभी Typst कोड नहीं बनता। action का आउटपुट JSON में सीरियलाइज़ होता है और टेम्पलेट उसे
dataके रूप में पढ़ता है (रेंडरर टेम्पलेट से पहले#let data = json("/data.json")बाँधता है)।#panic(),]या$x$जैसे मान टेक्स्ट के रूप में छपते हैं; एक टेस्ट यह जाँचता है। टेम्पलेट और कुछ नहीं पढ़ सकते: कोई फ़ाइल नहीं, कोई पैकेज नहीं। - रेंडरिंग blocking थ्रेड पर चलती है। अधिकतम
pdf.max_concurrentदस्तावेज़ (1 से 64; डिफ़ॉल्ट दो, एक CPU पर एक) एक ही समय पर रेंडर होते हैं; बाकी स्लॉट का इंतज़ार करते हैं। Typst का memoization कैश हर दस्तावेज़ के बाद खाली किया जाता है, इसलिए मेमोरी फिर नीचे आ जाती है।
| कुंजी | अर्थ |
|---|---|
pdf.fonts | अतिरिक्त .ttf, .otf, .ttc या .otc फ़ॉन्ट की डायरेक्टरी, शुरू में एक बार पढ़ी जाती है |
pdf.max_concurrent | एक ही समय पर रेंडर होने वाले दस्तावेज़ |
फ़ॉन्ट: Libertinus Serif (regular, bold, italic, bold italic; SIL Open Font License) एम्बेडेड है और तुर्की को कवर करता है; DejaVu Sans Mono उन चिह्नों के लिए फ़ॉलबैक के रूप में एम्बेडेड है जो Libertinus में नहीं हैं, जैसे ₺। उनके लाइसेंस crates/zeyn/assets/fonts में हैं। कोई math फ़ॉन्ट नहीं है; यदि टेम्पलेट समीकरण सेट करें तो pdf.fonts से एक जोड़ें।
Windows पर मापा गया (रिलीज़ बिल्ड, x86_64):
| परिणाम | |
|---|---|
| बाइनरी आकार | pdf के बिना 5.5 MB, उसके साथ लगभग 46 MB (Typst लगभग 39 MB जोड़ता है, फ़ॉन्ट 1.6 MB) |
| एक पृष्ठ का दस्तावेज़ | 4 से 7 ms, रेंडर करते समय 1 MB से कम हीप, 10 KB फ़ाइल |
| 1,000 पंक्तियों की सूची रिपोर्ट (25 पृष्ठ) | 0.54 से 0.74 s, रेंडर करते समय 51 MB पीक हीप, बाद में 0 रखा, 0.87 MB फ़ाइल |
| शुरू के बाद पहला दस्तावेज़ | Typst की अपनी lazily बनी तालिकाओं के लिए लगभग 42 MB हीप रहता है |
| फ़ॉन्ट | फ़ॉन्ट इंडेक्स के लिए 0.25 MB हीप |
डिबग बिल्ड लगभग दस गुना धीमा है (शोकेस में 2,000 पंक्तियों की रिपोर्ट में 71 s लगे)।
ईमेल
email Cargo feature lettre से सूचनाएँ भेजता है।
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::sendसंदेश रेंडर करता है और उसे मेमोरी वाली कतार में रखता है (अधिकतम 10,000 संदेश), फिर लौट आता है।zeyn.mailबैकग्राउंड जॉब कतार तुरंत भेजता है, विफल संदेश बैकऑफ़ के साथ फिर आज़माता है (दस सेकंड से पाँच बार जब तकMailer::retryकुछ और न कहे) और हर मिनट कतार देखता है। आठ विफल प्रयासों के बाद संदेश लॉग में त्रुटि के साथ छोड़ दिया जाता है। हर जॉब की तरह, जो कतार प्रक्रिया रुकने तक भेजी नहीं गई वह खो जाती है; टिकाऊ डिलीवरी को डेटा परत की डेटाबेस-समर्थित कतार चाहिए।- संदेश i18n कैटलॉग से आते हैं:
<template>.subject,<template>.textऔर वैकल्पिक रूप से<template>.html,{name}आर्ग्युमेंट के साथ। प्राप्तकर्ता की भाषा भेजने वाले की है जब तकMail::localeउसे सेट न करे। हर संदेश में सादा टेक्स्ट और HTML भाग होता है; HTML भाग में हर आर्ग्युमेंट escape होता है, और.htmlप्रविष्टि के बिना HTML escape किया टेक्स्ट है। विषय से पंक्ति विराम हटाए जाते हैं, और पते पार्स किए जाते हैं, इसलिए डेटा हेडर या प्राप्तकर्ता नहीं जोड़ सकता। Mail::transitionवह बदलाव लिखता है जो कॉल ने अभीWorkflow::applyसे किया (रिकॉर्ड, from, to, किसने और टिप्पणी), बिल्ट इनzeyn.mail.transitionटेम्पलेट के साथ। शोकेस मैनेजर को बताता है जब अवकाश अनुरोध बदलता है।
| कुंजी | अर्थ |
|---|---|
mail.from | भेजने वाला, जैसे Northwind <no-reply@example.com> (आवश्यक) |
mail.transport | smtp, file (प्रति संदेश एक .eml फ़ाइल, डेवलपमेंट और टेस्ट के लिए) या log (प्राप्तकर्ताओं की संख्या और आकार वाली लॉग पंक्ति, कभी पते या सामग्री नहीं; डिफ़ॉल्ट) |
mail.smtp.host, mail.smtp.port | SMTP सर्वर |
mail.smtp.tls | starttls (डिफ़ॉल्ट), tls (implicit TLS, आमतौर पर पोर्ट 465) या none (केवल स्थानीय रिले) |
mail.smtp.user, mail.smtp.password | क्रेडेंशियल; पासवर्ड एनवायरनमेंट जैसे सीक्रेट स्रोत से पढ़ें |
mail.file.dir | file ट्रांसपोर्ट की डायरेक्टरी |
TLS Mozilla रूट सर्टिफ़िकेट (webpki-roots) के साथ rustls इस्तेमाल करता है।