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

एंटरप्राइज़ सुविधाएँ

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

बिज़नेस ऐप के लिए क्रॉस कटिंग सुविधाएँ: डेटा ऑडिट, व्यक्तिगत डेटा सुरक्षा, सुरक्षित डिप्लॉय, हेल्थ चेक, सत्र सुरक्षा, CSV और XLSX इम्पोर्ट, बैकग्राउंड जॉब और अनुमोदन वर्कफ़्लो। सब कुछ सर्वर पर जाँचा जाता है; ब्राउज़र रनटाइम केवल उसे इस्तेमाल करने में सुखद बनाता है। KVKK और GDPR ध्यान में रखे गए गोपनीयता कानून हैं, लेकिन यहाँ कुछ भी किसी देश या उद्योग के लिए विशिष्ट नहीं है।

फ़ील्ड स्तर डेटा ऑडिट

AuditLog हर फ़ॉर्म सबमिट और audit चिह्नित action की हर कॉल पर एक AuditEntry लिखता है। सेव के लिए प्रविष्टि रिकॉर्ड और बदले हर फ़ील्ड को भी नाम देती है:

{"kind":"form","screen":"customer","action":"save_customer","ok":true,"user":"ayse",
 "model":"Customer","record":"2","changes":[{"field":"name","old":"Customer 2","new":"Renamed"}]}
  • फ़ॉर्म यह अपने आप करते हैं: पुराने मान उस रिकॉर्ड से आते हैं जो Screen ने सर्वर पर लोड किया, नए मान सत्यापित Valid<M> से। क्लाइंट जो भेजता है उसमें से कुछ भी पुराने मान के रूप में इस्तेमाल नहीं होता, और जो फ़ील्ड फ़ॉर्म क्लाइंट से नहीं लेता वे कभी बदले हुए नहीं दिखते।
  • API सेव अपने Valid<M> इनपुट से मॉडल और कुंजी दर्ज करते हैं। केवल action जानती है कि डेटाबेस में क्या है, इसलिए अंतर वह खुद बताती है:
  #[action(permission = "crm.customer.write", audit)]
  pub async fn save_customer(c: Valid<Customer>, store: Customers, ctx: Ctx) -> Result<Customer> {
      let current = store.get(c.id)?;
      ctx.record_changes(&current, &c);
      store.save(c.into_inner())
  }

फ़ॉर्म सेव action में भी यही करना ठीक है: action की रिपोर्ट फ़ॉर्म की रिपोर्ट की जगह ले लेती है।

  • मान सादे, भाषा से स्वतंत्र स्ट्रिंग हैं (1234.56 TRY, 2024-05-01, choice मान)। पंक्ति संस्करण छोड़े जाते हैं। मास्क किए फ़ील्ड मास्क रहते हैं (नीचे देखें)।
  • विफल सेव फिर भी रिकॉर्ड को नाम देता है, बिना बदलावों के।

फ़ील्ड मास्किंग

व्यक्तिगत डेटा रखने वाले टेक्स्ट फ़ील्ड को #[mask] से चिह्नित करें। यहाँ राष्ट्रीय आईडी •••• 8901 के रूप में दिखती है, ईमेल p••••@example.com के रूप में और टिप्पणियाँ •••• के रूप में:

#[entity(table = "people")]
pub struct Person {
    #[key]
    pub id: PersonId,
    pub name: String,
    #[mask(last4)]
    pub national_id: String,
    #[mask(email, permission = "hr.contact.unmask")]
    pub email: Option<String>,
    #[mask]
    pub notes: Option<String>,
}
  • मास्किंग सर्वर पर होती है, ग्रिड सेल, फ़ॉर्म, API आउटपुट, एजेंट टूल में और इसलिए CSV निर्यात में भी, जो सेल से बनते हैं। कच्चा मान अनुमति के बिना कभी ब्राउज़र या API क्लाइंट तक नहीं पहुँचता।
  • unmask अनुमति snake case में <model>.<field>.unmask है (person.national_id.unmask) जब तक एट्रिब्यूट कोई नाम न दे।
  • अनुमति वाले उपयोगकर्ता भी पहले मास्क किया टेक्स्ट देखते हैं, एक reveal बटन के साथ। reveal करना POST /_zeyn/reveal कॉल करता है, जो अनुमति फिर जाँचता है और ऑडिट रिकॉर्ड लिखता है (kind reveal, फ़ील्ड revealed में)। अस्वीकार किए प्रयास भी ऑडिट होते हैं।
  • अनुमति वाले API क्लाइंट को कच्चे मान मिलते हैं; ऐसा हर फ़ील्ड कॉल के ऑडिट रिकॉर्ड के revealed में सूचीबद्ध होता है।
  • फ़ॉर्म मास्क किए फ़ील्ड को केवल पढ़ने योग्य टेक्स्ट के रूप में दिखाते हैं। सेव पर सहेजा मान बना रहता है, जब तक उपयोगकर्ता उसे reveal न कर सके और reveal करने के बाद बदले। खाली मास्क किया फ़ील्ड सामान्य इनपुट है, इसलिए डेटा एंट्री स्टाफ़ उसे एक बार भर सकता है बिना बाद में उसे वापस पढ़ पाए।
  • मास्क किए फ़ील्ड पर फ़िल्टर और क्रम उन उपयोगकर्ताओं के लिए अनदेखे किए जाते हैं जो उसे reveal नहीं कर सकते, ताकि खोजकर मान का अंदाज़ा न लगाया जा सके। यह ग्रिड के हर पठन (पृष्ठ, निर्यात, सभी पंक्तियों पर बल्क action, सहेजे गए व्यू, एजेंट टूल) और DataQuery लेने वाली API action पर लागू होता है। मास्क किए कॉलम में .filter() न जोड़ें।
  • पृष्ठांकन कर्सर में कभी मास्क किए फ़ील्ड का मान नहीं होता। मास्क किए फ़ील्ड से क्रमबद्ध सूची कुंजी की जगह स्थिति से पृष्ठबद्ध होती है।
  • ग्रिड कभी मास्क किए फ़ील्ड से समूहित नहीं करता; उस पर group_by अनदेखा और लॉग किया जाता है।
  • Island और अन्य डेटा स्रोत आउटपुट किसी भी गहराई पर मास्क होते हैं, API आउटपुट की तरह: भीतर के हर मॉडल के मास्क किए फ़ील्ड मास्क किए टेक्स्ट के रूप में निकलते हैं, और संवेदनशील मॉडल की कुंजियाँ पठन के रूप में दर्ज होती हैं।
  • ग्रिड reveal पंक्ति को ग्रिड की action से कुंजी पर फ़िल्टर के साथ खोजता है, इसलिए action को अपने कुंजी फ़ील्ड पर फ़िल्टर का सम्मान करना होगा।
  • #[mask] केवल String और Option<String> पर कंपाइल होता है।

संवेदनशील रिकॉर्ड पठन

किसने कौन सा व्यक्तिगत डेटा कब देखा, यह भी दर्ज होता है, ठीक उसी तरह जैसे किसने उसे बदला।

  • #[entity(table = "cases", sensitive)] (या #[entity] के नीचे #[sensitive]) मॉडल को चिह्नित करता है। हर बार जब उपयोगकर्ता को ऐसा रिकॉर्ड दिखाया जाता है, कॉल का ऑडिट रिकॉर्ड उसे reads में सूचीबद्ध करता है ({"model":"Case","id":"2"}): ग्रिड पृष्ठ की पंक्तियाँ, फ़ॉर्म, एजेंट टूल परिणाम और API आउटपुट। पठन वहाँ दर्ज होते हैं जहाँ डेटा रेंडर होता है, इसलिए लेआउट के भीतर के कंपोनेंट भी गिने जाते हैं।
  • #[screen(..., sensitive)] Screen के हर खुलने को पठन के रूप में दर्ज करता है जिसका model Screen का नाम और id उसके params हैं, उन Screen के लिए जो किसी एक मॉडल से न बँधा व्यक्तिगत डेटा दिखाती हैं।
  • #[screen(..., reason)] कोई भी डेटा दिखाए जाने से पहले उपयोगकर्ता से कारण भी पूछता है। जब तक कारण न दिया जाए, सर्वर पृष्ठ, उसके अंशों, ग्रिड, फ़ॉर्म और reveal कॉल को कोड reason_required के साथ 428 से उत्तर देता है (पूरे पृष्ठ लोड पर कारण फ़ॉर्म वाला पृष्ठ)। रनटाइम डायलॉग में पूछता है, POST /_zeyn/reason {screen, reason} भेजता है (3 से 300 अक्षर) और अनुरोध दोहराता है। कारण 15 मिनट तक HttpOnly, SameSite=Strict कुकी में रखा जाता है, केवल उसी Screen को खोलता है जिसके लिए दिया गया और उस Screen के हर ऑडिट रिकॉर्ड के reason में लिखा जाता है। एजेंट और API क्लाइंट कारण नहीं दे सकते, इसलिए वे ऐसी Screen नहीं खोल सकते।
  • संवेदनशील मॉडल के ग्रिड के निर्यात (CSV, XLSX, PDF) हर फ़ाइल के लिए एक ऑडिट रिकॉर्ड लिखते हैं जिसमें export सेट होता है: मॉडल, प्रारूप, पंक्तियों की संख्या और माँगे गए क्रम और फ़िल्टर। वे reads में हर पंक्ति सूचीबद्ध नहीं करते, इसलिए बड़ा निर्यात सस्ता रहता है; अन्य मॉडल के निर्यात पर कोई अतिरिक्त लागत नहीं। sensitive या reason Screen से निर्यात Screen पठन भी दर्ज करता है।
  • Screen के lazy हिस्से (POST /_zeyn/node, lazy टैब द्वारा इस्तेमाल) इंटरसेप्टर चलाते हैं और पठन ठीक Screen खोलने की तरह दर्ज करते हैं।
  • ctx.record_read(&record) एप्लिकेशन कोड से पठन दर्ज करता है।
  • पठन रिकॉर्ड AuditLog हर कॉल के लिए लिखता है, पृष्ठ लोड समेत, जैसे ही कोई संवेदनशील रिकॉर्ड दिखाया गया। ऑडिट इंटरसेप्टर के बिना कुछ दर्ज नहीं होता; जिन ऐप में व्यक्तिगत डेटा है उन्हें हमेशा एक पंजीकृत करना चाहिए।

अनुमोदन वर्कफ़्लो

वर्कफ़्लो एक छोटी, टाइप्ड स्टेट मशीन है। स्थितियाँ #[choice] enum हैं; हर बदलाव अपनी शुरुआती और अंतिम स्थिति और ज़रूरी अनुमति का नाम देता है:

#[choice]
pub enum LeaveState { Draft, Submitted, Approved, Rejected }

pub fn leave_flow() -> Workflow<LeaveState> {
    Workflow::new()
        .transition("submit", LeaveState::Draft, LeaveState::Submitted, "people.leave.request")
        .transition("approve", LeaveState::Submitted, LeaveState::Approved, "people.leave.approve")
        .transition("reject", LeaveState::Submitted, LeaveState::Rejected, "people.leave.approve")
}

#[action(permission = "people.leave.request", audit)]
pub async fn move_leave(req: TransitionRequest<LeaveRequestId>, store: LeaveStore, ctx: Ctx) -> Result<LeaveState> {
    let mut leave = store.get(req.id)?;
    leave.state = leave_flow().apply(&ctx, leave.state, &req.transition, req.comment.as_deref())?;
    store.save(leave)
}
  • apply सर्वर पर स्टोर से पढ़ी स्थिति के साथ चलता है। अज्ञात बदलाव 404 है, जो रिकॉर्ड अब शुरुआती स्थिति में नहीं है वह 409 है (किसी ने उसे पहले बदल दिया), अनुमति का अभाव 403 है, 1000 अक्षरों से लंबी टिप्पणी 422 है।
  • हर बदलाव ऑडिट रिकॉर्ड में transition में लिखा जाता है: नाम, from, to और टिप्पणी; उपयोगकर्ता और समय रिकॉर्ड में ही हैं।
  • Transitions::of(flow, state, id, action) मौजूदा स्थिति, एक वैकल्पिक टिप्पणी और उपयोगकर्ता जो हर बदलाव उससे कर सकता है उसके लिए एक बटन दिखाता है (Workflow::allowed)। रिकॉर्ड कुंजी सर्वर पर Screen से आती है, इसलिए क्लाइंट दूसरा रिकॉर्ड नहीं बदल सकता। एजेंट इसे फ़ॉर्म टूल के रूप में देखते हैं।
  • बटन लेबल कैटलॉग में workflow.<name> से आते हैं; .label(...) हाथ से एक सेट करता है।
  • Workflow::check अमान्य अनुमतियाँ और एक ही स्थिति से दो बार घोषित बदलाव बताता है; इसे टेस्ट में कॉल करें।
  • शोकेस के अवकाश अनुरोध इसे इस्तेमाल करते हैं: ड्राफ़्ट के रूप में सहेजें, सबमिट करें, फिर स्वीकृत या अस्वीकृत करें।

सुरक्षित डिप्लॉय

  • हर उत्तर में x-zeyn-build और हर पृष्ठ में <meta name="zeyn-build"> होता है। आईडी ZEYN_BUILD_ID है जब सेट हो (अक्षर, अंक, ., _, -, अधिकतम 64), वरना एक्ज़ीक्यूटेबल का हैश। zeyn::server::build_id() इसे लॉग के लिए लौटाता है।
  • जब रनटाइम दूसरी बिल्ड का उत्तर देखता है, वह विनम्र सूचना दिखाता है: नया संस्करण उपलब्ध है, तैयार होने पर रीलोड करें। यह अपने आप कभी रीलोड नहीं करता।
  • उपयोगकर्ता फ़ॉर्म में जो टाइप करते हैं वह प्रति Screen, params और फ़ॉर्म sessionStorage में रखा जाता है और रीलोड के बाद एक टिप्पणी और उसे हटाने के बटन के साथ वापस रखा जाता है। मास्क किए, पासवर्ड, फ़ाइल और छिपे इनपुट कभी नहीं रखे जाते। ड्राफ़्ट वह पंक्ति संस्करण रखता है जिस पर वह टाइप किया गया, इसलिए जिस रिकॉर्ड को बीच में किसी ने बदल दिया उसका ड्राफ़्ट सहेजना टकराव के रूप में अस्वीकार होता है। सफल सेव ड्राफ़्ट साफ़ करता है; एक दिन से पुराने ड्राफ़्ट हटा दिए जाते हैं। sessionStorage केवल उसी ब्राउज़र टैब में रहता है।

हेल्थ चेक

  • GET /_zeyn/health प्रक्रिया चलते रहने तक 200 ok का उत्तर देता है। इसे liveness के लिए इस्तेमाल करें।
  • GET /_zeyn/ready App::readiness(name, check) से जोड़ी हर जाँच को एक साथ चलाता है, प्रत्येक पाँच सेकंड, और 200 {"status":"ready"} या 503 {"status":"not_ready","failed":["database"]} का उत्तर देता है। केवल जाँचों के नाम दिखाए जाते हैं, त्रुटियाँ कभी नहीं।
  • दोनों base_path के नीचे हैं (/billing/_zeyn/health), Cache-Control: no-store के साथ उत्तर देते हैं और उन्हें साइन इन नहीं चाहिए।
  • readiness परिणाम एक सेकंड के लिए दोबारा इस्तेमाल होता है (READY_CACHE), और एक समय में केवल एक अनुरोध जाँचें चलाता है, इसलिए probes की बाढ़ डेटाबेस पर बोझ नहीं डाल सकती।
  • हेल्थ पर कभी दर सीमा नहीं लगती, इसलिए व्यस्त probe खुद को लॉक करके स्वस्थ ऐप को रीस्टार्ट नहीं करा सकता। Readiness दर सीमाओं में गिनी जाती है जब तक वह probe_ips पते से न आए।
  • IP नियम अब भी लागू हैं: deny_ips हमेशा, allow_only_ips भी जब तक probe पता App::probe_ips([...]) में सूचीबद्ध न हो, जो केवल ये दो एंडपॉइंट उसके लिए खोलता है।

डेवलपमेंट लेटेंसी

App::dev_latency(Duration::from_millis(400)) हर कॉल (पृष्ठ, अंश, ग्रिड, फ़ॉर्म, API, MCP और रनटाइम कॉल; स्थिर एसेट या हेल्थ probe नहीं) में देरी करता है ताकि डेवलपर लोडिंग स्थितियाँ और डबल क्लिक वैसे देखें जैसे धीमे नेटवर्क पर उपयोगकर्ता देखते हैं। यह केवल डिबग बिल्ड में काम करता है: रिलीज़ बिल्ड में मेथड कुछ नहीं करता और देरी वाला कोड कंपाइल से बाहर हो जाता है, इसलिए गलती से छोड़ देना प्रोडक्शन को धीमा नहीं कर सकता। चालू रहने पर स्टार्टअप पर चेतावनी छपती है।

सत्र सुरक्षा

साइन इन सिंगल साइन ऑन प्रॉक्सी, बिल्ट इन OpenID Connect साइन इन या bearer टोकन से आता है (देखें docs/auth.md), इसलिए इन सुविधाओं को केवल इतना चाहिए कि ऑथेंटिकेटर बताए उपयोगकर्ता कौन है और, पुनः प्रमाणीकरण के लिए, उसने कब साइन इन किया (Identity::authenticated_at; ForwardAuth::auth_time("x-auth-time") Unix सेकंड या RFC 3339 पढ़ता है; DevAuth हर अनुरोध को ताज़ा साइन इन बताता है)।

  • App::idle_timeout(Duration) रनटाइम को अंत से idle_warning पहले (डिफ़ॉल्ट एक मिनट) उपयोगकर्ता को काउंटडाउन और "साइन इन रहें" बटन वाले alert डायलॉग में चेतावनी देने देता है, जो POST /_zeyn/keepalive कॉल करता है ताकि सामने का प्रॉक्सी गतिविधि देखे। ऐप के किसी भी टैब में इनपुट गिना जाता है। अंत में उपयोगकर्ता logout_url पर जाता है, वरना return_to के साथ login_url पर, वरना साइन आउट Screen देखता है। फ़ॉर्म ड्राफ़्ट रखे जाते हैं। निष्क्रियता टाइमआउट केवल भूली हुई Screen बंद करता है; असली सत्र खत्म करना आइडेंटिटी प्रोवाइडर का काम है।
  • #[action(permission = "...", reauth)] को App::reauth_within (डिफ़ॉल्ट पाँच मिनट) के भीतर साइन इन चाहिए। वरना क्लाइंट कुछ भी भेजे, सर्वर कोड reauth_required के साथ 401 का उत्तर देता है (API पर problem type urn:zeyn:problem:reauth_required)। रनटाइम उपयोगकर्ता से नए टैब में फिर साइन इन करने को कहता है (reauth_url, वरना login_url) और उनके लौटने पर अनुरोध दोहराता है, इसलिए टाइप किया कुछ नहीं खोता। साइन इन समय के बिना आइडेंटिटी कभी पास नहीं होती।

CSV इम्पोर्ट

#[action(permission = "people.leave.import", audit)]
pub async fn import_leaves(rows: Vec<Valid<LeaveRequest>>, store: LeaveStore) -> Result<usize> { ... }

Screen::new("Import").body(Import::of(import_leaves).max_rows(1000))
  • उपयोगकर्ता एक CSV फ़ाइल चुनता है। रनटाइम उसे UTF-8 के रूप में पढ़ता है, या Windows-1254 के रूप में जब वह वैध UTF-8 न हो (तुर्की स्प्रेडशीट), और टेक्स्ट को JSON बॉडी में POST /_zeyn/import पर भेजता है (multipart की ज़रूरत नहीं; अधिकतम 8 MB)। सर्वर सारांश, पहली दस पंक्तियों का पूर्वावलोकन, प्रति पंक्ति त्रुटियाँ और डाउनलोड योग्य त्रुटि रिपोर्ट के साथ उत्तर देता है।
  • पार्सिंग: बाइट ऑर्डर मार्क हटाया जाता है, शीर्षक पंक्ति से ,, ; या टैब पहचाना जाता है, विभाजकों और पंक्ति विराम के साथ " उद्धरण, \r\n या \n।
  • सीमाएँ फ़ाइल पढ़ते समय जाँची जाती हैं, बाद में नहीं: पढ़ना पंक्ति सीमा पर रुकता है, 512 से अधिक सेल का शीर्षक अस्वीकार होता है, और डेटा पंक्तियाँ शीर्षक से अधिकतम एक सेल आगे तक रखती हैं (ताकि अतिरिक्त मान फिर भी बताए जाएँ)। XLSX फ़ाइल शीर्षक में जितने कॉलम हैं उससे अधिक नहीं पढ़ती, उसका शीर्षक अधिकतम उतने कॉलम नाम दे सकता है जितने मॉडल इम्पोर्ट कर सकता है, और 16 MB से बड़ा shared strings भाग कुछ भी खोलने से पहले अस्वीकार होता है।
  • कॉलम फ़ील्ड नाम या ऐप की किसी भी भाषा के लेबल से मिलाए जाते हैं, अक्षर के छोटे-बड़े और तुर्की बिंदु वाले और बिना बिंदु वाले i की अनदेखी करके। अज्ञात, केवल पढ़ने योग्य, कुंजी और पंक्ति संस्करण कॉलम अस्वीकार होते हैं।
  • हर पंक्ति ठीक मॉडल के फ़ॉर्म की तरह पढ़ी और सत्यापित होती है: मान वैसे जैसे उस भाषा का उपयोगकर्ता टाइप करता है (1.234,56, 05.12.2026), फ़ील्ड नियम और मॉडल का अपना validate।
  • इम्पोर्ट action को केवल मान्य पंक्तियाँ भेजता है; .all_or_nothing() तब तक कुछ इम्पोर्ट नहीं करता जब तक हर पंक्ति मान्य न हो। इम्पोर्ट पर सर्वर फ़ाइल को फिर पढ़ता और सत्यापित करता है; पूर्वावलोकन से कुछ भी भरोसेमंद नहीं माना जाता। दोनों चरणों को action की अनुमति चाहिए, और इम्पोर्ट ऑडिट होता है।
  • .max_rows(n) पंक्तियाँ सीमित करता है (डिफ़ॉल्ट 5000, कभी 50000 से अधिक नहीं)। त्रुटि रिपोर्ट स्प्रेडशीट फ़ॉर्मूले निष्क्रिय करती है। मास्क किए फ़ील्ड पूर्वावलोकन में मास्क दिखते हैं।

XLSX इम्पोर्ट

xlsx Cargo feature के साथ वही कंपोनेंट Excel वर्कबुक (.xlsx) भी लेता है। रनटाइम फ़ाइल base64 में भेजता है (उसी JSON बॉडी में "csv" की जगह "xlsx"), और सर्वर पहली वर्कशीट को calamine से उसी पंक्ति दर पंक्ति सत्यापन, पूर्वावलोकन और त्रुटि रिपोर्ट में पढ़ता है जैसा CSV का।

  • सेल अपने प्रकार रखते हैं: संख्या संख्या के रूप में और तारीख़ सेल तारीख़ के रूप में पढ़ी जाती है, उपयोगकर्ता की भाषा चाहे जो हो, इसलिए तुर्की सत्र में 1234.5 फिर भी 1.234,50 है। तारीख़ सेल में दीवार घड़ी के समय उपयोगकर्ता के समय क्षेत्र में पढ़े जाते हैं, जैसे फ़ॉर्म पढ़ता है। Choice मान या लेबल से मेल खाते हैं।
  • सीमाएँ: अधिकतम 8 MB फ़ाइल, और आर्काइव अधिकतम 1000 भागों में अधिकतम 64 MB में खुल सकता है, जो कुछ खोलने से पहले आर्काइव के घोषित आकारों से जाँचा जाता है। सेल एक-एक करके पढ़े जाते हैं और पढ़ना पंक्ति सीमा पर रुकता है, इसलिए दस लाख पंक्तियों की शीट सीमा से अधिक लागत नहीं लगाती। जो फ़ाइल वर्कबुक नहीं है, पासवर्ड से सुरक्षित है या अधूरी कटी है उसे स्पष्ट त्रुटि मिलती है।
  • केवल पहली वर्कशीट पढ़ी जाती है। फ़ॉर्मूले अपने कैश किए मान के साथ गिने जाते हैं।

बैकग्राउंड जॉब

App::new("Billing")
    .job_concurrency(2)
    .job("send_reminders", Schedule::every(Duration::from_secs(3600)).retry(3, Duration::from_secs(30)), send_reminders)
    .job("rebuild_index", Schedule::manual(), rebuild_index)
    .jobs_permission("ops.jobs.read")
    .serve_until("0.0.0.0:3000", shutdown_signal())
  • जॉब async fn(Ctx) -> Result<()> है। उसके कॉन्टेक्स्ट में ऐप की सेवाएँ, कॉन्फ़िगरेशन और डिफ़ॉल्ट भाषा हैं, और आइडेंटिटी job:<name> है जिसकी कोई अनुमति नहीं।
  • Schedule::every(d) हर अंतराल पर चलता है; Schedule::manual() केवल ट्रिगर होने पर। action इंजेक्ट होने योग्य Jobs सेवा से जॉब ट्रिगर करती हैं: jobs.trigger("rebuild_index")। चलते समय ट्रिगर हुआ जॉब बाद में एक बार और चलता है; जॉब एक ही समय में दो बार कभी नहीं चलता।
  • .retry(times, first_delay) विफल रन को फिर आज़माता है, इंतज़ार को दस मिनट तक दोगुना करते हुए।
  • अधिकतम job_concurrency जॉब (डिफ़ॉल्ट चार) एक साथ चलते हैं।
  • स्थिति (state, रन, विफलताएँ, आखिरी शुरुआत और अंत, आखिरी त्रुटि कोड) कोड में jobs.status() है और jobs_permission वाले उपयोगकर्ताओं के लिए GET /_zeyn/jobs। त्रुटि विवरण केवल सर्वर लॉग में जाते हैं।
  • App::serve_until(addr, signal) सिग्नल पूरा होने पर अनुरोध लेना बंद करता है, चल रहे अनुरोध खत्म होने देता है, पुनः प्रयास के इंतज़ार रद्द करता है और चल रहे जॉब को 30 सेकंड देता है।
  • स्टोर के बिना जॉब मेमोरी में हैं: प्रक्रिया रुकने पर जो काम प्रतीक्षा में या चल रहा है वह खो जाता है, और कई सर्वर के साथ हर एक अपना शेड्यूल चलाता है।

टिकाऊ जॉब

App::new("Billing")
    .job_store(SqlJobStore::new(db))
    .queued_job("send_invoice", Schedule::manual().retry(5, Duration::from_secs(10)), send_invoice)
    .job("close_day", Schedule::every(Duration::from_secs(86_400)), close_day)

async fn send_invoice(ctx: Ctx, invoice_id: String) -> Result<()> { ... }
jobs.enqueue("send_invoice", "42").await?;
  • JobStore रन रखता है: enqueue, schedule, लीज़ (वर्कर आईडी और अंत समय) के साथ claim, heartbeat, complete और fail। MemoryJobStore (अधिकतम 10,000 रन, सबसे पुराना विफल रन पहले हटाया जाता है) डिफ़ॉल्ट है; SqlJobStore (feature postgres, oracle) रन zeyn_jobs में रखता है। create_job_tables(dialect, prefix) माइग्रेशन के लिए स्टेटमेंट देता है।
  • स्टोर सेट होने पर Schedule::every जॉब अपना अगला रन स्टोर में रखते हैं, इसलिए रीस्टार्ट उसे नहीं हिलाता और हर अंतराल केवल एक सर्वर चलाता है। trigger भी वहाँ एक रन कतार में लगाता है।
  • queued_job जॉब को हर रन का पेलोड मिलता है (अधिकतम MAX_JOB_PAYLOAD, 4000 बाइट); सादे जॉब भी कतार में लग सकते हैं और उसे अनदेखा करते हैं। enqueue_after रन में देरी करता है।
  • दावा करना उन जॉब के देय रन लेता है जिन्हें यह सर्वर जानता है, सबसे पुराने पहले, और उन्हें JOB_LEASE (60 सेकंड) के लिए रखता है, रन के चलते रहने पर हर 20 सेकंड नवीनीकृत। complete और fail रन को केवल तभी बदलते हैं जब वर्कर उसे अब भी रखता हो। जिस रन का वर्कर रुक गया उसे लीज़ खत्म होने के बाद फिर दावे में लिया जाता है; खोया प्रयास गिना जाता है, इसलिए जो रन अपना सर्वर क्रैश कर दे वह विफल के रूप में खत्म होता है।
  • विफल प्रयास बैकऑफ़ का इंतज़ार करता है (दोगुना, अधिकतम MAX_BACKOFF_MS, दस मिनट) जब तक प्रयास बचे हों, फिर अपने त्रुटि कोड के साथ failed के रूप में रहता है। शेड्यूल अपने अगले अंतराल पर चला जाता है।
  • वर्कर हर JOB_POLL (एक सेकंड) पर और उसी सर्वर पर enqueue के तुरंत बाद देय रन खोजते हैं। हर दावा अधिकतम उतने रन लेता है जितने जॉब स्लॉट खाली हैं, इसलिए मेमोरी कतार के साथ नहीं बढ़ती।
  • PostgreSQL एक UPDATE ... WHERE id IN (SELECT ... FOR UPDATE SKIP LOCKED) RETURNING में दावा करता है। Oracle 11g और बाद के ट्रांज़ैक्शन में ROWNUM और FOR UPDATE SKIP LOCKED के साथ आईडी select करते हैं, उन्हें अपडेट करते हैं और कमिट से पहले उन्हें वापस पढ़ते हैं। Oracle लॉक करने वाले select को क्रमबद्ध नहीं कर सकता, इसलिए वहाँ देय रन किसी निश्चित क्रम के बिना लिए जाते हैं। समय BIGINT या NUMBER(19) कॉलम में Unix मिलीसेकंड हैं।
  • स्टोर के साथ, एक ही जॉब का शेड्यूल्ड रन और ट्रिगर किया रन ओवरलैप कर सकते हैं; जो जॉब एक साथ दो बार नहीं चलने चाहिए उन्हें खुद अपनी रक्षा करनी चाहिए (उदाहरण के लिए पंक्ति लॉक)।