تخطي إلى المحتوى

ميزات المؤسسات

هذه الصفحة مترجمة من الإنجليزية. اقرأ الصفحة الإنجليزية

ميزات شاملة لتطبيقات الأعمال: تدقيق البيانات، حماية البيانات الشخصية، عمليات النشر الآمنة، فحوصات الصحة، أمان الجلسات، استيراد ملفات CSV وXLSX، المهام الخلفية وسير عمل الموافقات. كل شيء يُفحص على الخادم، ودور بيئة التشغيل في المتصفح هو جعل الاستخدام مريحًا فقط. قانونا KVKK وGDPR هما قانونا الخصوصية المأخوذان في الاعتبار، لكن لا شيء هنا خاص ببلد أو بقطاع معين.

تدقيق البيانات على مستوى الحقل

يكتب AuditLog سجلًا واحدًا من نوع AuditEntry لكل إرسال نموذج ولكل استدعاء لإجراء موسوم بـ audit. وفي عمليات الحفظ يذكر السجل أيضًا السجل المعدَّل وكل حقل تغيّر:

{"kind":"form","screen":"customer","action":"save_customer","ok":true,"user":"ayse",
 "model":"Customer","record":"2","changes":[{"field":"name","old":"Customer 2","new":"Renamed"}]}
  • تقوم النماذج بذلك من تلقاء نفسها: القيم القديمة تأتي من السجل الذي حمّلته الشاشة على الخادم، والقيم الجديدة من Valid<M> بعد التحقق. لا يُستخدم أي شيء يرسله العميل كقيمة قديمة، ولا تظهر كمتغيرة الحقول التي لا يأخذها النموذج من العميل.
  • عمليات الحفظ عبر API تسجّل النموذج والمفتاح من مدخل Valid<M>. الإجراء وحده يعرف ما في قاعدة البيانات، لذلك يبلّغ هو نفسه عن الفرق:
  #[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())
  }

لا مانع من فعل الشيء نفسه في إجراءات الحفظ الخاصة بالنماذج: يحل تقرير الإجراء محل تقرير النموذج.

  • القيم نصوص بسيطة لا تعتمد على اللغة (1234.56 TRY، 2024-05-01، قيم الخيارات). تُستبعد إصدارات الصفوف. تبقى الحقول المقنّعة مقنّعة (انظر أدناه).
  • الحفظ الفاشل يذكر السجل أيضًا، دون أي تغييرات.

إخفاء الحقول

ضع #[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 وأدوات الوكلاء (agents)، وبالتالي في ملفات CSV المصدَّرة التي تُبنى من الخلايا. لا تصل القيمة الخام إلى متصفح أو عميل API دون الصلاحية.
  • صلاحية رفع الإخفاء هي <model>.<field>.unmask بنمط snake case (person.national_id.unmask) ما لم تحدد السمة اسمًا آخر.
  • المستخدمون الذين يملكون الصلاحية يرون النص المقنّع أولًا، مع زر للكشف. يستدعي الكشف POST /_zeyn/reveal، الذي يفحص الصلاحية مرة أخرى ويكتب سجل تدقيق (النوع reveal، والحقل في revealed). المحاولات المرفوضة تُدقَّق أيضًا.
  • عملاء API الذين يملكون الصلاحية يحصلون على القيم الخام، ويُذكر كل حقل من هذا النوع في revealed ضمن سجل تدقيق الاستدعاء.
  • تعرض النماذج الحقول المقنّعة كنص للقراءة فقط. تبقى القيمة المخزنة عند الحفظ ما لم يكن للمستخدم حق الكشف وقام بتغييرها بعد الكشف. الحقل المقنّع الفارغ مدخل عادي، فيستطيع موظفو إدخال البيانات تعبئته مرة واحدة دون أن يتمكنوا من قراءته لاحقًا.
  • تُتجاهل المرشحات (filters) والفرز على حقل مقنّع للمستخدمين غير المسموح لهم بكشفه، فلا يمكن تخمين القيمة بالبحث. ينطبق هذا على كل قراءة للجدول (الصفحات، التصدير، الإجراءات الجماعية على كل الصفوف، العروض المحفوظة، أدوات الوكلاء) وعلى إجراءات API التي تأخذ DataQuery. لا تضف .filter() إلى الأعمدة المقنّعة.
  • لا تحتوي مؤشرات التصفح (cursors) على قيمة حقل مقنّع أبدًا. القائمة المرتبة حسب حقل مقنّع تتصفح حسب الموضع بدل المفتاح.
  • لا يجمّع الجدول حسب حقل مقنّع أبدًا؛ يُتجاهل group_by عليه ويُسجَّل.
  • مخرجات الجزر (islands) ومصادر البيانات الأخرى تُقنَّع على أي عمق، مثل مخرجات API: تخرج الحقول المقنّعة لكل نموذج بداخلها كنص مقنّع، وتُسجَّل مفاتيح النماذج الحساسة كقراءات.
  • يبحث كشف الجدول عن الصف عبر إجراء الجدول مع مرشح على المفتاح، لذا يجب أن يحترم الإجراء المرشحات على حقل مفتاحه.
  • لا يُترجم #[mask] بنجاح إلا على String وOption<String>.

قراءات السجلات الحساسة

يُسجَّل من رأى أي بيانات شخصية ومتى، كما يُسجَّل من غيّرها.

  • #[entity(table = "cases", sensitive)] (أو #[sensitive] تحت #[entity]) يعلّم نموذجًا. في كل مرة يُعرض فيها سجل من هذا النوع على مستخدم، يدرجه سجل تدقيق الاستدعاء في reads ({"model":"Case","id":"2"}): صفوف صفحة جدول، نموذج، نتيجة أداة وكيل ومخرجات API. تُسجَّل القراءات حيث تُعرض البيانات، لذا تُحتسب المكونات داخل التخطيطات أيضًا.
  • #[screen(..., sensitive)] يسجّل كل فتح للشاشة كقراءة يكون model فيها اسم الشاشة وid معاملاتها، للشاشات التي تعرض بيانات شخصية غير مرتبطة بنموذج واحد.
  • #[screen(..., reason)] يسأل المستخدم أيضًا عن السبب قبل عرض أي بيانات. يرد الخادم بالرمز 428 مع الشفرة reason_required (صفحة فيها نموذج سبب عند التحميل الكامل للصفحة) على الصفحة وأجزائها واستدعاءات الجدول والنموذج والكشف، إلى أن يُعطى سبب. تسأل بيئة التشغيل في مربع حوار، وترسل POST /_zeyn/reason {screen, reason} (من 3 إلى 300 حرف) وتعيد الطلب. يُحفظ السبب 15 دقيقة في كعكة (cookie) من نوع HttpOnly وSameSite=Strict، ولا يفتح إلا الشاشة التي أُعطي لها، ويُكتب في reason لكل سجل تدقيق لتلك الشاشة. لا يستطيع الوكلاء وعملاء API إعطاء سبب، لذا لا يمكنهم فتح هذه الشاشات.
  • تصدير (CSV وXLSX وPDF) جدول على نموذج حساس يكتب سجل تدقيق واحدًا لكل ملف مع تعيين export: النموذج والصيغة وعدد الصفوف والفرز والمرشحات المطلوبة. لا يُدرج كل صف في reads، فيبقى التصدير الكبير رخيصًا؛ أما تصدير النماذج الأخرى فلا يكلّف شيئًا إضافيًا. التصدير من شاشة sensitive أو reason يسجّل أيضًا قراءة الشاشة.
  • الأجزاء الكسولة من الشاشة (POST /_zeyn/node، وتستخدمها علامات التبويب الكسولة) تشغّل المعترضات (interceptors) وتسجّل القراءات تمامًا مثل فتح الشاشة.
  • ctx.record_read(&record) يسجّل قراءة من كود التطبيق.
  • يكتب AuditLog سجلات القراءة لأي استدعاء، بما فيه تحميل الصفحات، بمجرد عرض سجل حساس. دون معترض تدقيق لا يُسجَّل شيء؛ ينبغي للتطبيقات التي تحتفظ ببيانات شخصية أن تسجّل واحدًا دائمًا.

سير عمل الموافقات

سير العمل (workflow) آلة حالات صغيرة ذات أنواع محددة. الحالات هي enum من نوع #[choice]؛ وكل انتقال يسمّي حالة البداية وحالة النهاية والصلاحية التي يحتاجها:

#[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: الاسم والحالة السابقة والحالة الجديدة والتعليق؛ أما المستخدم والوقت فهما في السجل نفسه.
  • Transitions::of(flow, state, id, action) يعرض الحالة الحالية وتعليقًا اختياريًا وزرًا واحدًا لكل انتقال يحق للمستخدم تنفيذه منها (Workflow::allowed). يأتي مفتاح السجل من الشاشة على الخادم، فلا يستطيع العميل نقل سجل آخر. يراه الوكلاء كأداة نموذج.
  • تأتي تسميات الأزرار من workflow.<name> في الفهارس (catalogs)؛ و.label(...) يضبط واحدة يدويًا.
  • يبلّغ Workflow::check عن الصلاحيات غير الصالحة وعن الانتقالات المعلنة مرتين من الحالة نفسها؛ استدعِه في اختبار.
  • طلبات الإجازة في العرض التوضيحي (showcase) تستخدم هذا: حفظ كمسودة، ثم إرسال، ثم موافقة أو رفض.

عمليات النشر الآمنة

  • تحمل كل استجابة الترويسة x-zeyn-build، وتحمل كل صفحة <meta name="zeyn-build">. المعرّف هو ZEYN_BUILD_ID إن كان مضبوطًا (حروف وأرقام و. و_ و-، حتى 64 حرفًا)، وإلا فهو تجزئة (hash) للملف التنفيذي. تعيده zeyn::server::build_id() للاستخدام في السجلات.
  • عندما ترى بيئة التشغيل استجابة من بناء (build) آخر، تعرض إشعارًا مهذبًا: يتوفر إصدار جديد، أعد التحميل عندما تكون مستعدًا. لا تعيد التحميل من تلقاء نفسها أبدًا.
  • ما يكتبه المستخدمون في النماذج يُحفظ في 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 ولا يحتاجان إلى تسجيل دخول.
  • تُعاد نتيجة الجاهزية نفسها لمدة ثانية واحدة (READY_CACHE)، ولا يشغّل الفحوصات إلا طلب واحد في كل مرة، لذا لا يستطيع سيل من الفحوصات تحميل قاعدة البيانات.
  • لا تُطبَّق حدود المعدل على الصحة أبدًا، فلا يستطيع فحص مزدحم أن يحجب نفسه فيعيد تشغيل تطبيق سليم. أما الجاهزية فتُحتسب ضمن حدود المعدل ما لم تأتِ من عنوان في probe_ips.
  • تبقى قواعد IP سارية: deny_ips دائمًا، وallow_only_ips أيضًا ما لم يكن عنوان الفحص مدرجًا في App::probe_ips([...])، الذي يفتح هاتين النقطتين فقط له.

زمن الاستجابة أثناء التطوير

App::dev_latency(Duration::from_millis(400)) يؤخّر كل استدعاء (الصفحات والأجزاء والجداول والنماذج وAPI وMCP واستدعاءات بيئة التشغيل؛ لا الملفات الثابتة ولا فحوصات الصحة) ليرى المطورون حالات التحميل والنقرات المزدوجة كما يراها المستخدمون على شبكة بطيئة. يعمل في بناءات التصحيح (debug) فقط: في بناءات الإصدار (release) لا تفعل الدالة شيئًا ويُحذف كود التأخير عند الترجمة، لذا فإن تركها عن طريق الخطأ لا يبطئ الإنتاج. يُطبع تحذير عند بدء التشغيل ما دامت مفعّلة.

أمان الجلسات

يأتي تسجيل الدخول من وكيل تسجيل دخول موحد (single sign on) أو من تسجيل دخول 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 من النهاية (دقيقة واحدة افتراضيًا) في مربع حوار تنبيه مع عدّاد تنازلي وزر "ابقَ مسجلًا"، يستدعي POST /_zeyn/keepalive ليرى الوكيل الذي أمامه نشاطًا. يُحتسب الإدخال في أي علامة تبويب للتطبيق. عند النهاية يذهب المستخدم إلى logout_url، وإلا إلى login_url مع return_to، وإلا يرى شاشة تسجيل خروج. تبقى مسودات النماذج. مهلة الخمول تغلق الشاشات المنسية فقط؛ إنهاء الجلسة الفعلية من مهام مزوّد الهوية.
  • #[action(permission = "...", reauth)] يحتاج إلى تسجيل دخول ضمن App::reauth_within (خمس دقائق افتراضيًا). وإلا يرد الخادم بـ 401 مع الشفرة reauth_required (نوع المشكلة urn:zeyn:problem:reauth_required في API)، مهما أرسل العميل. تطلب بيئة التشغيل من المستخدم تسجيل الدخول مجددًا في علامة تبويب جديدة (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). يرد الخادم بملخص ومعاينة لأول عشرة صفوف والأخطاء لكل صف وتقرير أخطاء قابل للتنزيل.
  • التحليل: تُحذف علامة ترتيب البايت (BOM)، ويُكتشف , أو ; أو علامة التبويب من سطر الترويسة، ويُدعم الاقتباس بـ " مع وجود فواصل وفواصل أسطر داخله، و\r\n أو \n.
  • تُفحص الحدود أثناء قراءة الملف لا بعدها: تتوقف القراءة عند حد الصفوف، وتُرفض ترويسة فيها أكثر من 512 خلية، وتحتفظ صفوف البيانات بخلية واحدة على الأكثر بعد الترويسة (فتُبلَّغ القيم الزائدة). يقرأ ملف XLSX أعمدة لا تزيد على عدد أعمدة الترويسة، ولا يجوز أن تسمّي ترويسته أعمدة أكثر مما يستطيع النموذج استيراده، ويُرفض جزء النصوص المشتركة (shared strings) الأكبر من 16 MB قبل فك أي شيء.
  • تُطابَق الأعمدة باسم الحقل أو بتسميته بأي لغة من لغات التطبيق، دون اعتبار حالة الأحرف ودون اعتبار الفرق بين حرفي i التركيين المنقوط وغير المنقوط. تُرفض الأعمدة المجهولة وللقراءة فقط والمفاتيح وإصدار الصف.
  • يُقرأ كل صف ويُتحقق منه تمامًا كما في نموذج هذا النموذج: القيم كما يكتبها مستخدم تلك اللغة (1.234,56، 05.12.2026)، وقواعد الحقول وvalidate الخاصة بالنموذج.
  • يرسل الاستيراد الصفوف الصالحة فقط إلى الإجراء؛ و.all_or_nothing() لا يستورد شيئًا ما لم تكن كل الصفوف صالحة. عند الاستيراد يقرأ الخادم الملف ويتحقق منه مرة أخرى؛ لا يُوثَق بأي شيء من المعاينة. كلتا الخطوتين تحتاجان إلى صلاحية الإجراء، ويُدقَّق الاستيراد.
  • .max_rows(n) يحدد الصفوف (5000 افتراضيًا، ولا أكثر من 50000 أبدًا). يُبطل تقرير الأخطاء مفعول صيغ جداول البيانات. تظهر الحقول المقنّعة مقنّعة في المعاينة.

استيراد XLSX

مع ميزة Cargo المسماة xlsx يقبل المكوّن نفسه أيضًا مصنّفات Excel (.xlsx). ترسل بيئة التشغيل الملف بترميز base64 ("xlsx" بدل "csv" في جسم JSON نفسه)، ويقرأ الخادم ورقة العمل الأولى بواسطة calamine إلى نفس التحقق صفًا بصف والمعاينة وتقرير الأخطاء كما في CSV.

  • تحتفظ الخلايا بأنواعها: يُقرأ الرقم كرقم وخلية التاريخ كتاريخ، مهما كانت لغة المستخدم، لذا فإن 1234.5 في جلسة تركية يبقى 1.234,50. تُقرأ أوقات الساعة الحائطية في خلية التاريخ بالمنطقة الزمنية للمستخدم، كما يقرأها النموذج. تُطابَق الخيارات بالقيمة أو بالتسمية.
  • الحدود: ملف لا يزيد على 8 MB، ويجوز أن يُفك الأرشيف إلى 64 MB على الأكثر في 1000 جزء على الأكثر، ويُفحص ذلك من الأحجام التي يعلنها الأرشيف قبل فك أي شيء. تُقرأ الخلايا واحدة تلو أخرى وتتوقف القراءة عند حد الصفوف، لذا فإن ورقة فيها مليون صف لا تكلّف أكثر من الحد. الملف الذي ليس مصنّفًا أو المحمي بكلمة مرور أو المقطوع يحصل على خطأ واضح.
  • تُقرأ ورقة العمل الأولى فقط. تُحتسب الصيغ بقيمتها المخزنة مؤقتًا.

المهام الخلفية

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())
  • المهمة (job) هي async fn(Ctx) -> Result<()>. يحتوي سياقها على خدمات التطبيق وإعداداته واللغة الافتراضية، والهوية job:<name> دون أي صلاحيات.
  • Schedule::every(d) تعمل كل فترة؛ وSchedule::manual() عند التشغيل اليدوي فقط. تشغّل الإجراءات المهام عبر الخدمة القابلة للحقن Jobs: jobs.trigger("rebuild_index"). المهمة التي تُشغَّل وهي تعمل تعمل مرة أخرى بعد انتهائها؛ ولا تعمل مهمة مرتين في الوقت نفسه أبدًا.
  • .retry(times, first_delay) يعيد محاولة التشغيل الفاشل، مضاعفًا الانتظار حتى عشر دقائق.
  • يعمل job_concurrency مهمة على الأكثر في وقت واحد (أربع افتراضيًا).
  • الحالة (الوضع وعدد التشغيلات والإخفاقات وآخر بداية ونهاية وشفرة آخر خطأ) هي jobs.status() في الكود وGET /_zeyn/jobs للمستخدمين الذين يملكون jobs_permission. تفاصيل الأخطاء تذهب إلى سجل الخادم فقط.
  • 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 مع عقد إيجار (lease) (معرّف العامل ووقت الانتهاء) وheartbeat وcomplete وfail. MemoryJobStore (10,000 تشغيل على الأكثر، ويُحذف أقدم تشغيل فاشل أولًا) هو الافتراضي؛ وSqlJobStore (الميزتان postgres وoracle) يحتفظ بالتشغيلات في zeyn_jobs. تعطي create_job_tables(dialect, prefix) العبارات اللازمة للترحيل (migration).
  • مع ضبط مخزن، تحتفظ مهام Schedule::every بموعد تشغيلها التالي في المخزن، فلا تنقله إعادة التشغيل ولا يشغّل كل فترة إلا خادم واحد. يضع trigger تشغيلًا في الطابور هناك أيضًا.
  • مهام queued_job تحصل على حمولة (payload) كل تشغيل (MAX_JOB_PAYLOAD على الأكثر، 4000 بايت)؛ ويمكن وضع المهام العادية في الطابور أيضًا فتتجاهلها. enqueue_after يؤخّر تشغيلًا.
  • يأخذ الاستيلاء (claim) التشغيلات المستحقة للمهام التي يعرفها هذا الخادم، الأقدم أولًا، ويحتفظ بها لمدة JOB_LEASE (60 ثانية)، ويجدد ذلك كل 20 ثانية ما دام التشغيل يعمل. لا يغيّر complete وfail تشغيلًا إلا ما دام العامل ما زال ممسكًا به. التشغيل الذي توقف عامله يُستولى عليه من جديد بعد انتهاء عقد إيجاره؛ وتُحتسب المحاولة الضائعة، لذا فإن تشغيلًا يُسقط خادمه ينتهي فاشلًا.
  • المحاولة الفاشلة تنتظر التراجع (backoff) (مضاعفًا، حتى 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 داخل معاملة، ثم يحدّثها ويقرأها قبل الإتمام (commit). لا يستطيع Oracle ترتيب استعلام قفل، لذا تؤخذ التشغيلات المستحقة هناك دون ترتيب ثابت. الأوقات هي ميلي ثانية Unix في أعمدة BIGINT أو NUMBER(19).
  • مع وجود مخزن، قد يتداخل تشغيل مجدول وتشغيل مُطلَق لنفس المهمة؛ ينبغي للمهام التي يجب ألا تعمل مرتين في وقت واحد أن تحمي نفسها (بقفل صف، مثلًا).