मुख्य सामग्री पर जाएँ

संचालन

यह पृष्ठ अंग्रेज़ी से अनुवादित है। अंग्रेज़ी पृष्ठ पढ़ें

प्रोडक्शन में 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.json

acme.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_signed LicenseError (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)

सर्वर प्रति उपयोगकर्ता क्या रखता है, और कहाँ:

स्थितिकहाँसाझा
दर सीमा काउंटरRateStoreRedisStore
सहेजे गए ग्रिड व्यू और डॉक लेआउटDataGridViewStoreRedisStore
निष्क्रियता टाइमआउटब्राउज़र (ऐप के सभी टैब)सर्वर पर कुछ नहीं
साइन इन और पुनः प्रमाणीकरणसिंगल साइन ऑन प्रॉक्सीसर्वर पर कुछ नहीं
संवेदनशील 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 पंक्ति जोड़ती है।
  • स्पैन, ट्रेसिंग बैकएंड के लिए:
स्पैनस्तरफ़ील्ड
requestinfoमेथड, पथ (क्वेरी स्ट्रिंग के बिना), स्टेटस, ms, और पता चलने पर: kind, Screen, action, उपयोगकर्ता, टेनेंट, ok, त्रुटि
runinfoएप्लिकेशन कोड: एक action, एक डेटा स्रोत (ग्रिड fetch, पंक्ति खोज, व्यू स्टोर), एक Screen बिल्डर
interceptordebugनाम (टाइप का नाम, या Interceptor::name), चरण (before या after)
  • otel Cargo 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)?)
  • PdfLink Screen पर डाउनलोड बटन रखता है। GET /_zeyn/pdf Screen को उसकी एक्सेस जाँचों के साथ खोलता है, उपयोगकर्ता के लिए action चलाता है (उसकी अनुमति और इंटरसेप्टर लागू होते हैं, कॉल का kind Document है) और 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.transportsmtp, file (प्रति संदेश एक .eml फ़ाइल, डेवलपमेंट और टेस्ट के लिए) या log (प्राप्तकर्ताओं की संख्या और आकार वाली लॉग पंक्ति, कभी पते या सामग्री नहीं; डिफ़ॉल्ट)
mail.smtp.host, mail.smtp.portSMTP सर्वर
mail.smtp.tlsstarttls (डिफ़ॉल्ट), tls (implicit TLS, आमतौर पर पोर्ट 465) या none (केवल स्थानीय रिले)
mail.smtp.user, mail.smtp.passwordक्रेडेंशियल; पासवर्ड एनवायरनमेंट जैसे सीक्रेट स्रोत से पढ़ें
mail.file.dirfile ट्रांसपोर्ट की डायरेक्टरी

TLS Mozilla रूट सर्टिफ़िकेट (webpki-roots) के साथ rustls इस्तेमाल करता है।