الأنظمة الموزعة صعبة. تتعطل العمليات، وتفشل الشبكات، وتتوقف الخدمات في أسوأ اللحظات. قوائم انتظار المهام التقليدية تُعالج المهام البسيطة، لكن ماذا يحدث حين تحتاج إلى مسار عمل يمتد لساعات ويشمل خدمات متعددة ويجب أن يصمد عند إعادة تشغيل الخادم؟
Temporal.io يحل هذه المشكلة من خلال توفير منصة تنفيذ دائمة حيث يعمل كودك كأن الأعطال لا وجود لها. تستمر مسارات عملك رغم الأعطال، وتحدث عمليات إعادة المحاولة تلقائياً، وتحصل على رؤية كاملة لكل خطوة من خطوات التنفيذ.
في هذا الدرس، ستبني نظام معالجة الطلبات باستخدام Temporal.io و TypeScript. بنهاية الدرس، ستُتقن نمذجة العمليات التجارية المعقدة كمسارات عمل موثوقة وقابلة للمراقبة.
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20 أو أحدث
- خبرة متوسطة في TypeScript
- فهم أساسي لـ async/await والـ Promises
- Docker (اختياري — Temporal CLI يتولى التطوير المحلي)
- إلمام بواجهات REST API
ما الذي ستبنيه
ستُنشئ نظام معالجة طلبات بالخطوات التالية:
- التحقق من الطلب والمخزون
- خصم مبلغ الدفع من العميل
- إرسال بريد إلكتروني للتأكيد
- جدولة التسليم والشحن
- معالجة الأخطاء في أي خطوة مع إعادة المحاولة التلقائية
مسار العمل هذا مقاوم للأعطال تماماً — إذا تعطل الخادم في منتصف العملية، يستأنف Temporal من حيث توقف عند إعادة تشغيل الخادم.
الخطوة 1: إعداد خادم Temporal للتطوير
أسهل طريقة لتشغيل Temporal محلياً هي استخدام Temporal CLI. ثبّته عبر Homebrew على macOS:
brew install temporalأو حمّله من صفحة إصدارات Temporal CLI الرسمية لأنظمة Linux و Windows.
ابدأ خادم التطوير:
temporal server start-devهذا يُشغّل:
- Temporal Server على المنفذ 7233
- واجهة الويب على
http://localhost:8233
افتح واجهة الويب في متصفحك لمراقبة تنفيذ مسارات العمل. أبقِ هذه النافذة مفتوحة طوال الدرس.
الخطوة 2: تهيئة مشروع TypeScript
أنشئ مشروع Node.js جديد:
mkdir temporal-order-processing
cd temporal-order-processing
npm init -yثبّت Temporal SDK وأدوات TypeScript:
npm install @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity
npm install -D typescript ts-node @types/nodeهيّئ TypeScript:
npx tsc --initحدّث tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}أنشئ هيكل المشروع:
mkdir -p src/{workflows,activities,workers,client,shared}الخطوة 3: المفاهيم الأساسية لـ Temporal
قبل كتابة الكود، لنفهم المكونات الأربعة الرئيسية:
مسارات العمل (Workflows)
مسار العمل دالة دائمة تُنسّق منطق عملك. مسارات العمل حتمية — بنفس المدخلات، تُنتج دائماً نفس تسلسل العمليات. هذه الحتمية هي ما يُمكّن Temporal من إعادة التشغيل والاستعادة بعد الأعطال.
القيود الأساسية لمسارات العمل:
- لا I/O مباشر (لا استعلامات قاعدة بيانات، لا طلبات HTTP، لا وصول لنظام الملفات)
- لا استخدام مباشر لـ
Date.now()أوMath.random() - جميع الآثار الجانبية تمر عبر الأنشطة
الأنشطة (Activities)
النشاط هو المكان الذي يعيش فيه منطق عملك الفعلي — استعلامات قاعدة البيانات، طلبات API، إرسال البريد الإلكتروني. يُسمح للأنشطة بالفشل وستُعاد محاولتها وفق سياسة إعادة المحاولة.
العمال (Workers)
العامل عملية تستضيف مسارات عملك وأنشطتك. يستطلع خادم Temporal للمهام وينفذها. يمكنك تشغيل عمال متعددين للتوسع الأفقي.
عميل Temporal
العميل يُستخدم لبدء مسارات العمل وإرسال إشارات إلى المسارات الجارية.
الخطوة 4: تعريف الأنواع المشتركة
// src/shared/types.ts
export interface OrderItem {
productId: string;
quantity: number;
price: number;
}
export interface OrderInput {
orderId: string;
customerId: string;
items: OrderItem[];
totalAmount: number;
paymentMethodId: string;
}
export interface OrderResult {
orderId: string;
status: "completed" | "failed" | "cancelled";
chargeId?: string;
trackingNumber?: string;
message: string;
}الخطوة 5: كتابة الأنشطة
الأنشطة تحتوي على منطق العمل الفعلي:
// src/activities/order-activities.ts
import { OrderInput } from "../shared/types";
export async function validateOrder(input: OrderInput): Promise<boolean> {
console.log(`التحقق من الطلب ${input.orderId}`);
await new Promise((resolve) => setTimeout(resolve, 100));
if (input.items.length === 0) {
throw new Error("يجب أن يحتوي الطلب على عنصر واحد على الأقل");
}
if (input.totalAmount <= 0) {
throw new Error("يجب أن يكون إجمالي الطلب أكبر من الصفر");
}
return true;
}
export async function chargePayment(
orderId: string,
amount: number,
paymentMethodId: string
): Promise<string> {
console.log(`خصم ${amount} للطلب ${orderId}`);
await new Promise((resolve) => setTimeout(resolve, 500));
const chargeId = `ch_${Date.now()}_${orderId}`;
return chargeId;
}
export async function sendConfirmationEmail(
orderId: string,
customerId: string,
chargeId: string
): Promise<void> {
console.log(`إرسال بريد التأكيد للطلب ${orderId}`);
await new Promise((resolve) => setTimeout(resolve, 200));
}
export async function scheduleShipping(
orderId: string,
itemCount: number
): Promise<string> {
console.log(`جدولة شحن الطلب ${orderId}`);
await new Promise((resolve) => setTimeout(resolve, 300));
return `TRK${Date.now()}`;
}الأنشطة يمكنها إجراء أي استدعاءات I/O — استعلامات قاعدة البيانات، طلبات HTTP، الوصول لنظام الملفات. إنها المكان الوحيد الذي يجب أن تحدث فيه الآثار الجانبية في مسار عمل Temporal.
الخطوة 6: كتابة مسار عمل الطلبات
الآن أنشئ مسار العمل الذي ينسق هذه الأنشطة:
// src/workflows/order-workflow.ts
import {
proxyActivities,
defineSignal,
defineQuery,
setHandler,
} from "@temporalio/workflow";
import type * as activities from "../activities/order-activities";
import type { OrderInput, OrderResult } from "../shared/types";
const {
validateOrder,
chargePayment,
sendConfirmationEmail,
scheduleShipping,
} = proxyActivities<typeof activities>({
startToCloseTimeout: "30 seconds",
retry: {
maximumAttempts: 3,
initialInterval: "1 second",
maximumInterval: "10 seconds",
backoffCoefficient: 2,
},
});
export const cancelOrderSignal = defineSignal<[string]>("cancelOrder");
export const getOrderStatusQuery = defineQuery<string>("getOrderStatus");
export async function orderWorkflow(input: OrderInput): Promise<OrderResult> {
let cancelled = false;
let cancellationReason = "";
let currentStatus = "pending";
setHandler(cancelOrderSignal, (reason: string) => {
cancelled = true;
cancellationReason = reason;
});
setHandler(getOrderStatusQuery, () => currentStatus);
try {
currentStatus = "validating";
await validateOrder(input);
if (cancelled) {
return {
orderId: input.orderId,
status: "cancelled",
message: `تم إلغاء الطلب قبل الدفع: ${cancellationReason}`,
};
}
currentStatus = "charging";
const chargeId = await chargePayment(
input.orderId,
input.totalAmount,
input.paymentMethodId
);
currentStatus = "confirming";
await sendConfirmationEmail(input.orderId, input.customerId, chargeId);
currentStatus = "shipping";
const trackingNumber = await scheduleShipping(
input.orderId,
input.items.length
);
currentStatus = "completed";
return {
orderId: input.orderId,
status: "completed",
chargeId,
trackingNumber,
message: "تمت معالجة الطلب بنجاح",
};
} catch (error) {
currentStatus = "failed";
return {
orderId: input.orderId,
status: "failed",
message: error instanceof Error ? error.message : "حدث خطأ غير معروف",
};
}
}لا تستورد وحدات Node.js المضمّنة مثل fs أو http أو crypto مباشرةً في ملفات مسارات العمل. بيئة تشغيل Temporal تمنع هذه الواردات. استخدم import type فقط للاستيراد من ملفات الأنشطة.
الخطوة 7: إعداد العامل
// src/workers/worker.ts
import { Worker, NativeConnection } from "@temporalio/worker";
import * as activities from "../activities/order-activities";
import path from "path";
async function run() {
const connection = await NativeConnection.connect({
address: "localhost:7233",
});
const worker = await Worker.create({
connection,
namespace: "default",
taskQueue: "order-processing",
workflowsPath: path.join(__dirname, "../workflows"),
activities,
});
console.log("العامل يعمل — يستطلع المهام على القائمة: order-processing");
await worker.run();
}
run().catch((err) => {
console.error("فشل تشغيل العامل:", err);
process.exit(1);
});الخطوة 8: إنشاء عميل مسار العمل
// src/client/start-workflow.ts
import { Client, Connection } from "@temporalio/client";
import {
orderWorkflow,
getOrderStatusQuery,
} from "../workflows/order-workflow";
import { OrderInput } from "../shared/types";
async function main() {
const connection = await Connection.connect({ address: "localhost:7233" });
const client = new Client({ connection, namespace: "default" });
const orderInput: OrderInput = {
orderId: "ORDER-001",
customerId: "CUST-123",
items: [
{ productId: "PROD-A", quantity: 2, price: 29.99 },
],
totalAmount: 59.98,
paymentMethodId: "pm_card_visa",
};
const handle = await client.workflow.start(orderWorkflow, {
taskQueue: "order-processing",
workflowId: `order-${orderInput.orderId}`,
args: [orderInput],
});
console.log(`بدأ مسار العمل: ${handle.workflowId}`);
// استطلاع الحالة حتى اكتمال مسار العمل
let done = false;
while (!done) {
const status = await handle.query(getOrderStatusQuery);
console.log(`الحالة الحالية: ${status}`);
if (status === "completed" || status === "failed") done = true;
await new Promise((r) => setTimeout(r, 1000));
}
const result = await handle.result();
console.log("النتيجة النهائية:", result);
await connection.close();
}
main().catch(console.error);الخطوة 9: إضافة السكريبتات وتشغيل النظام
حدّث package.json:
{
"scripts": {
"worker": "ts-node src/workers/worker.ts",
"start": "ts-node src/client/start-workflow.ts"
}
}افتح ثلاث نوافذ طرفية:
النافذة 1 — خادم Temporal:
temporal server start-devالنافذة 2 — عملية العامل:
npm run workerالنافذة 3 — تشغيل مسار العمل:
npm run startستظهر سجلات كل نشاط في نافذة العامل، وتحديثات الحالة في نافذة العميل. افتح http://localhost:8233 لمشاهدة سجل الأحداث الكامل في الوقت الفعلي.
الخطوة 10: فهم سياسات إعادة المحاولة
سياسات إعادة المحاولة قابلة للتهيئة بالكامل. هذا ما تعنيه كل إعداد:
proxyActivities({
startToCloseTimeout: "30 seconds", // أقصى وقت لمحاولة واحدة
scheduleToCloseTimeout: "5 minutes", // أقصى وقت إجمالي شاملاً جميع المحاولات
retry: {
maximumAttempts: 3,
initialInterval: "1 second", // انتظار 1 ثانية قبل أول إعادة محاولة
maximumInterval: "30 seconds", // لا تنتظر أكثر من 30 ثانية
backoffCoefficient: 2, // مضاعف أسي: 1s، 2s، 4s
nonRetryableErrorTypes: ["PaymentDeclinedError"],
},
})لأنشطة الدفع، استخدم maximumAttempts: 1 لتجنب الخصم المزدوج. لأنشطة البريد الإلكتروني، اسمح بما يصل إلى 5 محاولات — خدمات البريد الإلكتروني تعاني من أعطال عابرة تُحل عادةً تلقائياً.
الخطوة 11: مسارات العمل طويلة الأمد مع sleep
دالة sleep في Temporal تُوقف مسار العمل لأي مدة — ثوانٍ أو أشهر — دون استهلاك موارد:
import { sleep } from "@temporalio/workflow";
export async function subscriptionRenewalWorkflow(userId: string) {
// انتظر 30 يوماً قبل التجديد
await sleep("30 days");
await chargeRenewalFee(userId);
await sendRenewalConfirmation(userId);
// إعادة الجدولة للدورة القادمة
await sleep("30 days");
}هذه ميزة من أقوى ميزات Temporal — استبدل مهام cron بكود قابل للقراءة يعالج إعادة المحاولات والحالة تلقائياً.
الخطوة 12: الاختبار مع TestWorkflowEnvironment
Temporal يوفر بيئة اختبار تُتيح تشغيل مسارات العمل بدون خادم حقيقي:
// src/__tests__/order-workflow.test.ts
import { TestWorkflowEnvironment } from "@temporalio/testing";
import { Worker } from "@temporalio/worker";
import { orderWorkflow } from "../workflows/order-workflow";
import * as activities from "../activities/order-activities";
describe("مسار عمل الطلبات", () => {
let testEnv: TestWorkflowEnvironment;
beforeAll(async () => {
testEnv = await TestWorkflowEnvironment.createLocal();
});
afterAll(async () => {
await testEnv.teardown();
});
it("يكتمل بنجاح لطلب صالح", async () => {
const { client, nativeConnection } = testEnv;
const worker = await Worker.create({
connection: nativeConnection,
namespace: "default",
taskQueue: "test-queue",
workflowsPath: require.resolve("../workflows/order-workflow"),
activities,
});
const result = await worker.runUntil(
client.workflow.execute(orderWorkflow, {
taskQueue: "test-queue",
workflowId: "test-001",
args: [{
orderId: "TEST-001",
customerId: "CUST-001",
items: [{ productId: "P1", quantity: 1, price: 10 }],
totalAmount: 10,
paymentMethodId: "pm_test",
}],
})
);
expect(result.status).toBe("completed");
expect(result.trackingNumber).toBeDefined();
});
});استكشاف الأخطاء وإصلاحها
"Connection refused" عند تشغيل العامل:
تأكد من تشغيل temporal server start-dev في نافذة طرفية منفصلة وأن المنفذ 7233 متاح.
مسار العمل عالق في حالة "Running": تحقق من تشغيل عملية العامل وأنها متصلة بنفس اسم قائمة المهام المستخدم في العميل.
الأنشطة لا تُعاد محاولتها بعد الفشل:
تحقق من ضبط سياسة إعادة المحاولة على proxyActivities وليس داخل دالة مسار العمل.
الخطوات التالية
بعد إتقان الإعداد الأساسي، استكشف هذه الأنماط المتقدمة:
- مسارات العمل الفرعية: تقسيم مسارات العمل المعقدة إلى مسارات فرعية قابلة لإعادة الاستخدام
- الجداول الزمنية: استبدال مهام cron بواجهة برمجة جدولة Temporal المدمجة
- نمط Saga: تنفيذ المعاملات الموزعة مع أنشطة التعويض
- Temporal Cloud: Temporal المُدار بالكامل للإنتاج
الخلاصة
لقد بنيت نظام معالجة طلبات جاهزاً للإنتاج باستخدام Temporal.io و TypeScript. مسار عملك الآن يصمد عند أعطال الخادم، ويُعيد المحاولة تلقائياً للأنشطة الفاشلة، ويعرض الحالة في الوقت الفعلي عبر الاستعلامات.
القوة الحقيقية لـ Temporal تكمن في أنه يُزيل فئة كاملة من مشاكل الأنظمة الموزعة: لم تعد بحاجة لبناء منطق إعادة المحاولة يدوياً، أو إدارة حالة قائمة الانتظار، أو بناء معالجات المعاملات التعويضية من الصفر. سواء كنت تبني تدفقات دفع، أو خطوط إعداد المستخدمين، أو مهام ETL، تمنح Temporal منطق عملك المتانة التي يتطلبها الإنتاج.