Files
Caspian.WorkflowFramework/docs/workflow-cache-explained.fa.md

103 KiB
Raw Permalink Blame History

راهنمای آموزشی تغییرات کش ورکفلو: Memory، Redis و اجرای چندنودی

برای شروع از مفاهیم پایه و توضیح مرحله‌به‌مرحلهٔ UseCaching و UseCache، آموزش مخصوص مبتدی‌ها را بخوانید. این سند جزئیات فنی و فهرست تغییرات را ادامه می‌دهد.

مخاطب: توسعه‌دهنده‌ای که تازه با کش، Redis، تنظیم سرویس‌ها و اجرای چندنودی آشنا شده است.

تاریخ آغاز مستند: ۷ اکتبر ۲۰۲۶. آخرین تطبیق تنظیمات و راهنمای آموزشی با سورس: ۱۰ اکتبر ۲۰۲۶.

مبنای متن: کد نهایی موجود در ریپوها، فایل‌های تنظیمات، خروجی پکیج‌ها و گزارش تست‌ها. مثال‌های عددی برای آموزش هستند؛ شمارهٔ واقعی نسخهٔ اعتبار کش ممکن است متفاوت باشد.

نسخه‌های فعلی سورس و مصرف: ماژول‌های تغییرکردهٔ Core برابر 1.1.0-preview.10 و پکیج WorkflowFramework برابر 1.1.0-preview.28 هستند.

این فایل توضیح می‌دهد چه چیزی تغییر کرده، چرا تغییر کرده، چگونه تنظیم می‌شود و چه محدودیت‌هایی دارد. راهنمای کوتاه عملیاتی در workflow-cache.md قرار دارد.


خلاصهٔ سریع

UseCaching یک provider مشترک و تنظیمات آن را برای ماژول Elsa انتخاب می‌کند. management.UseCache، runtime.UseCache و http.UseCache مصرف‌کننده‌های تعریف‌ها، Triggerها و lookup HTTP ورودی را فعال می‌کنند. نام Redis در سه مصرف‌کننده تکرار نمی‌شود، چون مدیر کش مشترک از provider انتخاب‌شده استفاده می‌کند.

کش و قفل Redis اتصال میزبان را مصرف می‌کنند، اما تنظیم و مسئولیت آن‌ها مستقل است. SQL دادهٔ اصلی و اعتبار کش را نگه می‌دارد؛ Redis payload قابل اشتراک و قفل‌های موقت را نگه می‌دارد؛ اشیای اجرایی هر process محلی می‌مانند.

در NextBank فعلی، Sentinel در فایل پایهٔ appsettings.json هر سه میزبان قرار دارد و override لوکال در Development است. آرایهٔ Hosts بر اساس index ترکیب می‌شود؛ override یک endpoint، endpointهای اضافهٔ پایه را حذف نمی‌کند. نسخهٔ Framework فعلی 1.1.0-preview.28 است.

برای آموزش از ابتدا، راهنمای مبتدی را بخوانید. در ادامه، قراردادها، اعتبار SQL، جریان تغییر، انقضا و فایل‌های سورس با جزئیات توضیح داده می‌شوند.

۱. مسئله‌ای که می‌خواستیم حل کنیم

فرض کنید یک ورکفلو برای تأیید پرداخت داریم. تعریف آن می‌گوید پرداخت ابتدا توسط کارشناس و سپس توسط مدیر تأیید شود. حالا در Studio تعریف را تغییر می‌دهیم و مرحلهٔ دیگری اضافه می‌کنیم.

اگر برنامه فقط یک process داشته باشد، همان برنامه می‌تواند کش قبلی خود را کنار بگذارد و تعریف جدید را بخواند. اما در استقرار شما، چند سرور داریم و روی هر سرور دو node اجرا می‌شود. هر node می‌تواند process و حافظهٔ مستقل داشته باشد.

اگر درخواست تغییر تعریف به node اول برسد ولی درخواست اجرای بعدی به node دوم برسد، node دوم ممکن است هنوز تعریف قبلی را در حافظه داشته باشد. مشترک‌بودن SQL به‌تنهایی این نسخهٔ داخل حافظه را پاک نمی‌کند.

هدف تغییرات این بود که:

  1. کش داده‌های مشترک بتواند در Redis مشترک نگه‌داری شود.
  2. انتخاب Memory یا Redis قابل تنظیم باشد.
  3. اشیایی که باید مخصوص همان process باشند محلی باقی بمانند.
  4. همهٔ nodeها بتوانند اعتبار دادهٔ کش را بررسی کنند.
  5. اضافه‌کردن provider بعدی نیازمند بازنویسی مصرف‌کننده‌های کش نباشد.

۲. چند اصطلاح ساده که در ادامه لازم داریم

اصطلاح معنی ساده مثال در این پروژه
Server ماشینی که برنامه روی آن اجرا می‌شود یک سرور Windows
Process یک اجرای مستقل از برنامه با حافظهٔ خودش یک اجرای NextBankParsian
Node یک نمونه از برنامه در استقرار چندنمونه‌ای یکی از دو نمونهٔ روی سرور
Cache نسخه‌ای موقت از داده برای سریع‌تر خواندن آن کپی تعریف ورکفلو
MemoryCache کش داخل حافظهٔ همان process کش اسکریپت آمادهٔ اجرا
Redis سرویس جداگانه‌ای که چند process می‌توانند از آن استفاده کنند کش مشترک تعریف‌ها
Provider پیاده‌سازی یک قابلیت با یک فناوری مشخص Memory provider یا Redis provider
Store بخشی که دادهٔ اصلی را ذخیره یا جست‌وجو می‌کند Store تعریف‌ها در SQL
Invalidation کنارگذاشتن اعتبار دادهٔ قبلی کش پس از Publish نسخهٔ جدید
Payload خود داده‌ای که در کش نگه می‌داریم JSON تعریف یا فهرست Triggerها

در این مستند، node را process مستقل فرض می‌کنیم. اگر دو node واقعاً دو process باشند، حتی روی یک سرور هم MemoryCache مشترک ندارند.

تعریف ورکفلو با اجرای ورکفلو فرق دارد

تعریف ورکفلو شبیه دستورالعمل است: مراحل، شرط‌ها، ورودی‌ها و متغیرها را توضیح می‌دهد.

نمونهٔ در حال اجرا یک مورد واقعی است که طبق آن دستورالعمل پیش می‌رود؛ مثلاً پرداخت شمارهٔ ۱۲۳ که منتظر تأیید مدیر است.

تغییر کش تعریف‌ها به Redis به معنای انتقال وضعیت پرداخت ۱۲۳، Bookmarkهای آن یا لاگ‌هایش به Redis نیست. این داده‌های پایدار همچنان در Store اصلی SQL باقی می‌مانند.

۳. نقش پروژه‌ها در این مسیر

flowchart LR
    Studio["Caspian.Elsa.Studio\nویرایش و ارسال درخواست به بک‌اند"] --> Backend["بک‌اند ورکفلو"]
    Core["Caspian.Elsa.Core\nقراردادها، Featureها و موتور ورکفلو"] --> Framework["Caspian.WorkflowFramework\nتنظیم و بسته‌بندی قابلیت‌ها"]
    Framework --> NextBank["NextBankParsian\nمیزبان پکیج روی چند node"]
    NextBank --> Backend
  • Core: سازوکار کش و providerها را پیاده می‌کند.
  • WorkflowFramework: این قابلیت‌ها را برای مصرف‌کننده تنظیم و به‌صورت پکیج عرضه می‌کند.
  • NextBankParsian: پکیج را در Web، Admin و Workers استفاده می‌کند و تنظیم اتصال Redis را دارد.
  • Studio: تعریف را از طریق API بک‌اند ویرایش و منتشر می‌کند. برای این کار اتصال مستقیم Studio به Redis لازم نیست.

اگر Studio به یک بک‌اند جدا از NextBank متصل باشد، همان بک‌اند نیز writer تعریف است. برای هماهنگی، آن writer باید از پکیج و قرارداد جدید و تنظیم مناسب استفاده کند. تغییر صرفاً در UI، کش nodeهای بک‌اند را هماهنگ نمی‌کند.

۴. وضعیت قبل از تغییر چه بود؟

در WorkflowFramework دو فراخوانی وجود داشت:

management.UseCache();
runtime.UseCache();

اما مدیر کش Elsa، یعنی CacheManager، مستقیماً از IMemoryCache استفاده می‌کرد. بنابراین فعال‌کردن UseCache کش را در حافظهٔ همان process قرار می‌داد.

در NextBank نیز RegisterCache اتصال Redis و سرویس کش عمومی خود NextBank را ثبت می‌کرد. آن سرویس با مدیر کش Elsa یکی نبود. داشتن یک تنظیم به نام Redis در فایل تنظیمات، به‌خودی‌خود Elsa را Redis-backed نمی‌کرد.

flowchart TB
    A["Node A + MemoryCache خودش"] --> SQL["SQL مشترک"]
    B["Node B + MemoryCache خودش"] --> SQL
    C["Node C + MemoryCache خودش"] --> SQL

چرا این وضعیت مشکل داشت؟ SQL مشترک بود، ولی کپی‌های حافظهٔ nodeها مستقل بودند. تغییر داده در SQL، همهٔ آن کپی‌ها را خودکار تازه نمی‌کرد.

همچنین UseDistributedCache موجود در Core، برای انتقال پیام ابطال بود. وجود آن قابلیت به معنای نگه‌داری payload در Redis نبود. قفل اجرای توزیع‌شده نیز قابلیت جداگانه‌ای بود.

۵. وضعیت جدید چگونه است؟

در حالت Redis، دادهٔ قابل اشتراک در Redis ذخیره می‌شود. SQL محل نگه‌داری دادهٔ اصلی و ثبت پایدار اعتبار کش است. اشیای اجرایی در هر node ساخته می‌شوند.

flowchart TB
    A["Node A\nاسکریپت و مسیرهای محلی"] --> Redis["Redis مشترک\nتعریف‌ها و داده‌های قابل اشتراک"]
    B["Node B\nاسکریپت و مسیرهای محلی"] --> Redis
    A --> Ledger["SQL: CacheMutations\nاعتبار کش و تغییرهای ناتمام"]
    B --> Ledger
    A --> Data["SQL: دادهٔ اصلی ورکفلو"]
    B --> Data

این تصویر سه مسئولیت متفاوت را نشان می‌دهد:

مسئولیت محل انجام
نگه‌داری دادهٔ اصلی SQL
نگه‌داری کپی موقتِ مشترک Redis
نگه‌داری اشیای اجرایی مخصوص process حافظهٔ محلی

از طرف دیگر، جدول اعتبار SQL پاسخ می‌دهد: «آیا اکنون می‌توان از این نسخهٔ کش استفاده کرد؟» خود این جدول تعریف ورکفلو را نگه نمی‌دارد.

۶. ساختار جدید از چه الگویی پیروی می‌کند؟

الگوی providerهای دیتابیس در Core بررسی شد. در آن الگو، Feature اصلی قرارداد را می‌شناسد، provider پیاده‌سازی مناسب را انتخاب می‌کند و سرویس‌های مربوط را ثبت می‌کند.

مثال آشنای دیتابیس:

management.UseEntityFrameworkCore(ef =>
{
    ef.UseSqlServer(connectionString);
});

همین ایده برای کش استفاده شده است:

management.UseCache(cache =>
{
    cache.UseRedis(redis =>
    {
        redis.Database = 1;
        redis.KeyPrefix = "nextbank:workflow:cache:";
    });
});

Feature یعنی چه؟

Feature را یک «بستهٔ نصب و تنظیم قابلیت» در برنامه در نظر بگیرید. برای مثال، Feature کش تعیین می‌کند چه سرویس‌هایی باید در برنامه ثبت شوند.

در ساختار پروژه:

  • مرحلهٔ Configure برای تنظیم و ارتباط‌دادن Featureهاست.
  • مرحلهٔ Apply برای ثبت سرویس‌ها در مجموعهٔ DI است.
  • DependsOn می‌گوید یک Feature به چه Feature دیگری نیاز دارد.

DI و factory یعنی چه؟

DI مخفف Dependency Injection است. برنامه به‌جای ساختن وابستگی‌ها در هر متد، آن‌ها را از مجموعهٔ سرویس‌های ثبت‌شده دریافت می‌کند.

Factory یک تابع است که می‌گوید «این سرویس را چگونه پیدا یا ایجاد کن». مثلاً:

sp => sp.GetRequiredService<IConnectionMultiplexer>()

این تابع اتصال ثبت‌شدهٔ میزبان را پیدا می‌کند. با هر درخواست، اتصال تازه‌ای نمی‌سازد.

اجزای اصلی جدید

جزء وظیفه دلیل ایجاد
CacheFeature تنظیم مشترک و انتخاب provider انتخاب فناوری از مصرف‌کننده جدا شود
CacheConsumerFeature ورودی مشترک تنظیم برای Management، Runtime و HTTP بخش‌های مختلف از یک انتخاب هماهنگ استفاده کنند
ICacheManager عملیات کش مورد نیاز Elsa مصرف‌کننده نیاز نداشته باشد فناوری ذخیره‌سازی را بشناسد
ProviderCacheManager بررسی اعتبار، خواندن provider و اجرای factory سیاست کش در یک محل پیاده شود
ICacheProvider خواندن، نوشتن و حذف payload Memory و Redis قرارداد مشترک داشته باشند
ICacheConsistencyStore ثبت و خواندن اعتبار تغییرات اعتبار کش از محل ذخیرهٔ payload مستقل باشد
CacheEntryOptions عمر و وابستگی‌های ابطال یک ورودی تنظیم معمول کش به کلاس اختصاصی Memory وابسته نباشد

provider در سطح یک ماژول Elsa مشترک است. management.UseCache() و runtime.UseCache() دو انتخاب مستقل و پنهانی ایجاد نمی‌کنند. انتخاب صریح دو provider متفاوت در همان ماژول، خطای تنظیم می‌دهد.

۷. کدام داده‌ها مشترک‌اند و کدام داده‌ها محلی می‌مانند؟

داده در حالت Redis چه می‌شود؟ علت
تعریف و نسخهٔ ورکفلو در Redis کش می‌شود همهٔ nodeها باید تعریف معتبر را ببینند
فهرست و جست‌وجوی تعریف‌ها در Redis کش می‌شود نتیجهٔ جست‌وجو نیز با تغییر تعریف می‌تواند قدیمی شود
خلاصه، تعداد، وجود تعریف و یکتایی نام در Redis کش می‌شود فقط خود شیء تعریف نیازمند ابطال نیست
Triggerها در Redis کش می‌شوند مشخص می‌کنند رویداد کدام نسخه را شروع کند
نتیجهٔ جست‌وجوی HTTP شناسهٔ نسخه و Triggerها در Redis کش می‌شوند گراف اجرایی قابل اشتراک مستقیم نیست
گراف اجرایی از تعریف معتبر در همان process بازسازی می‌شود شامل Activityها و ارجاع‌های اجرایی است
اسکریپت C# کامپایل‌شده محلی می‌ماند delegate مربوط به همان process است
اسکریپت آمادهٔ JavaScript محلی می‌ماند ساختار آمادهٔ موتور اجرای محلی است
قالب parseشدهٔ Liquid در صورت فعال‌شدن Liquid، محلی می‌ماند شیء آمادهٔ کتابخانهٔ اجرای همان node است
جدول مسیرهای HTTP محلی می‌ماند ولی اعتبارش بررسی می‌شود خواندن محلی مسیرها لازم است؛ تغییرشان به همهٔ nodeها مربوط است
Bookmark، وضعیت اجرا و لاگ در SQL باقی می‌ماند دادهٔ پایدار اجرایی است
کش فایل‌های HTTP provider مستقل خود را دارد این تغییر مربوط به ذخیرهٔ فایل نیست
کش عمومی NextBank مستقل باقی می‌ماند قرارداد و مصرف‌کنندهٔ دیگری دارد
قفل اجرای ورکفلو تنظیم مستقل دارد هماهنگی اجرا با ذخیرهٔ payload فرق دارد

محلی‌بودن با بی‌نیازی از هماهنگی فرق دارد. جدول مسیرهای HTTP محلی است، ولی بعد از تغییر مسیر باید تازه شود. اسکریپت آمادهٔ اجرا نیز باید برای محتوایی که همان node اجرا می‌کند معتبر باشد.

در حالت Memory، کش داده‌های ورکفلو نیز محلی است. انتخاب Memory در چند node، به‌تنهایی کش مشترک یا هماهنگی خودکار بین آن‌ها ایجاد نمی‌کند. مسیر جدیدِ اعتبار SQL در این پیاده‌سازی برای provider مشترک انتخاب می‌شود؛ هماهنگی Memory با transportهای دیگر موضوع مستقلی است.

۸. تنظیم از appsettings.json

دو بخش متفاوت داریم: یکی اتصال عمومی Redis و دیگری سیاست کش ورکفلو.

۸.۱. اتصال عمومی Redis

ساختار اعلام‌شده برای سرورها به این شکل است:

{
  "Redis": {
    "AllowAdmin": true,
    "Ssl": false,
    "ConnectTimeout": 5000,
    "ConnectRetry": 2,
    "Database": 1,
    "ServiceName": "Redis1",
    "Hosts": [
      { "Host": "192.168.234.146", "Port": 26379 },
      { "Host": "192.168.234.145", "Port": 26379 },
      { "Host": "192.168.234.144", "Port": 26379 }
    ]
  }
}

این نمونه ساختار ارائه‌شدهٔ شماست؛ اتصال واقعی به این سرورها از این محیط آزمایش نشده است. در تنظیم اولیهٔ شما Port رشته بود؛ در این مثال برای خوانایی به‌صورت عدد آمده است.

ServiceName نام سرویسی است که Sentinel برای پیدا کردن Redis اصلی می‌شناسد. Sentinel دادهٔ تعریف‌ها را ذخیره نمی‌کند؛ Redis اصلی را معرفی می‌کند و در تغییر نقش سرورها کمک می‌کند. کد موجود NextBank این تنظیم را برای ساخت IConnectionMultiplexer مصرف می‌کند.

۸.۲. سیاست کش و قفل ورکفلو

بخش زیر به فایل‌های Web، Admin و Workers اضافه شده است:

{
  "Workflow": {
    "Cache": {
      "Enabled": true,
      "Provider": "Redis",
      "Duration": "1.00:00:00",
      "Redis": {
        "Database": 1,
        "KeyPrefix": "nextbank:workflow:cache:"
      }
    },
    "Runtime": {
      "DistributedLocking": {
        "Provider": "Redis",
        "Database": 1,
        "KeyPrefix": "nextbank:workflow:locks:",
        "LockAcquisitionTimeout": "00:00:10"
      }
    }
  }
}
تنظیم معنی
Enabled آیا payload کش خوانده و ذخیره شود؟
Provider محل نگه‌داری کش؛ فعلاً Memory یا Redis
Duration مدت نگه‌داری طبق نوع انقضای مصرف‌کننده
Redis:Database زیر Cache دیتابیس منطقی Redis برای payload کش
Redis:KeyPrefix زیر Cache پیشوند کلیدهای همین کش
DistributedLocking:Provider فناوری قفل اجرا؛ مستقل از provider کش
DistributedLocking:KeyPrefix فضای نام مستقل کلیدهای قفل
DistributedLocking:LockAcquisitionTimeout حداکثر انتظار برای گرفتن قفل؛ با مدت نگه‌داشتن قفل تفاوت دارد

1.00:00:00 یعنی یک روز. قالب TimeSpan به‌شکل روز، ساعت، دقیقه و ثانیه است؛ مثلاً 00:10:00 یعنی ده دقیقه.

با Enabled: false خواندن و ذخیرهٔ payload کنار گذاشته می‌شود. اگر provider هنوز Redis باشد، هماهنگی تغییرات و مسیرهای HTTP همچنان فعال می‌ماند. خاموش‌کردن کپی موقت داده نباید هماهنگی nodeها را خاموش کند.

۸.۳. پیش‌فرض‌ها

مورد پیش‌فرض
provider عمومی Core Memory
مدت عمومی Core یک دقیقه
مدت انتخاب‌شده در WorkflowFramework یک روز، مگر فایل یا کد آن را تغییر دهد
دیتابیس Redis provider -1؛ استفاده از دیتابیس پیش‌فرض اتصال
پیشوند Redis provider nextbank:workflow:cache:
قفل Runtime بدون تنظیم جدید رفتار قبلی File باقی می‌ماند

تنظیمات هنگام شروع برنامه خوانده می‌شوند. این تغییر، تعویض زندهٔ provider هنگام ویرایش فایل تنظیمات را پیاده نکرده است. برای اعمال انتخاب جدید برنامه باید دوباره راه‌اندازی شود.

۸.۴. اجرای لوکال Elsa.Server.Web در Core

اگر پروژهٔ startup شما Elsa.Server.Web است، تنظیمات Development اکنون هر دو مسئولیت را روی Redis فعال می‌کنند:

{
  "Redis": {
    "AllowAdmin": true,
    "Ssl": false,
    "ConnectTimeout": 5000,
    "ConnectRetry": 2,
    "Database": 1,
    "ServiceName": "",
    "Hosts": [
      { "Host": "localhost", "Port": 6379 }
    ]
  },
  "Workflow": {
    "Cache": {
      "Enabled": true,
      "Provider": "Redis",
      "Duration": "1.00:00:00",
      "Redis": {
        "Database": 1,
        "KeyPrefix": "nextbank:workflow:cache:"
      }
    },
    "Runtime": {
      "DistributedLocking": {
        "Provider": "Redis",
        "Database": 1,
        "KeyPrefix": "nextbank:workflow:locks:"
      }
    }
  }
}

فایل واقعی این تنظیمات appsettings.Development.json است. پروفایل Visual Studio باید محیط Development را انتخاب کند و Redis روی localhost:6379 اجرا باشد. ServiceName خالی یعنی اتصال مستقیم به Redis؛ در این نمونه Sentinel استفاده نمی‌شود.

در Program.cs، علاوه بر انتخاب provider کش، runtime.UseConfiguredDistributedLocking(configuration) فراخوانی می‌شود. helper تنظیم قفل تنظیمات قدیمی Runtime:DistributedLocking را می‌خواند و مقادیر مسیر جدید Workflow:Runtime:DistributedLocking را بر آن‌ها اعمال می‌کند. بنابراین Redis مسیر جدید، File فایل پایه را کنار می‌زند، ولی زمان انتظار ده‌ثانیه‌ای فایل پایه بدون تغییر باقی می‌ماند. اگر مسیر جدید وجود نداشته باشد، تنظیمات قبلی همچنان مصرف می‌شوند.

provider قفل Redis از همان IConnectionMultiplexer میزبان استفاده می‌کند که کش استفاده می‌کند. wrapper پیشوند را به نام هر قفل اضافه می‌کند تا Database و KeyPrefix واقعاً مصرف شوند. صرف نوشتن این فیلدها در JSON کافی نیست؛ کد راه‌اندازی باید آن‌ها را بخواند و در ساخت provider به کار ببرد.

کش یک کپی موقت از نتیجهٔ خواندن است؛ قفل اجازهٔ انجام هم‌زمان یک عملیات را کنترل می‌کند. دو process می‌توانند از کش مشترک بخوانند، اما وقتی یکی قفل یک نمونهٔ ورکفلو را گرفته است، دیگری باید منتظر آزادشدن آن بماند. قفل بعد از آزادشدن حذف می‌شود و ممکن است هنگام مشاهدهٔ Redis هیچ کلید قفلی نبینید. مدت یک‌روزهٔ کش، مدت قفل نیست.

این نمونه برای لوکال است. در اجرای چندسروری، همهٔ نودهای مرتبط باید به Redis مشترک با دیتابیس و پیشوند قفل یکسان وصل شوند؛ localhost روی هر سرور به خود همان سرور اشاره می‌کند. بعد از تغییر provider، برنامه را دوباره اجرا کنید.

۸.۵. تنظیم یکسان WorkflowFramework و NextBankParsian

در Framework، کش و قفل از اتصال Redis برنامهٔ میزبان استفاده می‌کنند. helper قفل تنظیمات قدیمی Runtime:DistributedLocking و سپس مسیر جدید Workflow:Runtime:DistributedLocking را می‌خواند. مقادیر جدید اولویت دارند. دیتابیس، پیشوند و زمان انتظار واقعاً در ساخت provider و Options مصرف می‌شوند.

RegisterCache اتصال singleton میزبان را ثبت می‌کند و پیش از AddWorkflowFramework فراخوانی می‌شود. کش Elsa با انتخاب provider Redis از همان اتصال استفاده می‌کند؛ provider قفل نیز همان سرویس را دریافت می‌کند.

محیط منبع فایل در ساختار فعلی تنظیم مورد نظر
Production appsettings.json پایه سه Sentinel با ServiceName=Redis1 و پورت 26379
Development فایل پایه به‌علاوهٔ appsettings.development.json اتصال مستقیم لوکال؛ باید اثر endpointهای اضافهٔ پایه برداشته شود

فایل‌های پایه: Web، Admin و Workers. فایل Production جداگانه‌ای در ساختار فعلی وجود ندارد. همهٔ میزبان‌ها از دیتابیس Redis 1، پیشوند کش nextbank:workflow:cache: و پیشوند قفل nextbank:workflow:locks: استفاده می‌کنند.

ترکیب فایل‌ها و آرایهٔ Hosts را درست بفهمیم

فایل appsettings.json پایه ابتدا خوانده می‌شود. اگر فایل مربوط به محیط وجود داشته باشد، مقادیر آن روی فایل پایه اعمال می‌شوند. در ساختار فعلی NextBank، فایل جداگانهٔ Production وجود ندارد؛ محیط Production تنظیمات Sentinel را از فایل پایه دریافت می‌کند. در Development، فایل appsettings.development.json روی پایه اعمال می‌شود.

تنظیمات بر اساس کلید ترکیب می‌شوند. آرایهٔ Hosts نیز کلیدهای شماره‌دار مانند Redis:Hosts:0:Host دارد. اگر فایل پایه سه endpoint و فایل Development یک endpoint داشته باشد، فقط شمارهٔ صفر جایگزین می‌شود؛ شماره‌های یک و دو از پایه باقی می‌مانند. نوشتن یک آرایهٔ کوتاه‌تر به معنی پاک‌شدن آرایهٔ قبلی نیست.

وقتی قصد دارید در لوکال فقط یک Redis فعال باشد، باید اثر endpointهای اضافی پایه را هم بردارید. در helper اتصال فعلی NextBank، Host خالی کنار گذاشته می‌شود، بنابراین این نمونهٔ override قابل استفاده است:

{
  "Redis": {
    "ServiceName": "",
    "Hosts": [
      { "Host": "localhost", "Port": 6379 },
      { "Host": "", "Port": 26379 },
      { "Host": "", "Port": 26379 }
    ]
  }
}

این نمونه روش تنظیم لوکال روی یک فایل پایهٔ دارای سه Sentinel را آموزش می‌دهد. عدد پورتِ ردیف دارای Host خالی مصرف نمی‌شود. در فایل‌های Development فعلی فقط ردیف صفر تعیین شده است؛ پس ترکیب فعلی فایل‌ها هنوز دو endpoint دیگر پایه را نگه می‌دارد. برای تشخیص تنظیم واقعی، مقادیر نهاییِ ترکیب‌شده را بررسی کنید، نه فقط یک فایل را.

متغیرهای محیطی، User Secrets و خط فرمان نیز می‌توانند مقادیر فایل‌ها را تغییر دهند. نام محیط و بزرگی و کوچکی حروف نام فایل، به‌خصوص روی Linux، باید با روش بارگذاری میزبان سازگار باشد. روی سرور باید فایل پایهٔ دارای تنظیمات Sentinel همراه خروجی باشد و override لوکال در محیط Production اعمال نشود.

در اجرای چندنودی، همهٔ میزبان‌های مرتبط باید به همان دیتابیس Workflow در SQL و Redis مشترک با فضای کلید هماهنگ وصل شوند. دیتابیس‌های Workflow مستقل نباید صرفاً به دلیل داشتن Revision یکسان، فضای کش مشترک داشته باشند. پس از تغییر provider یا پکیج، برنامه‌ها باید دوباره راه‌اندازی شوند.

فایل‌های Development فعلی NextBank در Git نادیده گرفته می‌شوند؛ روی ماشین دیگر، override لوکال باید فراهم شود. نسخهٔ Framework در سورس و مصرف NextBank اکنون 1.1.0-preview.28 است.

گزارش آزمون قبلیِ پکیج preview.27، اشتراک payload و رفتار قفل بین سه ServiceProvider با اتصال مستقل را تأیید کرد. بررسی فعلی فقط فایل‌ها، ترکیب تنظیمات و مستند را بازبینی کرده است؛ آزمون اجرایی نسخهٔ جدید، اتصال Sentinel و failover سروری انجام نشده‌اند.

۹. تنظیم مستقیم در کد و ترکیب آن با فایل

۹.۱. در مصرف WorkflowFramework

using Caspian.WorkflowFramework;
using Elsa.Extensions;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;

builder.AddWorkflowFramework(sqlConnectionString, cache =>
{
    cache.UseRedis(redis =>
    {
        redis.ConnectionFactory =
            sp => sp.GetRequiredService<IConnectionMultiplexer>();
        redis.Database = 1;
        redis.KeyPrefix = "nextbank:workflow:cache:";
    });
});

متد دوپارامتری قبلی نیز باقی مانده است:

builder.AddWorkflowFramework(sqlConnectionString);

این متد تنظیمات را از فایل میزبان می‌خواند. متد جدید یک callback اضافه دارد؛ callback همان قطعه‌کدی است که اجازه می‌دهد بعضی تنظیمات را صریحاً تغییر دهید.

۹.۲. در مصرف مستقیم Core

management.UseCache(cache =>
{
    cache.UseRedis(redis =>
    {
        redis.ConnectionFactory =
            sp => sp.GetRequiredService<IConnectionMultiplexer>();
        redis.Database = 1;
        redis.KeyPrefix = "nextbank:workflow:cache:";
    });
});

runtime.UseCache();

Runtime از انتخاب مشترک همان ماژول استفاده می‌کند. برای حالت Memory:

management.UseCache(cache => cache.UseMemory());
runtime.UseCache();

در حالت Redis باید Store پایدار اعتبار نیز فراهم باشد. Feature مدیریت EF Core این Store را برای provider مشترک متصل می‌کند. انتخاب Redis روی یک ماژول بدون Store پایدار مناسب، خطای واضح می‌دهد.

۹.۳. فایل و کد با هم

برای مصرف مستقیم Core، provider قابل انتخاب از فایل را یک‌بار ثبت می‌کنیم:

elsa.UseCaching(cache => cache.RegisterRedis());

management.UseCache(
    configuration.GetSection("Workflow:Cache"),
    cache => cache.UseRedis(redis =>
    {
        redis.KeyPrefix = "nextbank:workflow:cache:";
    }));

runtime.UseCache();

این قطعه فقط بخش تنظیم کش است؛ ثبت persistence و سایر قابلیت‌های برنامه نیز باید مثل قبل انجام شود.

در WorkflowFramework، RegisterRedis و خواندن فایل، داخل خود پکیج انجام می‌شود؛ مصرف‌کننده لازم نیست این دو مرحله را دوباره بنویسد.

ترتیب معمول اعمال تنظیمات:

پیش‌فرض‌ها → فایل تنظیمات → مقادیر صریح callback

مثلاً فایل Database را ۱ و Prefix را configured: تعیین کرده است. اگر callback فقط Prefix را تغییر دهد، Database همچنان ۱ می‌ماند.

provider انتخاب‌شده از فایل نیز می‌تواند با انتخاب صریح callback تغییر کند؛ مثلاً فایل Memory باشد و callback Redis را انتخاب کند. اما دو انتخاب صریح متفاوت در کدِ یک ماژول، خطا می‌دهند تا نتیجه به ترتیب ثبت بخش‌ها وابسته نباشد.

تنظیم مشترک را یک‌بار، پیش از callback تنظیم کنید. خواندن مجدد همان فایل در هر مصرف‌کننده، ترتیب روشن «فایل سپس کد» را مبهم می‌کند.

۹.۴. مالک اتصال Redis چه کسی است؟

سه حالت وجود دارد:

  1. اتصال ثبت‌شدهٔ میزبان: پیش‌فرض provider آن را دریافت می‌کند. پکیج کش این اتصال را Dispose نمی‌کند؛ مالک آن میزبان است.
  2. ConnectionFactory صریح: اتصال توسط تابعی که شما تعیین کرده‌اید دریافت می‌شود. این انتخاب بر ConnectionString تنظیم‌شده تقدم دارد.
  3. ConnectionString اختصاصی کش: اگر factory صریحی نداریم، provider اتصال خودش را ایجاد می‌کند و مسئول بستن آن است.

ConnectionString اختصاصی کش، اتصال عمومی مورد نیاز قفل Runtime را خودکار ثبت نمی‌کند. قفل Redis در WorkflowFramework از IConnectionMultiplexer میزبان استفاده می‌کند.

۱۰. یک درخواست عادی چگونه از کش می‌خواند؟

فرض کنید درخواست می‌خواهد تعریف «تأیید پرداخت» را بخواند. مسیر اصلی مدیر جدید کش چنین است:

  1. اگر کش خاموش باشد یا درخواست داخل ambient transaction باشد، factory مستقیماً اجرا می‌شود.
  2. در حالت عادی، وضعیت اعتبار خوانده می‌شود؛ برای Redis از جدول SQL و برای Memory از وضعیت محلی.
  3. اگر تغییر ناتمامِ مرتبط وجود داشته باشد، از payload کش استفاده نمی‌شود.
  4. کلید ورودی با نسخهٔ اعتبار فعلی ساخته می‌شود.
  5. provider برای همان کلید جست‌وجو می‌شود.
  6. اگر داده موجود باشد، نتیجه بازگردانده می‌شود.
  7. اگر موجود نباشد، factory اجرا می‌شود؛ در مصرف‌کنندهٔ تعریف‌ها، این تابع Store اصلی را می‌خواند.
  8. پس از اجرای factory، اعتبار دوباره بررسی می‌شود.
  9. فقط اگر اعتبار عوض نشده و تغییر ناتمام نداریم، نتیجه در کش نوشته می‌شود.
flowchart TD
    Start["درخواست خواندن داده"] --> Eligible{"کش قابل مصرف است؟"}
    Eligible -- "خیر" --> Store["اجرای factory / خواندن Store اصلی"]
    Eligible -- "بله" --> State["خواندن نسخهٔ اعتبار"]
    State --> Pending{"تغییر ناتمام وجود دارد؟"}
    Pending -- "بله" --> Store
    Pending -- "خیر" --> Lookup["جست‌وجو با کلید نسخهٔ فعلی"]
    Lookup --> Hit{"داده پیدا شد؟"}
    Hit -- "بله" --> Result["بازگرداندن نتیجه"]
    Hit -- "خیر" --> Factory["خواندن داده از Store"]
    Factory --> Recheck["بررسی دوبارهٔ اعتبار"]
    Recheck --> Valid{"همان نسخه و بدون تغییر ناتمام؟"}
    Valid -- "بله" --> Save["نوشتن نتیجه در provider"]
    Valid -- "خیر" --> Result
    Save --> Result
    Store --> Result

دلیل بررسی دوم: ممکن است درست هنگام خواندن از SQL، node دیگری تعریف را تغییر دهد. نتیجهٔ خواندن اولیه نباید به‌عنوان دادهٔ معتبرِ پس از آن تغییر در کش ذخیره شود.

یک درخواست که قبل از تغییر شروع شده ممکن است نتیجهٔ نسخهٔ قبلی را تمام کند. هدف این کنترل، جلوگیری از معتبرشدن دوبارهٔ آن نتیجه برای درخواست‌های بعدی است؛ این کد درخواست در حال اجرا را خودکار به نسخهٔ جدید مهاجرت نمی‌دهد.

۱۱. کلید Redis دقیقاً چه شکلی دارد؟

پیشوند مورد توافق:

nextbank:workflow:cache:

کلید واقعی تقریباً این ساختار را دارد:

nextbank:workflow:cache:v1:153:<SHA256-HEX>
  • nextbank:workflow:cache: پیشوند تنظیم‌شده است.
  • v1 نسخهٔ قالب داخلی کلید است.
  • 153 در این مثال نسخهٔ اعتبار کش است؛ عدد واقعی از ثبت‌های SQL به دست می‌آید.
  • بخش آخر hash نوع کامل داده و کلید منطقی است و در کد به‌شکل ۶۴ کاراکتر hexadecimal ساخته می‌شود.

در مصرف‌کننده‌های فعلی، کلید منطقی اطلاعات لازم مثل Tenant، نوع نتیجه و ورودی جست‌وجو را دارد. برای بعضی عملیات، نام عملیات، صفحه‌بندی و ترتیب نیز در ساخت hash لحاظ می‌شود.

این تفاوت‌ها مهم‌اند؛ نتیجهٔ صفحهٔ اول نباید به‌جای صفحهٔ دوم برگردد و Page<WorkflowDefinition> نباید با Page<StoredTrigger> یکی فرض شود. به همین دلیل نوع کامل داده به‌جای نام کوتاه آن استفاده شده و ورودی صفحه‌بندی و ترتیب Triggerها اصلاح شده است.

کلیدها در Redis الزاماً شامل کلمات خوانایی مثل definitions و triggers نیستند؛ نمونه‌های دسته‌بندیِ اولیه در گفتگو طرح مفهومی بودند. پیاده‌سازی نهایی از کلید نسخه‌دار و hash استفاده می‌کند.

نسخهٔ اعتبار کش با نسخهٔ خود ورکفلو فرق دارد. «ورکفلو نسخهٔ ۲» می‌تواند در «کش با اعتبار ۱۵۳» قرار بگیرد. نسخهٔ اول مربوط به دستورالعمل کسب‌وکار است؛ نسخهٔ دوم مربوط به معتبر بودن کپی موقت است.

۱۲. چرا جدول جدیدی در SQL اضافه شد؟

ممکن است بپرسید: «قرار بود کش در Redis باشد؛ چرا باز هم SQL می‌خوانیم؟»

Redis محل payload است. اما باید بدانیم این payload هنوز معتبر است یا نه. اگر اعتبار فقط در Redis یا در یک پیام لحظه‌ای نگه‌داری شود، قطع اتصال، از دست رفتن پیام یا تغییر Redis اصلی می‌تواند اطلاع یک node از تغییر را ناقص کند.

SQL همین حالا محل دادهٔ اصلی است. ثبت پایدار تغییرات در همان سامانهٔ ذخیره‌سازی به nodeها کمک می‌کند مستقل از دریافت پیام لحظه‌ای و مستقل از وضعیت payload Redis، اعتبار را بررسی کنند.

این انتخاب یک هزینه دارد: در یک خواندن واجد شرایط کش، ابتدا یک query کوچک برای اعتبار انجام می‌شود. هنگام miss و برخی عملیات مسیرهای HTTP، بررسی‌های بیشتری هم انجام می‌شود. بنابراین ادعای «همهٔ queryهای SQL حذف شدند» درست نیست. هدف، کاهش خواندن و تبدیل مکرر دادهٔ اصلی همراه با کنترل اعتبار است.

اندازه‌گیری سرعت و تعداد queryها در بار واقعی هنوز انجام نشده است. باید بررسی شود این هزینه در ورکفلوهای واقعی شما چه اثری دارد.

جدول CacheMutations چه اطلاعاتی دارد؟

ستون معنی
Id شمارهٔ ثبت SQL که در محاسبهٔ نسخهٔ اعتبار استفاده می‌شود
MutationId شناسهٔ یکتای تغییر، برای پیدا کردن و تکمیل ثبت
Scope دستهٔ تغییر؛ workflow یا routes
OwnerInstance نام ماشین، شناسهٔ process و شناسهٔ اجرای آن
IsCompleted آیا تغییر مربوط به این ثبت تمام شده است؟
CreatedAt زمان ساخت ثبت، به‌صورت UTC

دو index هم ایجاد شده‌اند: یکی برای یکتایی و جست‌وجوی MutationId و دیگری روی IsCompleted.

چرا دو Scope داریم؟

یک Bookmark ممکن است در جریان اجرای روزمره ایجاد یا حذف شود. این تغییر می‌تواند بر مسیرهای HTTP اثر بگذارد، ولی لزوماً تعریف ورکفلو را تغییر نداده است.

  • Scope برابر workflow برای تغییر تعریف‌ها، Triggerها و اعتبار عمومی دادهٔ کش است.
  • Scope برابر routes برای تغییر Bookmarkهایی است که هماهنگی مسیرهای محلی را لازم می‌کنند.

نسخهٔ دادهٔ کش از ثبت‌های workflow به دست می‌آید. نسخهٔ مسیرها تغییرهای هر دو دسته را در نظر می‌گیرد. به همین دلیل، تغییر Bookmark، دادهٔ تعریف‌ها را بی‌دلیل باطل نمی‌کند ولی مسیرهای nodeها می‌توانند تازه شوند.

ثبت‌ها تا همیشه زیاد می‌شوند؟

در تکمیل عادی، ثبت‌های کامل‌شدهٔ قدیمی پاک‌سازی می‌شوند. آخرین نسخهٔ مشاهده‌شدهٔ هر Scope و تمام ثبت‌های ناتمام حفظ می‌شوند.

پاک‌سازی فقط ثبت‌های کامل‌شده‌ای را حذف می‌کند که از حداکثر مشاهده‌شدهٔ همان Scope قدیمی‌تر باشند. ثبت تازه‌ای که node دیگر هم‌زمان ساخته است نباید حذف شود.

بنابراین در وضعیت عادی جدول کوچک می‌ماند. باقی‌ماندن تعداد زیادی ثبت ناتمام، نیازمند بررسی عملیاتی است و با پاک‌سازی معمول حذف نمی‌شود.

۱۳. تغییر تعریف از شروع تا پایان چه مسیری دارد؟

برای فهم مسیر، این سناریو را در نظر بگیرید: تعریف فعلی یک مرحلهٔ تأیید دارد و قرار است دو مرحله داشته باشد.

  1. پیش از تغییر داده، یک ثبت ناتمام در SQL ایجاد می‌شود.
  2. از این نقطه، مدیر کش می‌داند تغییر مربوط هنوز تمام نشده است.
  3. عملیات ذخیرهٔ تعریف در Store اصلی انجام می‌شود.
  4. پس از پایان موفق عملیات، ثبت تغییر تکمیل می‌شود.
  5. همراه تکمیل، یک ثبت کامل‌شدهٔ تازه ساخته می‌شود تا نسخهٔ اعتبار جلو برود.
  6. تکمیل ثبت قبلی و ساخت نسخهٔ تازه در یک SaveChanges و تراکنش SQL انجام می‌شوند.
  7. nodeهای بعدی با نسخهٔ تازهٔ اعتبار کلید می‌سازند و payload قبلی را مصرف نمی‌کنند.

فراخوانی‌های MutateAsync این ترتیب را دور عملیات Store قرار می‌دهند. دادهٔ اصلی همچنان توسط Store اصلی ذخیره می‌شود؛ لایهٔ کش دور آن سازوکار هماهنگی اضافه می‌کند.

چرا فقط «حذف کلید Redis پس از ذخیره» کافی نیست؟

فرض کنید یک خواننده هنوز مشغول خواندن تعریف قدیمی از SQL است. node دیگر تعریف را ذخیره و کلید Redis را حذف می‌کند. اگر خوانندهٔ اول کمی بعد نتیجهٔ قدیمی را با همان کلید بنویسد، دوباره دادهٔ قدیمی در کش قرار می‌گیرد.

نسخه‌دارکردن کلید و بررسی دوبارهٔ اعتبار پس از factory، این بازگشت را کنترل می‌کند. نوشتن دیرهنگام یک نتیجهٔ قدیمی نباید آن را به نسخهٔ اعتبار جدید متصل کند.

چرا آغاز و پایان تغییر هر دو اهمیت دارند؟

شماره‌های SQL لزوماً ترتیب پایان کار nodeها را نشان نمی‌دهند. دو node می‌توانند شمارهٔ ثبت بگیرند و با ترتیب متفاوتی عملیاتشان را تمام کنند.

اگر تکمیل یک تغییر قدیمی صرفاً IsCompleted را true کند، حداکثر شماره ممکن است جلو نرود؛ در نتیجه نسخهٔ قبلی کش دوباره قابل مصرف شود. ثبت تازهٔ پس از پایان داده‌نویسی، این مشکل را برطرف می‌کند.

این رفتار با تستی بررسی شده که ثبت با شناسهٔ کوچک‌تر، بعد از کش‌شدن ثبت بزرگ‌تر ظاهر و تکمیل می‌شود. تست دیگر، پاک‌سازی node اول را متوقف می‌کند، node دوم را جلو می‌برد و بررسی می‌کند نسخهٔ تازهٔ node دوم حذف نشده باشد.

۱۴. سه مثال زمانی برای فهم هم‌زمانی

مثال اول: یک node تعریف را تغییر می‌دهد

زمان Node A Node B
۱ تعریف قدیمی در کش موجود است همان کش مشترک را می‌خواند
۲ ثبت تغییر ناتمام را می‌نویسد وجود تغییر ناتمام را تشخیص می‌دهد
۳ تعریف اصلی را در SQL تغییر می‌دهد payload کش را کنار می‌گذارد و Store را می‌خواند
۴ تغییر را کامل و نسخهٔ تازه را ثبت می‌کند خواندن بعدی با نسخهٔ تازه انجام می‌شود
۵ تعریف جدید در Redis قرار می‌گیرد همان تعریف جدید را از Redis می‌گیرد

وجود تغییر ناتمام باعث bypass کش می‌شود؛ یعنی عبور از کپی موقت و مراجعه به Store اصلی. این کار درخواست را به انتظار نامحدود برای پیام Redis نمی‌گذارد.

مثال دوم: خواننده دیرتر نتیجهٔ قبلی را آماده می‌کند

زمان خوانندهٔ اول نویسنده
۱ نسخهٔ اعتبار ۱۰ را می‌گیرد و SQL را می‌خواند —
۲ نتیجهٔ قبلی را آماده می‌کند تغییر را ثبت و انجام می‌دهد
۳ هنوز می‌خواهد نتیجه را کش کند نسخهٔ اعتبار جدید را منتشر می‌کند
۴ بررسی دوم نشان می‌دهد اعتبار تغییر کرده است —
۵ نتیجهٔ قبلی را در کش معتبر جدید نمی‌نویسد درخواست بعدی دادهٔ جدید می‌گیرد

خوانندهٔ اول ممکن است همان نتیجه‌ای را که هنگام شروع گرفته است تمام کند. تضمین این مسیر، جلوگیری از زنده‌شدن آن نتیجه در کشِ درخواست‌های بعدی است.

مثال سوم: process وسط تغییر خاموش می‌شود

مرحله نتیجه
ثبت ناتمام در SQL نوشته شده است وضعیت تغییر پایدار است
process پیش از تکمیل خاموش می‌شود ثبت ناتمام باقی می‌ماند
node دیگری یا process تازه بالا می‌آید همان ثبت را در SQL می‌بیند
درخواست دادهٔ کش می‌آید payload قبلی مصرف نمی‌شود؛ Store خوانده می‌شود
وضعیت تغییر بررسی و درست تکمیل می‌شود کش دوباره قابل مصرف می‌شود

این طراحی اولویت را به معتبر بودن داده می‌دهد. هزینهٔ آن این است که ثبت ناتمامِ باقی‌مانده می‌تواند استفاده از کش آن دسته را متوقف کند تا بررسی و رفع شود.

این سازوکار به‌تنهایی تمام مراحل یک Publish چندعملیاتی را به یک تراکنش SQL واحد تبدیل نمی‌کند. همچنین مراجعهٔ مستقیم به SQL در حین تغییر، همان وضعیت قابل مشاهدهٔ SQL در آن لحظه را می‌خواند. هماهنگی کش با اتمیک‌کردن کل عملیات کسب‌وکار دو مسئلهٔ متفاوت‌اند.

۱۵. تراکنش چیست و چرا روی کش اثر دارد؟

تراکنش را یک گروه از تغییرات در نظر بگیرید که قرار است در نهایت با هم تأیید یا برگشت داده شوند.

  • Commit: تغییرات نهایی می‌شوند.
  • Rollback: تغییرات تراکنش برگشت داده می‌شوند.
  • InDoubt: پایان تراکنش هنوز قطعی نیست.

اگر وسط یک تراکنش، داده‌ای موقت را وارد کش مشترک کنیم، node دیگری ممکن است آن را ببیند؛ حتی اگر بعداً تراکنش rollback شود. برعکس، بستن ثبت اعتبار قبل از commit واقعی نیز می‌تواند زودهنگام باشد.

در کد جدید، برای ambient transaction که از طریق Transaction.Current مشخص می‌شود:

  1. خواندن و پرکردن payload کش کنار گذاشته می‌شود.
  2. ثبت آغاز تغییر، مستقل از تراکنش موقت انجام می‌شود.
  3. تکمیل ثبت تا پایان واقعی تراکنش به تعویق می‌افتد.
  4. پس از commit یا rollback، نسخهٔ اعتبار تازه ثبت می‌شود.
  5. در وضعیت InDoubt، ثبت باز می‌ماند.

TransactionScopeOption.Suppress در مسیر ثبت اعتبار استفاده شده است؛ یعنی query و ثبت اعتبار نباید از دید موقتِ تراکنش درخواست تأثیر بگیرند. برای ساخت snapshot عمومی مسیرها نیز همین جداسازی انجام شده است.

این توضیح مربوط به ambient transaction است. هر تراکنش دستی روی هر DbContext الزاماً از Transaction.Current قابل تشخیص نیست. همچنین تست commit و rollback این بخش، رفتار هماهنگی مدیر کش را بررسی کرده است؛ اجرای تراکنش واقعی SQL Server در محیط سرورها جزو تست محلی انجام‌شده نبوده است.

CancellationToken.None در پایان تغییر چه معنایی دارد؟

CancellationToken راهی برای اعلام انصراف از عملیات است؛ مثلاً وقتی درخواست قطع شده است.

بعد از آنکه داده ذخیره شد، قطع درخواست نباید باعث شود صرفاً به همان علت، ثبت موفقِ تغییر تکمیل نشود. به همین دلیل، مرحلهٔ تکمیل و بعضی اعلان‌های پس از ذخیره با CancellationToken.None انجام می‌شوند.

این انتخاب به معنای بی‌خطا بودن پایان عملیات نیست. اگر خود Store اعتبار در پایان خطا دهد، ثبت می‌تواند ناتمام باقی بماند و نیاز به بررسی داشته باشد.

۱۶. گراف اجرایی چرا مستقیم در Redis ذخیره نمی‌شود؟

تعریف ورکفلو شبیه یک نقشه است. گراف اجرایی، ساختاری است که موتور از آن نقشه می‌سازد تا بتواند مراحل را پیدا و اجرا کند.

WorkflowGraph شامل Activityها، ارتباط نودها و lookupهایی است که حتی می‌توانند با خود شیء Activity کلید شوند. نگه‌داری یک ارجاع به این شیء در MemoryCache، با تبدیل آن به JSON و بازسازی روی node دیگر برابر نیست.

در حالت مشترک، CachingWorkflowDefinitionService مسیر کش مستقیم گراف را کنار می‌گذارد و سرویس اصلی گراف را از تعریف معتبر می‌سازد.

در حالت Memory، قابلیت کش محلی گراف باقی می‌ماند. در حالت Redis، این تغییر یک کش ثانویهٔ مستقل برای گراف اضافه نکرده است؛ گراف از دادهٔ معتبر materialize می‌شود.

Materialize یعنی تبدیل دادهٔ تعریف به ساختار قابل استفادهٔ موتور. هزینهٔ این بازسازی باید در تست بار اندازه‌گیری شود.

۱۷. کش HTTP و جدول مسیرها چه تغییری کردند؟

فرض کنید مسیر /workflows/orders یک ورکفلو را شروع می‌کند. دو اطلاعات لازم داریم:

  1. آیا این مسیر با یک endpoint ورکفلو تطابق دارد؟
  2. کدام Trigger و نسخهٔ تعریف باید برای آن انتخاب شود؟

۱۷.۱. کش جست‌وجوی HTTP

در پکیج قبلی فقط .UseHttp() فعال بود. در پکیج جدید:

.UseHttp(http => http.UseCache())

فعال شده است.

برای Redis، نتیجهٔ قابل ذخیره شامل DefinitionVersionId و فهرست Triggerهاست. هنگام مصرف نتیجه، گراف مربوط به همان نسخه در process ساخته می‌شود. کل HttpWorkflowLookupResult همراه با گراف اجرایی به Redis ارسال نمی‌شود.

۱۷.۲. جدول محلی مسیرها

RouteTable در MemoryCache همان process باقی مانده است. قبل از تطبیق درخواست در middleware ورکفلو، اعتبار آن بررسی می‌شود.

  • اگر نسخه همان نسخهٔ معتبر محلی باشد، بازسازی لازم نیست.
  • اگر نسخه تغییر کرده باشد، مسیرها از Triggerها و Bookmarkهای اصلی دوباره جمع‌آوری می‌شوند.
  • اگر تغییر ناتمام باشد، وضعیت به‌عنوان snapshot معتبرِ قطعی ثبت نمی‌شود.
  • فهرست تازه به‌صورت یک dictionary آماده جایگزین فهرست قبلی می‌شود.

چرا Replace لازم بود؟

اگر فقط مسیر تازه را اضافه کنیم، مسیر حذف‌شده ممکن است در node دیگر باقی بماند. Replace باعث می‌شود snapshot تازه، حذف‌ها را هم منعکس کند.

در پیاده‌سازی RouteTable جایگزینی به‌صورت یک‌باره انجام می‌شود. خواننده نباید جدول را در حال پاک‌شدن تک‌تک مسیرها و اضافه‌شدن تدریجی مسیرهای تازه ببیند.

یک SemaphoreSlim محلی نیز مانع می‌شود چند درخواست همان process هم‌زمان چند بار snapshot را بازسازی کنند. این semaphore قفل مشترک بین سرورها نیست؛ فقط بازسازی محلی را کنترل می‌کند.

جمع‌آوری کامل مسیرها Tenant-agnostic انجام می‌شود و اطلاعات Tenant هر مسیر حفظ می‌شود. این موضوع برای یک جدول محلیِ process که مسیرهای چند Tenant را نگه می‌دارد مهم است.

۱۸. قفل اجرا چه فرقی با کش دارد؟

کش پاسخ می‌دهد: «داده را از کجا بخوانیم؟»

قفل پاسخ می‌دهد: «آیا دو node می‌توانند هم‌زمان این عملیات مشخص را انجام دهند؟»

ممکن است payload در Redis مشترک باشد ولی قفل هنوز فایل محلی باشد. در آن صورت دو سرور ممکن است هرکدام فکر کنند قفل را در اختیار دارند.

در NextBank، provider قفل نیز صریحاً Redis تنظیم شده و فضای کلید مستقلی دارد:

nextbank:workflow:cache:  ← payload کش
nextbank:workflow:locks:  ← قفل‌ها

PrefixedDistributedLockProvider پیشوند را به نام قفل اضافه می‌کند و ساخت قفل را به provider اصلی واگذار می‌کند.

Publish نیز اگر provider قفل در سرویس‌ها وجود داشته باشد، قفلی با نام منطقی زیر می‌گیرد:

workflow-definition:<DefinitionId>

این کار انتشارهای همان تعریف را تحت قفل قرار می‌دهد. دور جریان موفق انتشار هم ثبت تغییر نگه‌داری می‌شود و ابطال سرویس تعریف‌ها به اعلان‌های پس از تغییر منتقل شده است.

اگر provider قفل در دسترس نباشد، Redis-backed بودن payload به‌تنهایی اجازهٔ اجرای نیازمند قفل را نمی‌دهد. fallback خواندن کش و امکان گرفتن قفل دو مسیر مستقل هستند.

این تغییر به‌تنهایی تضمین اجرای دقیقاً یک‌بارِ عملیات کسب‌وکار در هر نوع خرابی را ارائه نمی‌کند. رفتار قفل، قطع شبکه و failover باید در توپولوژی واقعی بررسی شود.

۱۹. عمر کش، serialization و نتیجهٔ خالی

۱۹.۱. دو نوع انقضا

انقضای ثابت یا Absolute: از زمان ساخت ورودی محاسبه می‌شود. مثلاً ورودی بعد از ده دقیقه پایان می‌یابد، حتی اگر مرتب خوانده شود.

انقضای لغزان یا Sliding: با هر خواندن موفق تمدید می‌شود. مثلاً ورودی پس از ده دقیقه عدم استفاده پایان می‌یابد.

مصرف‌کننده نوع انقضای فعلی
جست‌وجوهای Store تعریف‌ها Sliding
کش سرویس تعریف‌ها Absolute
کش Triggerها Absolute
کش جست‌وجوی HTTP Sliding

بنابراین Duration برابر یک روز، برای ورودی Sliding به معنای پایان قطعی پس از یک روز از ایجاد نیست. مراجعهٔ مداوم می‌تواند آن را نگه دارد. تغییر نسخهٔ اعتبار مستقل از این مدت عمل می‌کند.

اگر هر دو محدودیت تعیین شوند، تمدید Sliding نباید از deadline مطلق عبور کند. این رفتار با Redis واقعی آزمایش شده است.

۱۹.۲. Redis چه چیزی ذخیره می‌کند؟

Serialization یعنی تبدیل شیء برنامه به یک نمایش قابل ذخیره، مانند JSON. Deserialization یعنی ساخت دوبارهٔ شیء از آن نمایش.

provider از تنظیمات serializer موجود Elsa برای داده استفاده می‌کند تا مواردی مانند Variable و payloadهای دارای نوع مشخص درست بازسازی شوند.

ورودی Redis به‌صورت Hash شامل سه بخش ذخیره می‌شود:

بخش محتوا
payload JSON داده
slide مدت Sliding بر حسب میلی‌ثانیه
deadline مرز انقضای Absolute، در صورت وجود

خواندن و تنظیم عمر با Lua script انجام می‌شود. در اینجا «اتمیک» یعنی چند مرحلهٔ مربوط به همان ورودی Redis با هم اجرا می‌شوند. این ویژگی، SQL و Redis را به یک تراکنش مشترک تبدیل نمی‌کند.

۱۹.۳. نبودن کلید با نتیجهٔ null فرق دارد

فرض کنید برای شناسه‌ای هنوز تعریف وجود ندارد. نتیجهٔ SQL می‌تواند null باشد. این نتیجه نیز می‌تواند موقتاً کش شود تا جست‌وجوی بی‌نتیجه تکرار نشود.

CacheResult<T> دو چیز را جدا نگه می‌دارد: آیا ورودی پیدا شد و مقدار آن چیست. به این ترتیب nullِ کش‌شده با miss اشتباه نمی‌شود.

اگر بعداً تعریف ساخته شود، ثبت تغییر نسخهٔ اعتبار را جلو می‌برد. نتیجهٔ null قبلی نمی‌تواند تعریف تازه را برای node دیگر پنهان نگه دارد.

۲۰. وقتی خطا رخ دهد چه اتفاقی می‌افتد؟

وضعیت رفتار کد پیامد عملی
payload در Redis نیست factory اجرا می‌شود داده از Store اصلی گرفته می‌شود
خواندن Redis با RedisException شکست می‌خورد warning ثبت و miss برگردانده می‌شود خواندن اصلی می‌تواند ادامه یابد
نوشتن Redis با RedisException شکست می‌خورد warning ثبت می‌شود نتیجهٔ اصلی وابسته به موفقیت پرکردن کش نیست
JSON کش خراب است و JsonException رخ می‌دهد warning ثبت و داده بازسازی می‌شود ورودی خراب به‌عنوان نتیجهٔ معتبر مصرف نمی‌شود
تغییر ناتمام مرتبط وجود دارد کش bypass می‌شود مراجعه به Store تا تعیین وضعیت ادامه دارد
SQL اعتبار در دسترس نیست خطا به مصرف‌کننده منتقل می‌شود اعتبار کش قابل اثبات نیست
ConnectionFactory یا سرویس لازم تنظیم نشده خطای تنظیم واضح رخ می‌دهد این خطا با قطعی موقت Redis یکی نیست
provider ناشناخته یا دو انتخاب صریح متفاوت خطای تنظیم رخ می‌دهد انتخاب پنهانی بر اساس ترتیب سرویس‌ها نداریم
گرفتن قفل Redis شکست می‌خورد عملیات نیازمند قفل تحت قواعد قفل موفق اعلام نمی‌شود fallback کش جای قفل را نمی‌گیرد

مدیر Redis به payload Memory مستقل fallback نمی‌کند. چنین fallbackی می‌توانست دوباره nodeها را با داده‌های متفاوت روبه‌رو کند.

در تغییر ناتمام، SQL منبع خواندن است. این به معنای رفع خودکار خطای کسب‌وکار یا کامل‌کردن خودکار Publish شکست‌خورده نیست؛ فقط مصرف payload نامطمئن کنار گذاشته می‌شود.

همچنین هر Exception به‌عنوان قطعی Redis نادیده گرفته نمی‌شود. خطاهای برنامه‌نویسی، نوع دادهٔ پشتیبانی‌نشده یا تنظیم ناقص ممکن است نیازمند اصلاح مستقیم باشند.

۲۱. ثبت ناتمام را چگونه بررسی کنیم؟

query زیر فقط وضعیت را می‌خواند:

SELECT Id, MutationId, Scope, OwnerInstance, CreatedAt
FROM Elsa.CacheMutations
WHERE IsCompleted = 0
ORDER BY Id;

OwnerInstance کمک می‌کند ماشین و process مربوط به ثبت را پیدا کنیم. بخش تصادفیِ شناسه، اجرای فعلی process را از اجرای بعدی با PID مشابه متمایز می‌کند.

قبل از رفع ثبت باید مشخص شود writer واقعاً پایان یافته، وضعیت دادهٔ اصلی چیست و عملیات هنوز فعال یا InDoubt نیست. گذشت زمان به‌تنهایی اثبات نمی‌کند تغییر تمام شده است.

پس از بررسی، مسیر تکمیل باید از API مدیر کش انجام شود:

await cacheManager.CompleteMutationAsync(
    mutationId,
    CancellationToken.None);

این نمونه دستور اجرای خودکار یا توصیهٔ تکمیل همهٔ ثبت‌ها نیست. تکمیل باید برای ثبت مشخص و پس از بررسی انجام شود. اگر در ambient transaction هستید، تکمیل به پایان آن وابسته می‌شود؛ برای تعمیر عملیاتی باید این زمینه نیز روشن باشد.

UPDATE مستقیم IsCompleted یا DELETE مستقیم ثبت، می‌تواند نسخهٔ تازهٔ لازم را تولید نکند. API درست هم ثبت را می‌بندد و هم نسخهٔ جدید را منتشر می‌کند.

۲۲. چگونه provider جدید اضافه کنیم؟

فرض کنید بعداً فناوری دیگری برای payload لازم شود. مصرف‌کننده‌های تعریف و Trigger نباید بازنویسی شوند.

مسیر توسعه چنین است:

  1. ICacheProvider را پیاده کنید.
  2. مشخص کنید provider مشترک است یا محلی؛ مقدار IsShared باید درست باشد.
  3. روش تشخیص miss، cached null، نوشتن، حذف و انقضا را پیاده کنید.
  4. Feature و Options اختصاصی فناوری را بسازید.
  5. نام provider را برای تنظیم از فایل ثبت کنید.
  6. یک extension خوانا برای انتخاب مستقیم در کد ایجاد کنید.
  7. قرارداد اعتبار پایدار و تست‌های چندنودی را برای provider مشترک رعایت کنید.

نمونهٔ مفهومی ثبت انتخاب از فایل:

cache.RegisterProvider("AnotherProvider", section =>
{
    // تنظیم Options از section و ثبت Feature اختصاصی انجام می‌شود.
    cache.SelectProvider(
        "AnotherProvider",
        sp => sp.GetRequiredService<AnotherCacheProvider>());
});

AnotherCacheProvider در پروژهٔ فعلی وجود ندارد؛ این مثال فقط محل اتصال provider بعدی را نشان می‌دهد. Feature اختصاصی باید سرویس آن را هم ثبت کند.

در این طراحی، نام‌های قابل ثبت محدود به enum بستهٔ Core نیستند. ولی هر provider مشترک همچنان به Store پایدار اعتبار نیاز دارد. صرف داشتن Get و Set برای حل هم‌زمانی کافی نیست.

سازگاری با API قبلی

امضای قدیمی GetOrCreateAsync با ICacheEntry باقی مانده است. در حالت Memory، مسیر قدیمی از رفتار اصلی MemoryCache استفاده می‌کند. در مسیر جدید، مصرف‌کننده‌ها به CacheEntryOptions منتقل شده‌اند.

برای توسعهٔ جدید بهتر است قرارداد مستقل از Memory استفاده شود. ویژگی‌هایی مانند callback خروج ورودی، Priority و Size که در ICacheEntry حافظه وجود دارند، نباید بدون بررسی به‌عنوان قابلیت کامل Redis فرض شوند. adapter سازگاری، Redis را به همهٔ رفتارهای IMemoryCache تبدیل نمی‌کند.

۲۳. دقیقاً چه تست‌هایی انجام شد؟

تست‌ها فقط بررسی نکردند که کلاس Redis قابل ساختن است؛ رفتار قابل مشاهدهٔ مصرف‌کننده نیز بررسی شد.

سناریوی تست کش چیزی که ثابت می‌کند
Memory پیش‌فرض نتیجهٔ تکراری کش می‌شود و شیء محلیِ اسکریپت حفظ می‌شود
تقدم فایل و کد فقط مقدار صریح callback، مقدار متناظر فایل را تغییر می‌دهد
انتخاب Memory روی Redis فایل تنظیم صریح provider می‌تواند انتخاب فایل را تغییر دهد
تقدم factory اتصال ConnectionString، factory صریح را بازنویسی نمی‌کند
provider نامعتبر یا انتخاب صریح متعارض خطای واضح داریم
Redis بدون Store پایدار اعتبار تنظیم ناقص موفق اعلام نمی‌شود
کش خاموش factory در هر مراجعه اجرا می‌شود
خواندن قدیمی هم‌زمان با تغییر نتیجهٔ دیرهنگام وارد کش معتبر تازه نمی‌شود
Replace جدول مسیر مسیر حذف‌شده در snapshot تازه باقی نمی‌ماند
تولید migration SQL Server DDL جدول، ستون Scope و Identity درست تولید می‌شود
اشتراک داده و cached null بین دو سرویس null و miss متمایزند و تغییر، نتیجهٔ خالی قدیمی را باطل می‌کند
ثبت ناتمام پس از restart process تازه نیز payload قبلی را کنار می‌گذارد
roundtrip تعریف و Trigger Variable و payload دارای نوع مشخص قابل بازسازی‌اند
قطع اتصال client و اتصال مجدد مقدار قدیمی Redis با اعتبار جدید زنده نمی‌شود
تغییر فقط مسیرها payload تعریف بدون علت باطل نمی‌شود
مسیر واقعی Feature مدیریت ذخیره و به‌روزرسانی تعریف در یک node به خوانندهٔ node دیگر می‌رسد
ambient transaction با commit کش bypass و ثبت تا پایان واقعی نگه‌داری می‌شود
ambient transaction با rollback همان قاعده در rollback هم برقرار است
ثبت آغاز با ترتیب دیرهنگام Identity تکمیل، نسخهٔ تازهٔ لازم را منتشر می‌کند
پاک‌سازی هم‌زمان دو node node اول ثبت تازهٔ node دوم را حذف نمی‌کند
Redis با payload cache خاموش هماهنگی مشترک همچنان فعال است
Sliding همراه Absolute تمدید از deadline مطلق عبور نمی‌کند

نتیجهٔ ثبت‌شده: ۲۲ تست کش و ۹ تست موجود Runtime موفق بودند؛ مجموعاً ۳۱ تست.

تست با process مستقل چه تفاوتی دارد؟

دو ServiceProvider در یک تست مفیدند، ولی هنوز در یک process اجرا می‌شوند. برای بررسی مستقل‌بودن حافظه، یک برنامهٔ console جدا نیز نوشته شد.

برنامه در چند اجرای جداگانه این مراحل را انجام داد:

  1. process اول payload را در Redis قرار داد.
  2. process دوم همان payload را بدون اجرای factory دریافت کرد.
  3. اجرای بعدی اعتبار را تغییر داد و payload تازه نوشت.
  4. process دیگر payload به‌روز را دید.
  5. یک process ثبت ناتمام ایجاد کرد و پایان یافت.
  6. process تازه تشخیص داد باید Store را بخواند و از payload قبلی عبور کند.

این آزمایش‌ها با Redis واقعی ایزوله روی پورت محلی 16379 و SQLite مشترک برای Store اعتبار اجرا شدند. برنامهٔ Redis تست پس از پایان بررسی متوقف شد.

build چه چیزی را بررسی کرد؟

  • ماژول‌های نهایی Core برای net8.0 و net9.0 ساخته و pack شدند.
  • پکیج WorkflowFramework با وابستگی‌های جدید ساخته شد.
  • Web، Admin و Workers در NextBankParsian با پکیج جدید build شدند.
  • JSON تنظیمات جدید و سازگاری نسخهٔ dependencyهای پکیج‌ها بررسی شد.
  • syntax اسکریپت PowerShell بررسی شد.

موفق‌بودن build یعنی کد و ارجاع‌ها قابل کامپایل‌اند. به معنای اجرای برنامه روی SQL Server واقعی، اعمال migration یا اندازه‌گیری بار production نیست.

کدام بررسی‌ها هنوز در محیط استقرار لازم‌اند؟

اجرای migration روی SQL Server واقعی، Sentinel و failover واقعی، قطع شبکهٔ سرورها، رفتار provider قفل در آن شرایط و تست بار هنوز روی توپولوژی شما اجرا نشده‌اند.

تست محلی Redis به‌تنهایی جای این بررسی‌ها را نمی‌گیرد. همچنین تست SQL Server در این نوبت، تولید و بررسی script migration بوده است؛ Store اعتبار در تست‌های اجرایی از SQLite استفاده کرده است.

۲۴. فهرست تغییر فایل‌ها و دلیل هر تغییر

این بخش نقشهٔ مراجعه به کد است. لینک‌ها به فایل‌های واقعی ریپوهای هم‌جوار اشاره می‌کنند. اگر فقط همین فایل را جداگانه کپی کنید، برای استفاده از لینک‌های سورس باید ساختار ریپوها نیز در دسترس باشد.

۲۴.۱. تنظیمات ریشهٔ Core

فایل تغییر و دلیل
Directory.Packages.props نسخهٔ مرکزی Configuration Binder اضافه شد تا provider بتواند Options را از فایل bind کند
Elsa.sln ماژول Redis و پروژه‌های تست به Solution اضافه شدند

۲۴.۲. هستهٔ کش

مسیر پایهٔ این بخش: Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching.

فایل تغییر و دلیل
ICacheManager.cs قرارداد انتخاب رفتار مشترک، وضعیت اعتبار، ثبت تغییر و API مستقل از Memory اضافه شد؛ API قبلی حفظ شد
ICacheProvider.cs قرارداد storage و تشخیص miss از cached null ایجاد شد
ICacheConsistencyStore.cs قرارداد نسخهٔ اعتبار و تغییر ناتمام، مستقل از payload ایجاد شد
CacheEntryOptions.cs تنظیم Absolute، Sliding و Tags به قرارداد مشترک منتقل شد
CacheFeature.cs انتخاب provider، registry، خواندن فایل، validation و ثبت مدیر مشترک اضافه شد
CacheConsumerFeature.cs مصرف‌کننده‌ها به تنظیم مشترک همان ماژول متصل شدند
MemoryCacheFeature.cs ثبت حافظهٔ محلی از انتخاب مدیر جدید جدا شد؛ fallback سازگار با TryAdd حفظ شد
ProviderCacheManager.cs سیاست خواندن، بررسی دوباره، کلید نسخه‌دار، رفتار تراکنش و سازگاری API قبلی متمرکز شد
MemoryCacheProvider.cs Memory پشت قرارداد provider قرار گرفت و Tags به expiration token متصل شدند
MemoryCacheConsistencyStore.cs اعتبار و وضعیت ناتمام محلی برای حالت Memory ایجاد شد
Extensions/CacheFeatureExtensions.cs ورودی مشترک UseCaching برای تنظیم ماژول اضافه شد
CacheMutationExtensions.cs ترتیب ثبت آغاز، اجرای عملیات و تکمیل آن در helper مشترک قرار گرفت
Elsa.Caching.csproj dependencyهای لازم و نسخهٔ پکیج به‌روزرسانی شدند

کلاس قبلی CacheManager همچنان برای مسیر سازگاری Memory وجود دارد. انتخاب provider جدید از CacheFeature انجام می‌شود و لازم نیست نیازهای محلی C# و JavaScript حذف شوند.

۲۴.۳. ماژول Redis

مسیر پایه: src/modules/Elsa.Caching.Redis؛ این ماژول جدید است.

فایل تغییر و دلیل
Elsa.Caching.Redis.csproj Redis در پکیج مستقل قرار گرفت
RedisCacheOptions.cs تنظیم اتصال، دیتابیس و Prefix تعریف شد؛ factory صریح تشخیص داده می‌شود
RedisCacheFeature.cs سرویس Redis و مالکیت اتصال اختصاصی ثبت و تنظیمات بررسی می‌شوند
Extensions/CacheFeatureExtensions.cs UseRedis برای Feature مشترک و مصرف‌کننده‌ها و RegisterRedis برای فایل ایجاد شد
RedisCacheProvider.cs storage واقعی، serialization، scriptهای عمر کش و رفتار RedisException پیاده شد
FodyWeavers.xml تنظیم ساخت ماژول با الگوی ابزارهای موجود پروژه هماهنگ شد

۲۴.۴. مدیریت تعریف‌ها

مسیر پایه: src/modules/Elsa.Workflows.Management.

فایل تغییر و دلیل
Extensions/ModuleExtensions.cs overload تنظیم از IConfigurationSection همراه callback اضافه شد
CachingWorkflowDefinitionsFeature.cs وابستگی به Feature مشترک کش و تنظیم قابل انتخاب اضافه شد
CachingWorkflowDefinitionStore.cs API مستقل از Memory، کلیدهای دقیق‌تر و ثبت تغییر دور Save/Delete اضافه شد
CachingWorkflowDefinitionService.cs گراف در provider مشترک مستقیماً کش نمی‌شود؛ دادهٔ تعریف با options و tag مشترک خوانده می‌شود
WorkflowDefinitionCacheManager.cs Tenant به کلیدهای سرویس تعریف افزوده شد
EvictWorkflowDefinitionServiceCache.cs ابطال به اعلان‌های پس از Publish، Retract، Delete و Update منتقل شد
WorkflowDefinitionPublisher.cs قفلِ در دسترس برای همان تعریف، ثبت دور Publish و تکمیل پس از انتشار موفق اضافه شد
Elsa.Workflows.Management.csproj نسخهٔ پکیج تغییر کرد

۲۴.۵. Runtime و Bookmark

مسیر پایه: src/modules/Elsa.Workflows.Runtime.

فایل تغییر و دلیل
Extensions/ModuleExtensions.cs overload تنظیم از فایل همراه callback اضافه شد
CachingWorkflowRuntimeFeature.cs تنظیم مشترک کش و decorator هماهنگی Bookmark متصل شد
CachingTriggerStore.cs تغییرها fenced شدند؛ ابطال پس از عملیات انجام می‌شود؛ صفحه‌بندی و ترتیب در کلید اصلاح شد
BookmarkConsistencyStore.cs Bookmark در Store اصلی می‌ماند ولی تغییرش نسخهٔ مسیرها را جلو می‌برد
Elsa.Workflows.Runtime.csproj نسخهٔ پکیج تغییر کرد

۲۴.۶. EF Core و migration

فایل تغییر و دلیل
CacheMutation.cs مدل ثبت پایدار و شناسهٔ owner ایجاد شد
Management/DbContext.cs DbSet، mapping، طول ستون‌ها و indexهای مدل اضافه شد
EFCoreCacheConsistencyStore.cs خواندن اعتبار از SQL، آغاز مستقل، تکمیل اتمیک و پاک‌سازی امن پیاده شد
WorkflowDefinitionPersistenceFeature.cs برای provider مشترک، Store اعتبار EF و برای provider محلی، Store محلی انتخاب شد
Elsa.EntityFrameworkCore.csproj نسخهٔ پکیج تغییر کرد
20261007000000_AddCacheMutationFences.cs migration جدول SQL Server و indexها اضافه شد
ManagementElsaDbContextModelSnapshot.cs snapshot مدل با جدول جدید هماهنگ شد تا migration بعدی آن را دوباره اضافه نکند
Elsa.EntityFrameworkCore.SqlServer.csproj نسخهٔ پکیج دارای migration تغییر کرد

۲۴.۷. HTTP

مسیر پایه: src/modules/Elsa.Http.

فایل تغییر و دلیل
IRouteTable.cs قرارداد Replace snapshot اضافه شد
RouteTable.cs dictionary آماده به‌صورت یک‌باره جایگزین می‌شود
DefaultRouteTableUpdater.cs جمع‌آوری کامل مسیرها، حذف مسیرهای قدیمی با Replace و خواندن Tenant-agnostic اضافه شد
RouteTableRevisionRefresher.cs نسخهٔ محلی، gate بازسازی و بررسی اعتبار SQL اضافه شد
HttpWorkflowsMiddleware.cs پیش از تطبیق مسیر، تازه‌بودن جدول بررسی می‌شود
CachingHttpWorkflowLookupService.cs DTO قابل ذخیره جای گراف مشترک را گرفت و هنگام مصرف گراف بازسازی می‌شود
HttpFeature.cs سرویس refresher و وضعیت محلی مشترک در همان process ثبت شد
HttpCacheFeature.cs تنظیمات کش HTTP به Feature مصرف‌کنندهٔ مشترک وصل شد
Extensions/ModuleExtensions.cs overload تنظیم از فایل همراه callback اضافه شد
Elsa.Http.csproj نسخهٔ پکیج تغییر کرد

۲۴.۸. تست‌ها و اسکریپت

فایل دلیل ایجاد
Elsa.Caching.UnitTests.csproj پروژهٔ تست رفتار cache و providerها
CacheTestHost.cs ساخت hostهای مستقل، Redis ایزوله و Store اعتبار SQLite
CacheProviderTests.cs تست‌های تنظیم، serialization، تراکنش، restart و هم‌زمانی
Elsa.Caching.ProcessTests.csproj برنامهٔ اجرایی برای process مستقل
ProcessTests/Program.cs حالت‌های نوشتن، خواندن، تغییر، ثبت ناتمام و bypass
Test-WorkflowCache.ps1 اجرای تکرارپذیر تست‌ها و چند process با فضای کلید تصادفی

۲۴.۹. WorkflowFramework

فایل تغییر و دلیل
DependencyInjection.cs callback تنظیم کش، خواندن فایل، ثبت Redis، فعال‌کردن کش HTTP و انتخاب مستقل قفل اضافه شد
PrefixedDistributedLockProvider.cs پیشوند مستقل به نام قفل اضافه می‌شود
Caspian.WorkflowFramework.Package.csproj وابستگی Redis و نسخه‌های تغییرکردهٔ Core متصل شدند؛ نسخهٔ پکیج به ۲۶ رسید
NuGet.Config feed محلی workspace برای بررسی پکیج‌های تولیدشده اضافه شد؛ منابع قبلی حفظ شدند
workflow-cache.md راهنمای کوتاه تنظیم و استقرار ایجاد و متن آن با نتیجهٔ نهایی هماهنگ شد
همین فایل آموزش مفاهیم، رفتار و دلیل تمام تغییرات

۲۴.۱۰. NextBankParsian

فایل تغییر و دلیل
Directory.Packages.props مصرف WorkflowFramework روی preview.28 تنظیم شده است
NuGet.Config feed محلی workspace برای restore نسخهٔ جدید اضافه شد
Web/appsettings.json سیاست Redis کش و قفل ورکفلو برای Web اضافه شد
Admin/appsettings.json همان تنظیم هماهنگ برای Admin اضافه شد
Workers/appsettings.json همان تنظیم هماهنگ برای Workers اضافه شد

این تغییرات تنظیم اتصال Redis اعلام‌شدهٔ روی سرورها را از این محیط اعمال یا آزمایش نکرده‌اند. بخش Workflow به سورس تنظیمات اضافه شده است؛ تنظیم نهایی محیط پابلیش باید هماهنگ بررسی شود.

۲۵. پکیج‌ها و فایل تحویلی

پکیج نسخهٔ نهایی Target framework
Caspian.Elsa.Caching 1.1.0-preview.10 net8.0 و net9.0
Caspian.Elsa.Caching.Redis 1.1.0-preview.10 net8.0 و net9.0
Caspian.Elsa.Workflows.Management 1.1.0-preview.10 net8.0 و net9.0
Caspian.Elsa.Workflows.Runtime 1.1.0-preview.10 net8.0 و net9.0
Caspian.Elsa.Http 1.1.0-preview.10 net8.0 و net9.0
Caspian.Elsa.EntityFrameworkCore 1.1.0-preview.10 net8.0 و net9.0
Caspian.Elsa.EntityFrameworkCore.SqlServer 1.1.0-preview.10 net8.0 و net9.0
Caspian.WorkflowFramework در سورس فعلی 1.1.0-preview.28 net9.0

فقط ماژول‌های تغییرکرده با نسخهٔ جدید تحویل شده‌اند. سایر وابستگی‌های تغییرنکردهٔ Core الزاماً نسخهٔ ۱۰ نشده‌اند.

آرشیو ساخته‌شدهٔ مرحلهٔ قبل در D:\Work\.workflow-packages شامل پکیج Framework نسخهٔ preview.27، پکیج‌های Core و مستندات همان مرحله است. manifest.json و workflow-cache-packages.1.1.0-preview.27.zip متعلق به آن خروجی‌اند. نسخهٔ سورس فعلی preview.28 است؛ در بازسازی این مستند پکیج یا ZIP جدیدی ساخته نشده است.

برای مصرف در ماشین دیگر یا CI، نسخهٔ دقیق پکیج و وابستگی‌های آن باید در feedهای پیکربندی‌شدهٔ NuGet در دسترس باشند. وجود یک خروجی محلی با نسخهٔ قدیمی‌تر، جای پکیج نسخهٔ فعلی را نمی‌گیرد.

ساخت پکیج، نصب از NuGet، publish برنامه و اجرای migration مراحل جدا هستند. بازبینی و بازسازی مستند به معنی انجام این مراحل روی سرور نیست.

۲۶. ترتیب بررسی پیش از استقرار چندنودی

این بخش ترتیب عملیاتیِ کار را توضیح می‌دهد؛ هیچ‌کدام از مراحل سروری از این مستند اجرا نشده‌اند.

  1. نسخه‌ها را هماهنگ کنید. همهٔ nodeها و بک‌اندهای writer باید قرارداد جدید را داشته باشند. یک writer قدیمی یا SQL مستقیم، ثبت اعتبار جدید را تولید نمی‌کند.
  2. Store اصلی را مشخص کنید. همه باید به همان دیتابیس Workflow مورد نظر در SQL متصل باشند.
  3. migration را آماده و اعمال کنید. جدول Elsa.CacheMutations باید پیش از مصرف کش مشترک وجود داشته باشد. مسیر migration در سورس EF فعال است، ولی build آن را روی SQL Server شما اجرا نمی‌کند.
  4. اتصال Redis را بررسی کنید. ServiceName باید به سرویس درست Sentinel اشاره کند؛ تمام nodeها باید Redis و Database هماهنگ داشته باشند.
  5. Prefix را هماهنگ کنید. در همین محیط همهٔ nodeها nextbank:workflow:cache: را استفاده می‌کنند. محیط مستقل نباید ناخواسته همان فضای کلید را مصرف کند.
  6. قفل را جدا بررسی کنید. تنظیم قفل باید مشترک باشد و در قطع شبکه و failover آزمایش شود.
  7. تغییر از یک node و مصرف از node دیگر را آزمایش کنید. تعریف، Trigger و مسیر HTTP مربوط را در نظر بگیرید.
  8. restart و تغییر ناتمام را بررسی کنید. node تازه باید وضعیت ثبت SQL را تشخیص دهد.
  9. بار واقعی را اندازه بگیرید. queryهای اعتبار، هزینهٔ ساخت گراف و اندازهٔ payloadها باید بررسی شوند.

برای دیتابیس‌های دیگر، Store اعتبار EF می‌تواند از schema متناظر استفاده کند، اما migration آمادهٔ این انتشار برای SQL Server است. اجرای موفق تست با EnsureCreated در SQLite به معنای فراهم‌بودن migration production برای همهٔ providerهای دیتابیس نیست.

۲۷. چند پرسش رایج

آیا Redis اکنون منبع اصلی تعریف ورکفلو است؟

خیر. دادهٔ اصلی در SQL است؛ Redis کپی موقت آن را نگه می‌دارد.

آیا به‌روزرسانی تعریف باید payload تمام nodeها را دستی پاک کند؟

در مسیر قرارداد جدید، نسخهٔ اعتبار تغییر می‌کند. درخواست بعدی کلید نسخهٔ تازه را مصرف می‌کند. کلید قبلی ممکن است تا انقضا در Redis بماند، ولی کلید معتبرِ خواندن فعلی نیست.

آیا تمام MemoryCacheهای پروژه حذف شدند؟

خیر. بعضی داده‌ها باید محلی باشند. این تغییر، انتخاب محل کش داده‌های ورکفلو را فراهم کرده است؛ تبدیل هر شیء داخل حافظه به دادهٔ مشترک هدف آن نیست.

اگر Redis قطع شد، آیا برنامه همیشه می‌تواند اجرا را ادامه دهد؟

خواندن payload می‌تواند به Store اصلی مراجعه کند. اما اگر SQL هم در دسترس نباشد یا اجرای مورد نظر به قفل Redis نیاز داشته باشد، موفقیت آن مسیر تضمین نمی‌شود. نتیجه به قابلیت‌های مورد نیاز همان عملیات بستگی دارد.

آیا node تازه به پیام‌های زمان خاموشی نیاز دارد؟

برای اعتبار payload در حالت Redis، نسخه و وضعیت ناتمام را از SQL می‌خواند. به دریافت پیام‌های لحظه‌ای زمان خاموشی متکی نیست.

اگر تغییر در SQL دستی انجام شود چه؟

این مسیر از decorator و ثبت تغییر عبور نمی‌کند. بنابراین هماهنگی این قرارداد را تولید نمی‌کند. writerها باید از مسیر جدید استفاده کنند یا سازوکار اعتبار متناسبی داشته باشند.

آیا Publish نسخهٔ ۲، نمونهٔ در حال اجرای نسخهٔ ۱ را تبدیل می‌کند؟

خیر. انتخاب تعریف برای خواندن‌های جدید با مهاجرت نمونهٔ در حال اجرا فرق دارد. این تغییر مهاجرت خودکار نمونه‌ها را اضافه نکرده است.

آیا این مستند می‌گوید هیچ خرابی ممکن نیست؟

خیر. مستند رفتار پیاده‌سازی و نتیجهٔ تست‌های انجام‌شده را مشخص می‌کند. سلامت شبکه، SQL، Redis، provider قفل و سایر منطق‌های برنامه نیز بر نتیجه اثر دارند. بررسی واقعی سرورها بخش جداگانهٔ استقرار است.

۲۸. ترتیب پیشنهادی خواندن کد برای یک مبتدی

اگر می‌خواهید کد را هم بفهمید، از فایل‌های پیچیدهٔ SQL یا Lua شروع نکنید. این ترتیب ساده‌تر است:

  1. ICacheProvider و CacheEntryOptions: قرارداد خواندن و نوشتن و عمر ورودی را ببینید.
  2. MemoryCacheProvider: نمونهٔ سادهٔ پیاده‌سازی قرارداد را بخوانید.
  3. CacheFeature و extensionهای UseRedis: بفهمید انتخاب provider چگونه انجام می‌شود.
  4. ProviderCacheManager: مسیر خواندن معمول و بررسی دوبارهٔ اعتبار را دنبال کنید.
  5. CachingWorkflowDefinitionStore: ببینید یک مصرف‌کنندهٔ واقعی چگونه از مدیر کش استفاده می‌کند.
  6. ICacheConsistencyStore و CacheMutation: مفهوم تغییر ناتمام و نسخه را به کد وصل کنید.
  7. EFCoreCacheConsistencyStore: ثبت مستقل و تکمیل اتمیک را بخوانید.
  8. RedisCacheProvider: serialization و عمر ورودی Redis را بررسی کنید.
  9. RouteTableRevisionRefresher: نمونهٔ هماهنگی یک شیء محلی را دنبال کنید.
  10. تست‌ها: برای هر سناریو، رفتار قبل و بعد از تغییر را با انتظار تست مقایسه کنید.

هنگام خواندن هر فایل، سه سؤال کمک می‌کند: دادهٔ اصلی کجاست، چه چیزی می‌تواند قدیمی شود، و چه رویدادی باید اعتبار آن را تغییر دهد؟

۲۹. واژه‌نامهٔ تکمیلی

واژه معنی در این مستند
Singleton یک نمونهٔ سرویس در یک ServiceProvider؛ singleton هر node با node دیگر یکی نیست
Scoped سرویس با عمر محدود به یک scope؛ معمولاً یک درخواست
Decorator لایه‌ای دور سرویس اصلی که رفتار اضافه می‌کند و کار اصلی را به آن می‌سپارد
DTO نمایش سادهٔ داده برای جابه‌جایی یا ذخیره، بدون شیء اجرایی پیچیده
Revision نسخهٔ اعتبار کپی موقت
Write fence ثبت پایدار آغاز تغییر که مصرف کش را تا تعیین پایان کنترل می‌کند
Snapshot فهرست آمادهٔ وضعیت در یک زمان، مثل مجموعهٔ مسیرها
Atomic انجام چند مرحله به‌صورت یک واحد در محدودهٔ مشخص، مثل یک تراکنش SQL یا یک script Redis
Tenant محدودهٔ مشتری یا سازمان در برنامهٔ چندمشتری
Hash نمایش محاسبه‌شدهٔ یک ورودی برای ساخت کلید مشخص
Migration تغییر ساختار دیتابیس به‌صورت نسخه‌دار
Model snapshot تصویر مدل EF برای محاسبهٔ تغییر migration بعدی
Cache hit ورودی مورد نظر در کش پیدا شده است
Cache miss ورودی مورد نظر در کش پیدا نشده است
Bypass عبور از payload کش و اجرای مسیر اصلی
Failover تغییر سرور اصلی به سرور جایگزین

۳۰. نکته‌هایی که باید از این تغییر به خاطر بمانند

Redis دادهٔ قابل اشتراک را نگه می‌دارد، SQL هم دادهٔ اصلی و هم ثبت اعتبار را نگه می‌دارد، و هر process اشیای اجرایی خودش را دارد.

انتخاب provider با محل نگه‌داری داده ارتباط دارد. نسخهٔ اعتبار مشخص می‌کند کدام کپی قابل مصرف است. قفل مشخص می‌کند کدام اجرا اجازهٔ پیش‌رفتن دارد. این سه مسئولیت با هم کار می‌کنند، اما جای یکدیگر را نمی‌گیرند.

فایل تنظیمات و کد هر دو پشتیبانی می‌شوند. توسعهٔ provider بعدی از قرارداد و Feature آغاز می‌شود. هماهنگی چندنودی نیز فقط با اتصال به Redis کامل نمی‌شود؛ مسیر ذخیره، ترتیب پایان تغییر، تراکنش و اشیای محلی وابسته باید در نظر گرفته شوند.