هذه الصفحة مترجمة من الإنجليزية. اقرأ الصفحة الإنجليزية
ميزات شاملة لتطبيقات الأعمال: تدقيق البيانات، حماية البيانات الشخصية، عمليات النشر الآمنة، فحوصات الصحة، أمان الجلسات، استيراد ملفات 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(¤t, &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). - مع وجود مخزن، قد يتداخل تشغيل مجدول وتشغيل مُطلَق لنفس المهمة؛ ينبغي للمهام التي يجب ألا تعمل مرتين في وقت واحد أن تحمي نفسها (بقفل صف، مثلًا).