Files
Caspian.WorkflowFramework/docs/workflow-cache-beginner.fa.md
T

962 lines
68 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# آموزش کش ورکفلو از ابتدا: از UseCache تا Redis و اجرای چندنودی
این راهنما برای کسی نوشته شده که می‌خواهد کد و تنظیمات پروژه‌های Caspian را بفهمد، حتی اگر قبلاً با کش، Redis، Dependency Injection یا اجرای چندنودی کار نکرده باشد.
مثال‌ها بر اساس کد فعلی `Caspian.Elsa.Core`، `Caspian.WorkflowFramework` و `NextBankParsian` هستند. نسخهٔ ماژول‌های تغییرکردهٔ Core، `1.1.0-preview.10` و نسخهٔ Framework در سورس و مصرف NextBank، `1.1.0-preview.28` است. این راهنما در تاریخ `2026-10-10` با ساختار فعلی فایل‌های تنظیمات بازسازی شده است.
برای فهرست گستردهٔ فایل‌های تغییرکرده و جزئیات پیاده‌سازی، [مستند مفصل تغییرات](workflow-cache-explained.fa.md) را بخوانید. [راهنمای کوتاه تنظیمات](workflow-cache.md) هم برای مراجعهٔ سریع موجود است.
## خلاصهٔ سریع؛ اگر فقط چند دقیقه وقت دارید
| موضوع | مطلب اصلی |
|---|---|
| محل کش | `UseCaching`، provider و مدت کش را یک‌بار برای همان ماژول Elsa تنظیم می‌کند. |
| فعال‌کردن مصرف‌کننده‌ها | سه `UseCache`، کش تعریف‌ها، Triggerها و lookup HTTP ورودی را به همان provider متصل می‌کنند. |
| انتخاب Redis | `RegisterRedis` قابلیت انتخاب را ثبت می‌کند؛ `Provider=Redis` در فایل یا `UseRedis` در کد آن را انتخاب می‌کند. |
| اتصال | کش و قفل Redis از `IConnectionMultiplexer` میزبان استفاده می‌کنند. هر process اتصال خودش را به Redis مشترک دارد. |
| تنظیمات فعلی NextBank | Sentinel در `appsettings.json` پایهٔ Web، Admin و Workers است؛ تنظیمات لوکال در فایل Development قرار می‌گیرند. |
| فضای داده | دیتابیس Redis برابر `1`؛ پیشوند کش `nextbank:workflow:cache:` و پیشوند قفل `nextbank:workflow:locks:` است. |
| دادهٔ اصلی | تعریف‌ها و وضعیت اجرا در SQL می‌مانند. Redis کپی موقت دادهٔ قابل اشتراک را نگه می‌دارد. |
| اعتبار | شمارهٔ اعتبار از SQL خوانده می‌شود تا کلیدهای وضعیت قدیمی مصرف نشوند. |
| قفل | برای هماهنگی عملیات هم‌زمان است و پس از آزادشدن می‌تواند سریع از Redis حذف شود. |
| تعداد کلیدها | نتیجهٔ جست‌وجو کش می‌شود؛ تعداد کلیدها با تعداد تعریف‌های SQL برابر نیست. |
| اولویت مدت کش | پیش‌فرض Framework، سپس فایل، سپس مقدار صریح callback. مدت پیش‌فرض Framework یک روز است. |
| چندنودی | میزبان‌های مرتبط باید SQL ورکفلو، Redis، دیتابیس Redis و پیشوندهای هماهنگ داشته باشند. |
دو پرسش را جدا در نظر بگیرید: `UseCaching` تعیین می‌کند کش **کجا و چگونه** نگه‌داری شود؛ `UseCache` تعیین می‌کند **کدام بخش** از آن استفاده کند. provider قفل نیز جداگانه تنظیم می‌شود.
برای مطالعهٔ عمیق، از فصل ۱ ادامه دهید. مثال‌های فایل و کد در فصل‌های ۱۳ تا ۱۸، اعتبار و قفل در فصل‌های ۲۰ تا ۲۴ و روش اجرا و عیب‌یابی در فصل‌های ۲۶ و ۲۷ توضیح داده شده‌اند.
## ۱. ابتدا بدانیم «ورکفلو» و «اجرای ورکفلو» چه تفاوتی دارند
فرض کنید یک ورکفلو داریم که مراحلش چنین است:
```text
ثبت درخواست ← بررسی درخواست ← انتظار برای تأیید ← ادامهٔ پردازش
```
این نقشهٔ مراحل، «تعریف ورکفلو» است. وقتی درخواست اول وارد سیستم می‌شود، یک اجرای مشخص از این تعریف ساخته می‌شود. درخواست دوم می‌تواند اجرای دیگری از همان تعریف داشته باشد.
| اصطلاح | معنی ساده | مثال |
|---|---|---|
| Workflow Definition | نقشه و تنظیمات مراحل ورکفلو | تعریف «بررسی درخواست» |
| DefinitionId | شناسهٔ منطقی آن تعریف | شناسه‌ای که نسخه‌های مختلف همان تعریف را به هم مرتبط می‌کند |
| نسخهٔ تعریف | یکی از نسخه‌های ذخیره‌شدهٔ نقشه | نسخهٔ ۱ یا ۲ تعریف |
| Workflow Instance | یک اجرای مشخص از یک تعریف | اجرای مربوط به درخواست شمارهٔ ۱۰۰ |
| WorkflowInstanceId | شناسهٔ همان اجرای مشخص | شناسهٔ اجرای درخواست شمارهٔ ۱۰۰ |
| Trigger | مشخصات رویداد یا شرط شروع ورکفلو | شروع با یک درخواست HTTP مشخص |
| Bookmark | اطلاعات نقطه‌ای که اجرای ورکفلو در آن منتظر ادامه است | انتظار برای دریافت تأیید |
یک تعریف می‌تواند چند نسخه و تعداد زیادی اجرای مستقل داشته باشد. پس تعداد تعریف‌ها، نسخه‌ها، اجراها و کلیدهای کش را نباید یک عدد فرض کنیم.
## ۲. کش چیست و چه کمکی می‌کند؟
کش یک کپی موقت از نتیجهٔ خواندن اطلاعات است. اگر دوباره همان اطلاعات را بخواهیم و کپی هنوز معتبر باشد، می‌توانیم از آن استفاده کنیم.
مثلاً برنامه از دیتابیس می‌پرسد:
> آخرین نسخهٔ منتشرشدهٔ تعریف A را بده.
نتیجه دریافت و در کش ذخیره می‌شود. درخواست بعدی برای همان جست‌وجو می‌تواند نتیجه را از کش بخواند. اگر تعریف تغییر کند، باید تشخیص دهیم که کپی قبلی دیگر قابل استفاده نیست.
در پروژهٔ ما، تعریف‌های اصلی و وضعیت اجرای ورکفلو در SQL باقی می‌مانند. Redis محل نگه‌داری کپی‌های موقتِ قابل اشتراک است.
کش به معنی حذف تمام درخواست‌های SQL نیست. در حالت کش مشترک، برنامه برای بررسی اعتبار کش یک query کوچک به SQL می‌زند. صرفه‌جویی اصلی در تکرار خواندن داده‌های قابل کش است.
چند واژه که در ادامه می‌بینید:
| واژه | معنی |
|---|---|
| Cache Hit | نتیجهٔ قابل استفاده در کش پیدا شده است |
| Cache Miss | نتیجهٔ قابل استفاده پیدا نشده و باید از مسیر اصلی خوانده شود |
| Payload | خود داده‌ای که داخل ورودی کش نگه‌داری می‌شود |
| TTL | زمان باقی‌مانده تا انقضای کلید |
| Absolute Expiration | پایان اعتبار در زمانی ثابت نسبت به ایجاد ورودی |
| Sliding Expiration | پایان اعتبار بعد از مدت مشخصی بدون خواندن؛ خواندن معتبر زمان را تمدید می‌کند |
| Revision | شمارهٔ وضعیت اعتبار کش |
## ۳. MemoryCache و Redis چه تفاوتی دارند؟
`MemoryCache` اطلاعات را داخل حافظهٔ process برنامه نگه می‌دارد. اگر دو process از برنامه اجرا کنید، هرکدام حافظهٔ کش خودشان را دارند.
Redis یک سرویس مستقل است. چند process می‌توانند به همان Redis وصل شوند و داده‌های مشترک را از آن بخوانند.
| موضوع | MemoryCache | Redis |
|---|---|---|
| محل دادهٔ کش | حافظهٔ process برنامه | سرویس Redis |
| اشتراک بین processها | هر process کپی خودش را دارد | با اتصال و فضای کلید مشترک، داده مشترک است |
| پس از restart برنامه | کش آن process از بین می‌رود | restart برنامه به‌تنهایی داده‌های Redis را پاک نمی‌کند |
| نیاز به اتصال شبکه | برای خود کش محلی ندارد | دارد |
| مناسب‌بودن برای شیء اجرایی محلی | بله، بسته به مصرف‌کننده | معمولاً داده باید قابل تبدیل به متن یا ساختار ذخیره‌شدنی باشد |
اگر یک سرور دو node داشته باشد، منظور می‌تواند دو process یا دو نمونهٔ اجرای برنامه باشد. حتی روی همان سرور، حافظهٔ این دو process یکی نیست.
در اجرای چندسروری، `localhost` روی هر سرور به خود همان سرور اشاره می‌کند. برای دادهٔ مشترک، همهٔ نودهای مرتبط باید به Redis مشترک وصل شوند.
## ۴. هر پروژه در این مسیر چه مسئولیتی دارد؟
| پروژه | مسئولیت |
|---|---|
| `Caspian.Elsa.Core` | پیاده‌سازی اصلی موتور ورکفلو، قرارداد کش و providerهای آن |
| `Elsa.Server.Web` در Core | برنامهٔ میزبان برای اجرای مستقیم موتور، مثلاً هنگام استفاده از Studio |
| `Caspian.Elsa.Studio` | رابط کاربری طراحی و مدیریت؛ با بک‌اند ارتباط دارد |
| `Caspian.WorkflowFramework` | پکیجی که امکانات Core را برای برنامهٔ مصرف‌کننده پیکربندی می‌کند |
| `NextBankParsian.Web` | میزبان درخواست‌های Web و اجرای مربوط به آن‌ها |
| `NextBankParsian.Admin` | میزبان مدیریت و عملیات مرتبط با ورکفلو |
| `NextBankParsian.Workers` | میزبان پردازش‌های پس‌زمینه |
این نمودار رابطهٔ پکیج‌ها و داده را نشان می‌دهد. پکیج، سرویس شبکه‌ای جداگانه نیست؛ کد آن داخل برنامهٔ میزبان اجرا می‌شود.
```mermaid
flowchart LR
Core["پکیج‌های Core"] --> Framework["پکیج WorkflowFramework"]
Framework --> Web["NextBank Web"]
Framework --> Admin["NextBank Admin"]
Framework --> Workers["NextBank Workers"]
Web --> Redis["Redis مشترک"]
Admin --> Redis
Workers --> Redis
Web --> SQL["SQL مشترک ورکفلو"]
Admin --> SQL
Workers --> SQL
```
هر میزبان، سرویس‌ها و اشیای خودش را می‌سازد. اشتراک داده از طریق Redis و SQL انجام می‌شود.
## ۵. Provider یعنی چه؟
Provider یعنی پیاده‌سازیِ محل ذخیره و خواندن کش. در این پروژه، `Memory` و `Redis` دو provider هستند.
مصرف‌کنندهٔ کش می‌گوید «این مقدار را ذخیره کن» یا «مقدار این کلید را بخوان». provider کار مربوط به محل ذخیره را انجام می‌دهد.
مثلاً برای یک مصرف‌کننده، عملیات چنین است:
```text
نتیجهٔ جست‌وجوی تعریف A را ذخیره کن.
```
وقتی provider برابر Memory باشد، ذخیره در حافظهٔ همان process انجام می‌شود. وقتی Redis باشد، دادهٔ قابل ذخیره به Redis فرستاده می‌شود.
در Core، قرارداد `ICacheProvider` این عملیات را تعریف می‌کند. به همین دلیل، برای افزودن provider جدید لازم نیست منطق جست‌وجوی تعریف‌ها را از ابتدا بنویسیم.
## ۶. UseCaching چه کاری می‌کند؟
این قسمت از [کد Framework](../Caspian.WorkflowFramework.Package/Config/DependencyInjection.cs) تنظیمات مشترک کش را انتخاب می‌کند:
```csharp
.UseCaching(cache =>
{
cache.Duration = TimeSpan.FromDays(1);
cache.RegisterRedis();
cache.UseConfiguration(
builder.Configuration.GetSection("Workflow:Cache"));
configureCache?.Invoke(cache);
})
```
هر خط را جدا بررسی کنیم:
| خط | کار واقعی |
|---|---|
| `cache.Duration = TimeSpan.FromDays(1)` | مدت پیش‌فرض کش در Framework را یک روز قرار می‌دهد |
| `cache.RegisterRedis()` | نام Redis را به‌عنوان provider قابل انتخاب ثبت می‌کند |
| `cache.UseConfiguration(...)` | تنظیمات بخش `Workflow:Cache` را می‌خواند و provider را انتخاب می‌کند |
| `configureCache?.Invoke(cache)` | اگر مصرف‌کننده callback داده باشد، آن را اجرا می‌کند |
`RegisterRedis()` به‌تنهایی Redis را انتخاب نمی‌کند. این خط می‌گوید «امکان انتخاب Redis وجود دارد». انتخاب با تنظیم `Provider` در فایل یا با `UseRedis(...)` در کد انجام می‌شود.
اگر تنظیم زیر خوانده شود، Redis انتخاب می‌شود:
```json
{
"Workflow": {
"Cache": {
"Provider": "Redis"
}
}
}
```
اگر provider از تنظیمات یا کد انتخاب نشده باشد، پیش‌فرض عمومی Core برابر Memory است. وجود یک سرور Redis یا نصب پکیج Redis، به‌تنهایی provider کش Elsa را تغییر نمی‌دهد.
## ۷. پس چرا داخل سه UseCache نام Redis وجود ندارد؟
بعد از انتخاب تنظیمات مشترک، این خطوط مصرف‌کننده‌های کش را فعال می‌کنند:
```csharp
management.UseCache();
runtime.UseCache();
.UseHttp(http => http.UseCache())
```
تفاوت دو نوع فراخوانی این است:
| فراخوانی | سؤالی که جواب می‌دهد |
|---|---|
| `UseCaching(...)` | کش را کجا و با چه تنظیماتی نگه داریم؟ |
| `management.UseCache()` | کش مربوط به تعریف‌های ورکفلو فعال باشد؟ |
| `runtime.UseCache()` | کش مربوط به Triggerها و هماهنگی مرتبط با Runtime فعال باشد؟ |
| `http.UseCache()` | نتیجهٔ پیدا‌کردن ورکفلو برای درخواست HTTP ورودی کش شود؟ |
سه مصرف‌کننده به تنظیمات مشترک همان ماژول Elsa متصل‌اند. نام Redis در آن‌ها تکرار نمی‌شود، چون provider قبلاً انتخاب شده است.
```mermaid
flowchart TD
Settings["Workflow:Cache در فایل تنظیمات"] --> Feature["UseCaching / CacheFeature"]
Feature --> Manager["مدیر کش مشترک در همین ماژول"]
Management["management.UseCache"] --> Manager
Runtime["runtime.UseCache"] --> Manager
Http["http.UseCache"] --> Manager
Manager --> Provider["provider انتخاب‌شده: Redis یا Memory"]
```
این استفاده از تنظیم مشترک است. برنامه از روی وجود Redis در شبکه، provider را حدس نمی‌زند.
اگر در تنظیمات مشترک `Provider` را به Memory تغییر دهید و برنامه را دوباره اجرا کنید، این سه خط همچنان همان شکل قبلی را دارند، ولی از provider جدید استفاده می‌کنند.
## ۸. این اشتراک در کد چگونه پیاده شده است؟
برای این بخش فقط سه مفهوم لازم داریم:
| مفهوم | معنی ساده |
|---|---|
| Module | مجموعهٔ قابلیت‌های نصب‌شدهٔ Elsa در این میزبان |
| Feature | یکی از قابلیت‌های آن مجموعه، مثلاً قابلیت کش |
| DI یا Dependency Injection | سازوکاری که سرویس‌های ثبت‌شده را هنگام نیاز به کلاس‌ها می‌دهد |
در [Module](../../../Caspian.Elsa.Core/Repo/src/common/Elsa.Features/Implementations/Module.cs)، Featureها بر اساس نوع نگه‌داری می‌شوند. وقتی چند مصرف‌کننده تنظیم `CacheFeature` را درخواست کنند، به همان Feature متعلق به آن ماژول دسترسی پیدا می‌کنند.
در [CacheConsumerFeature](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Features/CacheConsumerFeature.cs)، این ارتباط مشخص است:
```csharp
public CacheFeature Cache => Module.Configure<CacheFeature>();
```
مدیر کش، یعنی `ICacheManager`، از طریق DI به مصرف‌کننده‌ها داده می‌شود. مدیر کش هم از `ICacheProvider` انتخاب‌شده استفاده می‌کند.
```text
مصرف‌کنندهٔ کش
↓
ICacheManager
↓
ICacheProvider
↓
Redis یا Memory
```
در ساختار فعلی، provider کش در سطح یک ماژول مشترک است. انتخاب صریح دو provider متفاوت برای مصرف‌کننده‌های همان ماژول خطا می‌دهد. این ساختار برای سه مصرف‌کنندهٔ بالا، سه انتخاب مستقل و پنهانی ایجاد نمی‌کند.
## ۹. management.UseCache دقیقاً چه چیزی را کش می‌کند؟
Management با تعریف‌های ورکفلو کار می‌کند. کش آن شامل خواندن یک تعریف، فهرست تعریف‌ها، خلاصه‌ها و بعضی نتایج جست‌وجو و شمارش است.
مثلاً این دو درخواست می‌توانند دو کلید متفاوت داشته باشند:
```text
تعریف A، نسخهٔ منتشرشده
فهرست تعریف‌های منتشرشده، صفحهٔ اول
```
کلید دوم می‌تواند چند تعریف را داخل یک مقدار نگه دارد. کلیدها بر اساس نوع عملیات، ورودی جست‌وجو، صفحه‌بندی، نوع داده و Tenant ساخته می‌شوند.
گراف اجراییِ ورکفلو از تعریف معتبر بازسازی می‌شود. اشیای اجرایی و delegateهای آمادهٔ اجرا مستقیماً به Redis فرستاده نمی‌شوند.
منبع این رفتار: [CachingWorkflowDefinitionStore](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs) و [CachingWorkflowDefinitionService](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs).
## ۱۰. runtime.UseCache دقیقاً چه کاری دارد؟
Runtime باید بتواند Triggerهای مربوط به یک رویداد را پیدا کند. کش Runtime نتیجهٔ خواندن Triggerها را نگه می‌دارد.
برای مثال، هنگام دریافت یک رویداد، موتور می‌پرسد:
> کدام تعریف‌های ورکفلو Trigger متناظر با این رویداد دارند؟
نتیجهٔ این جست‌وجو می‌تواند کش شود. این کار در [CachingTriggerStore](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs) انجام می‌شود.
در تغییر جدید Core، این قابلیت لایهٔ هماهنگی تغییر Bookmarkها را هم ثبت می‌کند. Bookmarkها همچنان در SQL هستند، ولی تغییرشان برای تازه‌شدن جدول مسیرهای HTTP نودهای دیگر ثبت می‌شود.
وضعیت اجرای ورکفلو، Bookmarkها و لاگ‌ها با این خط به Redis منتقل نمی‌شوند. تنظیم قفل اجرای چندنودی هم از مسیر جداگانهٔ `DistributedLocking` انجام می‌شود.
### چرا شرط ثابت useCaching حذف شد؟
قبلاً در Framework داشتیم:
```csharp
const bool useCaching = true;
if (useCaching)
runtime.UseCache();
```
چون مقدار ثابت برابر true بود، این شرط همیشه اجرا می‌شد. اکنون کنترل خواندن و نوشتن دادهٔ کش با `Workflow:Cache:Enabled` انجام می‌شود.
با `Enabled=false`، لایه‌های ثبت‌شده همچنان وجود دارند، اما مدیر کش برای خواندن داده از مسیر اصلی استفاده می‌کند. ثبت و هماهنگی تغییرات در حالت Redis نیز ادامه پیدا می‌کند.
## ۱۱. http.UseCache چه کاربردی دارد؟
این کد دو کار را ترکیب می‌کند:
```csharp
.UseHttp(http => http.UseCache())
```
`UseHttp` قابلیت‌های HTTPِ Elsa را فعال می‌کند. `http.UseCache` کشِ جست‌وجوی ورکفلو برای درخواست HTTP ورودی را فعال می‌کند.
فرض کنید در یک تعریف، HTTP Endpoint زیر ساخته‌اید:
```text
POST /workflows/review
```
وقتی درخواست برسد، Elsa باید تعریف و Trigger متناظر را پیدا کند. نتیجهٔ پیدا‌کردن آن‌ها قابل کش است.
در Redis، اطلاعاتی مانند شناسهٔ نسخهٔ تعریف و Triggerها ذخیره می‌شود. گراف اجرایی بعداً از تعریف معتبر ساخته می‌شود.
اگر Activityِ `SendHttpRequest` دارید که از داخل ورکفلو سرویس دیگری را صدا می‌زند، `http.UseCache` پاسخ آن سرویس را کش نمی‌کند. کش عمومی پاسخ‌های APIهای NextBank هم موضوع جداگانه‌ای است.
این قابلیت برای استفاده از کش Redisِ تعریف‌ها الزامی نیست. کاربردش زمانی است که ورکفلوهای دارای HTTP Endpoint ورودی را اجرا می‌کنید.
منبع: [CachingHttpWorkflowLookupService](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Services/CachingHttpWorkflowLookupService.cs).
## ۱۲. چرا Configure<CachingOptions> قبلی حذف شد؟
قبلاً مدت کش را جداگانه ثابت می‌کردیم:
```csharp
builder.Services.Configure<CachingOptions>(
options => options.CacheDuration = TimeSpan.FromDays(1));
```
اکنون در ابتدای تنظیم مشترک، یک روز فقط مقدار پیش‌فرض است:
```csharp
cache.Duration = TimeSpan.FromDays(1);
cache.UseConfiguration(
builder.Configuration.GetSection("Workflow:Cache"));
```
فرض کنید فایل تنظیمات این مقدار را دارد:
```json
{
"Workflow": {
"Cache": {
"Duration": "00:30:00"
}
}
}
```
ترتیب نتیجه چنین می‌شود:
```text
پیش‌فرض Framework: یک روز
↓
مقدار فایل: سی دقیقه
↓
مقدار صریح callback، اگر تعیین شده باشد
```
اگر ثبت ثابت قبلی بعد از تنظیم Feature باقی می‌ماند، می‌توانست مقدار نهایی Options را دوباره یک روز کند. آن ثبت حذف شد تا مدت کش از همین مسیر مشترک تعیین شود.
خود [CacheFeature](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Features/CacheFeature.cs) هنوز مقدار نهایی را در `CachingOptions.CacheDuration` ثبت می‌کند. پس مصرف‌کننده‌ها Options را دریافت می‌کنند؛ فقط محل تعیین مقدار آن یکپارچه شده است.
پیش‌فرض عمومی Core یک دقیقه است. Framework آن را پیش از خواندن تنظیمات، یک روز قرار می‌دهد. مقدار صریح فایل یا callback می‌تواند هر دو را تغییر دهد.
## ۱۳. سه بخش تنظیمات را از هم تشخیص بدهیم
در فایل تنظیمات سه بخش مهم داریم:
| مسیر | مسئولیت |
|---|---|
| `Redis` در ریشه | آدرس و مشخصات اتصال Redis برنامهٔ میزبان |
| `Workflow:Cache` | انتخاب provider و تنظیمات کش ورکفلو |
| `Workflow:Runtime:DistributedLocking` | انتخاب و تنظیمات قفل‌های مشترک |
برای مثال، آدرس شبکه در `Redis:Hosts` قرار دارد. پیشوند کش در `Workflow:Cache:Redis:KeyPrefix` و پیشوند قفل در `Workflow:Runtime:DistributedLocking:KeyPrefix` قرار دارد.
### Database چیست؟
Redis می‌تواند چند دیتابیس منطقی شماره‌دار داشته باشد. در این پروژه شمارهٔ `1` انتخاب شده است. این شماره با نام دیتابیس SQL تفاوت دارد.
`Redis:Database` دیتابیس پیش‌فرض اتصال میزبان است. کش و قفل می‌توانند دیتابیس مورد استفادهٔ خودشان را صریحاً مشخص کنند. در تنظیمات فعلی، هر سه مقدار برابر `1` هستند.
### KeyPrefix چیست؟
پیشوند به ابتدای نام کلید اضافه می‌شود تا کلیدهای بخش‌های مختلف قابل تشخیص باشند:
```text
nextbank:workflow:cache:... ← دادهٔ کش ورکفلو
nextbank:workflow:locks:... ← قفل‌ها
```
یک اتصال Redis می‌تواند هر دو گروه را نگه دارد. متفاوت‌بودن پیشوند، به معنی ساخته‌شدن دو اتصال شبکه نیست.
پیشوند کش فقط برای تعریف‌ها نیست؛ سایر داده‌های قابل کشِ فعال‌شده در همان ماژول، مانند Triggerها و نتیجهٔ جست‌وجوی HTTP، نیز از provider و پیشوند مشترک استفاده می‌کنند. نوع داده و ورودی جست‌وجو در ساخت بخش داخلی کلید نقش دارند.
## ۱۴. اتصال مشترک با IConnectionMultiplexer چگونه کار می‌کند؟
`IConnectionMultiplexer` سرویس مدیریت ارتباط با Redis در کتابخانهٔ StackExchange.Redis است. کلاس‌هایی که به Redis نیاز دارند، می‌توانند این سرویس را از DI بگیرند.
در NextBank، کد زیر آن را ثبت می‌کند:
```csharp
builder.Services.RegisterCache(builder.Configuration);
```
در `Elsa.Server.Web`، ثبت اتصال با این helper انجام می‌شود:
```csharp
services.AddWorkflowRedis(configuration);
```
در هر دو حالت، اتصال به‌صورت Singleton ثبت شده است. یعنی داخل همان ServiceProvider، مصرف‌کننده‌ها همان نمونهٔ سرویس را دریافت می‌کنند.
ثبت سرویس لزوماً اتصال فوری ایجاد نمی‌کند. factory ثبت‌شده هنگام نیاز به سرویس اجرا می‌شود.
Singleton متعلق به همان process و ServiceProvider است. Web، Admin و Workers هرکدام سرویس اتصال خودشان را دارند؛ اگر تنظیماتشان یکی باشد، به همان Redis مشترک وصل می‌شوند.
```text
داخل process مربوط به Web:
کش ورکفلو ─┐
قفل‌ها ────┼── IConnectionMultiplexer همان میزبان ── Redis
سایر مصرف‌کنندگان اتصال ─┘
```
در تنظیم پیش‌فرض provider کش Redis، `ConnectionFactory` سرویس میزبان را می‌گیرد. provider قفل Redis نیز همان سرویس را دریافت می‌کند.
بنابراین تغییر آدرس‌های بخش اصلی `Redis` روی سایر مصرف‌کنندگان همان اتصال در NextBank، مانند کش عمومی یا Redis Streamها، هم اثر دارد.
کش عمومی NextBank و کش Elsa دو سازوکار جدا هستند. داشتن `RegisterCache` به‌تنهایی provider کش Elsa را Redis نمی‌کند؛ انتخاب `Workflow:Cache:Provider` یا `UseRedis` همچنان لازم است.
## ۱۵. تنظیم کامل بخش‌های مربوط به لوکال
این نمونه فقط بخش‌های مربوط به اتصال، کش و قفل را نشان می‌دهد. آن را با سایر تنظیمات برنامه ترکیب کنید؛ تنظیمات SQL و سرویس‌های دیگر برنامه همچنان لازم‌اند.
```json
{
"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:",
"LockAcquisitionTimeout": "00:00:10"
}
}
}
}
```
| گزینه | معنی |
|---|---|
| `AllowAdmin` | اجازهٔ عملیات مدیریتی Redis در تنظیمات این اتصال |
| `Ssl` | استفاده از ارتباط TLS برای اتصال Redis |
| `ConnectTimeout` | زمان انتظار اتصال، به میلی‌ثانیه؛ اینجا پنج ثانیه |
| `ConnectRetry` | تعداد تلاش مجدد اتصال در تنظیمات کتابخانه |
| `ServiceName` خالی | استفاده از اتصال مستقیم در این نمونه، بدون Sentinel |
| `Enabled` | اجازهٔ خواندن و نوشتن payload کش |
| `Provider` زیر Cache | محل ذخیرهٔ کش ورکفلو |
| `Duration` | مدت پایهٔ انقضای ورودی‌های کش |
| `Provider` زیر DistributedLocking | فناوری قفل اجرای مشترک |
| `LockAcquisitionTimeout` | حداکثر انتظار برای گرفتن قفل؛ اینجا ده ثانیه |
قالب مدت کش `TimeSpan` است:
```text
1.00:00:00 = یک روز
00:30:00 = سی دقیقه
00:05:00 = پنج دقیقه
```
نوع انقضا را مصرف‌کننده تعیین می‌کند. مثلاً کش Trigger از انقضای ثابت نسبت به زمان ایجاد استفاده می‌کند و کش خواندن Store تعریف‌ها از انقضای تمدیدشونده با خواندن استفاده می‌کند. پس همهٔ کلیدها لزوماً دقیقاً یک روز بعد از اولین ذخیره حذف نمی‌شوند.
در Core، فایل واقعی [appsettings.Development.json](../../../Caspian.Elsa.Core/Repo/src/apps/Elsa.Server.Web/appsettings.Development.json) است. زمان انتظار قفل فعلی Core از تنظیم ده‌ثانیه‌ای مسیر قدیمی در فایل پایه به ارث می‌رسد؛ نمونهٔ بالا همان مقدار را در مسیر جدید صریح کرده است.
در NextBank، تنظیمات محلی در فایل‌های `appsettings.development.json` قرار دارند و در Git نادیده گرفته می‌شوند. نمونهٔ لوکال بالا، اتصال مستقل را نشان می‌دهد. هنگام اعمال آن روی فایل پایهٔ دارای سه Sentinel، ترکیب آرایهٔ Hosts را طبق فصل بعد در نظر بگیرید.
## ۱۶. تنظیم سرور و نقش Sentinel
Sentinel برای پیدا‌کردن و پایش Redis master استفاده می‌شود. در تنظیم شما، پورت `26379` متعلق به Sentinelهاست. برنامه از نام سرویس `Redis1` برای پیدا‌کردن master مربوط استفاده می‌کند؛ داده‌های کش روی Redis master ذخیره می‌شوند.
در ساختار فعلی NextBank، تنظیمات سرور داخل فایل پایهٔ `appsettings.json` هر سه میزبان قرار دارند. فایل `appsettings.Production.json` جداگانه‌ای وجود ندارد. مثال زیر بخش‌های مرتبط را نشان می‌دهد؛ سایر تنظیمات برنامه مانند ConnectionStrings باید در فایل حفظ شوند:
```json
{
"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
}
]
},
"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"
}
}
}
}
```
فایل‌های واقعی عبارت‌اند از:
- [Web/appsettings.json](../../../NextBankParsian/Repo/src/Web/appsettings.json)
- [Admin/appsettings.json](../../../NextBankParsian/Repo/src/Admin/appsettings.json)
- [Workers/appsettings.json](../../../NextBankParsian/Repo/src/Workers/appsettings.json)
در هر سه میزبان، کش و قفل از دیتابیس Redis `1` استفاده می‌کنند. مدت پایهٔ کش یک روز و حداکثر انتظار گرفتن قفل Runtime ده ثانیه است. همهٔ نودهایی که روی همان مجموعهٔ ورکفلو کار می‌کنند باید به همان SQL ورکفلو و Redis مشترک وصل باشند.
### ترکیب فایل‌ها و آرایهٔ 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 قابل استفاده است:
```json
{
"Redis": {
"ServiceName": "",
"Hosts": [
{ "Host": "localhost", "Port": 6379 },
{ "Host": "", "Port": 26379 },
{ "Host": "", "Port": 26379 }
]
}
}
```
این نمونه روش تنظیم لوکال روی یک فایل پایهٔ دارای سه Sentinel را آموزش می‌دهد. عدد پورتِ ردیف دارای Host خالی مصرف نمی‌شود. در فایل‌های Development فعلی فقط ردیف صفر تعیین شده است؛ پس ترکیب فعلی فایل‌ها هنوز دو endpoint دیگر پایه را نگه می‌دارد. برای تشخیص تنظیم واقعی، مقادیر نهاییِ ترکیب‌شده را بررسی کنید، نه فقط یک فایل را.
متغیرهای محیطی، User Secrets و خط فرمان نیز می‌توانند مقادیر فایل‌ها را تغییر دهند. نام محیط و بزرگی و کوچکی حروف نام فایل، به‌خصوص روی Linux، باید با روش بارگذاری میزبان سازگار باشد. روی سرور باید فایل پایهٔ دارای تنظیمات Sentinel همراه خروجی باشد و override لوکال در محیط Production اعمال نشود.
## ۱۷. تنظیم از کد چگونه انجام می‌شود؟
در NextBank، ثبت اتصال میزبان و دریافت اتصال معتبر SQL از قبل انجام می‌شود. پس از آن، می‌توانید تنظیمات فایل را مصرف کنید:
```csharp
builder.Services.RegisterCache(builder.Configuration);
builder.AddWorkflowFramework(connectionString);
```
`connectionString` در این مثال، اتصال معتبر SQL ورکفلو است که برنامهٔ میزبان آن را خوانده و در صورت نیاز رمزگشایی کرده است.
برای تعیین صریح بعضی مقادیر کش در callback، شکل فراخوانی چنین است:
```csharp
using Caspian.WorkflowFramework;
using Elsa.Extensions;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
builder.AddWorkflowFramework(connectionString, cache =>
{
cache.Duration = TimeSpan.FromMinutes(30);
cache.UseRedis(redis =>
{
redis.ConnectionFactory =
sp => sp.GetRequiredService<IConnectionMultiplexer>();
redis.Database = 1;
redis.KeyPrefix = "nextbank:workflow:cache:";
});
});
```
`sp` همان ServiceProvider است. `GetRequiredService` سرویس ثبت‌شدهٔ اتصال Redis میزبان را دریافت می‌کند. `ConnectionFactory` مشخص می‌کند provider کش، اتصال را از کجا بگیرد.
در تنظیم فعلی، دریافت اتصال میزبان رفتار پیش‌فرض هم هست؛ نوشتن صریح این factory، تصمیم را در کد نشان می‌دهد.
callback بعد از فایل اجرا می‌شود. مقدارهایی که صریحاً تعیین می‌کنید، بر مقدار فایل اولویت دارند. تنظیماتی که در callback تعیین نشده‌اند، از مسیر قبلی باقی می‌مانند؛ مثلاً انتخاب Redis به‌تنهایی مقدار `Enabled=false` را true نمی‌کند.
در استفادهٔ مستقیم از Core، این قطعه داخل تنظیم Workflow Management قابل استفاده است:
```csharp
management.UseCache(cache =>
{
cache.UseRedis(redis =>
{
redis.ConnectionFactory =
sp => sp.GetRequiredService<IConnectionMultiplexer>();
redis.Database = 1;
redis.KeyPrefix = "nextbank:workflow:cache:";
});
});
```
این قطعه جایگزین ثبت کامل persistence و سایر قابلیت‌های Core نیست. provider انتخاب‌شده، برای سایر مصرف‌کننده‌های کش همان ماژول هم استفاده می‌شود.
قفل‌های Runtime در Framework فعلی از تنظیمات `DistributedLocking` خوانده می‌شوند؛ callback بالا تنظیمات کش را تغییر می‌دهد.
## ۱۸. اگر Memory یا کش غیرفعال بخواهیم چه می‌شود؟
برای Memory می‌توانید تنظیم کنید:
```json
{
"Workflow": {
"Cache": {
"Enabled": true,
"Provider": "Memory",
"Duration": "00:30:00"
}
}
}
```
خطوط `management.UseCache()`، `runtime.UseCache()` و `http.UseCache()` تغییر نمی‌کنند. provider مشترکِ انتخاب‌شده Memory خواهد بود، مگر اینکه در callback صریحاً انتخاب دیگری کرده باشید.
برای کنارگذاشتن خواندن و نوشتن payload کش در حالت Redis:
```json
{
"Workflow": {
"Cache": {
"Enabled": false,
"Provider": "Redis"
}
}
}
```
مدیر کش در این حالت داده را از Store اصلی می‌خواند. هماهنگی تغییرات و مسیرهای HTTP در حالت Redis همچنان برقرار می‌ماند. تغییر provider کش یا Enabled، provider قفل را خودکار تغییر نمی‌دهد.
بعد از تغییر provider یا تنظیمات مربوط، برنامه را دوباره راه‌اندازی کنید. این تنظیمات هنگام راه‌اندازی Featureها مصرف می‌شوند؛ بارگذاری مجدد فایل به‌تنهایی به معنی تعویض زندهٔ provider نیست.
## ۱۹. چه چیزهایی مشترک و چه چیزهایی محلی می‌مانند؟
| داده یا شیء | محل در حالت Redis این پیاده‌سازی | دلیل |
|---|---|---|
| کپی تعریف‌ها و نتیجهٔ جست‌وجوی آن‌ها | Redis | چند نود به همان اطلاعات تعریف نیاز دارند |
| کپی Triggerها | Redis | نتیجهٔ جست‌وجوی Trigger قابل اشتراک است |
| نتیجهٔ جست‌وجوی ورکفلوی HTTP | دادهٔ قابل ذخیره در Redis | پیدا‌کردن تعریف و Trigger متناظر قابل کش است |
| گراف اجرایی | از تعریف معتبر در process ساخته می‌شود | شیء اجرایی کامل مستقیم در Redis ذخیره نمی‌شود |
| کد C# کامپایل‌شده، اسکریپت یا قالب آماده | حافظهٔ همان process | هر process اشیای اجرایی خودش را دارد |
| جدول مسیرهای HTTP | حافظهٔ هر process، با کنترل اعتبار SQL | هر نود جدول آمادهٔ خودش را بازسازی می‌کند |
| تعریف اصلی، نسخه‌ها، وضعیت اجرا و لاگ‌ها | SQL | دادهٔ اصلی و پایدار سیستم هستند |
| Bookmarkها | SQL | اطلاعات انتظار و ادامهٔ اجرای ورکفلو هستند |
| کلیدهای قفل | Redis، در زمان نیاز به قفل | هماهنگی عملیات هم‌زمان نودها |
نتیجهٔ جست‌وجوی خالی هم می‌تواند کش شود. مثلاً اگر هیچ Triggerی برای یک رویداد پیدا نشد، همین پاسخ خالی می‌تواند جلوی تکرار خواندن دادهٔ مشابه را بگیرد.
فایل‌ها، کش عمومی NextBank، Redis Streamها و سایر قابلیت‌های برنامه تنظیمات خودشان را دارند. اشتراک اتصال Redis به معنی یکی‌شدن همهٔ این قابلیت‌ها نیست.
## ۲۰. چرا برای کش Redis یک جدول SQL اضافه شد؟
مشکل فقط محل نگه‌داری کپی نیست. باید بدانیم کپی هنوز معتبر است یا نه.
فرض کنید Web نتیجهٔ تعریف A را کش کرده است. Admin تعریف A را تغییر می‌دهد. Web در خواندن بعدی باید بتواند تشخیص دهد که نتیجهٔ قبلی دیگر متعلق به وضعیت جدید نیست.
در این پیاده‌سازی، [EFCoreCacheConsistencyStore](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.EntityFrameworkCore/Modules/Management/EFCoreCacheConsistencyStore.cs) شمارهٔ اعتبار و وضعیت تغییر ناتمام را از جدول `Elsa.CacheMutations` در SQL می‌خواند.
این شماره «Revision کش» است. با نسخهٔ تجاری تعریف ورکفلو تفاوت دارد.
```text
قبل از تغییر مرتبط:
Revision کش = 14
بعد از ثبت و تکمیل تغییر:
Revision کش = یک مقدار جدید
```
هنگام خواندن، مدیر کش از Revision فعلی برای ساخت کلید استفاده می‌کند. درخواستی که Revision جدید را از SQL می‌خواند، کلید مربوط به Revision قبلی را مصرف نمی‌کند.
در حالت تغییر ناتمام، خواندن و نوشتن کش مربوط کنار گذاشته می‌شود و Store اصلی خوانده می‌شود. بعد از خواندن داده برای یک Cache Miss نیز وضعیت دوباره بررسی می‌شود؛ فقط اگر وضعیت معتبر مانده باشد، نتیجه در کش قرار می‌گیرد.
این ترتیب، خواندن عادی از کش مشترک را نشان می‌دهد:
```text
خواندن وضعیت اعتبار از SQL
↓
ساخت کلید با Revision فعلی
↓
خواندن Redis
├── نتیجه موجود است: استفاده از نتیجه
└── نتیجه موجود نیست: خواندن Store اصلی
↓
بررسی دوبارهٔ اعتبار
↓
ذخیرهٔ نتیجه، اگر هنوز معتبر است
```
ثبت اعتبار پایدار کمک می‌کند هماهنگی فقط به رسیدن یک پیام لحظه‌ای بین نودها وابسته نباشد. کلیدهای Revision قبلی ممکن است تا پایان زمان انقضا در Redis باقی بمانند، اما درخواست‌های دارای Revision جدید سراغ آن‌ها نمی‌روند.
برای SQL Server، migration آمادهٔ این تغییر [AddCacheMutationFences](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Management/20261007000000_AddCacheMutationFences.cs) است. schema مربوط باید پیش از مصرف کش مشترک وجود داشته باشد. برنامه‌ها قابلیت اجرای migration در startup دارند؛ انتخاب SQL مقصد باید هنگام راه‌اندازی بررسی شود.
جدول اعتبار، جای جدول `WorkflowDefinitions` را نمی‌گیرد. اولی اعتبار کپی موقت را ثبت می‌کند و دومی دادهٔ اصلی تعریف‌ها را نگه می‌دارد.
## ۲۱. چرا تعداد کلیدهای Redis با تعداد تعریف‌های SQL برابر نیست؟
در startup، کل جدول تعریف‌ها خودکار به Redis کپی نمی‌شود. کش هنگام خواندن اطلاعات پر می‌شود؛ به این روش معمولاً Cache-aside می‌گویند.
| درخواست | تعداد احتمالی کلید برای همان نتیجه |
|---|---|
| خواندن یک تعریف با فیلتر مشخص | یک کلید |
| خواندن یک صفحه شامل ۲۰ تعریف | یک کلید با مقدار شامل آن صفحه |
| خواندن همان اطلاعات با فیلتر یا صفحه‌بندی دیگر | کلید دیگر |
| نتیجهٔ خالی یک جست‌وجو | یک کلید با مقدار خالی |
بنابراین ۲۸ تعریف ممکن است داخل چند نتیجهٔ فهرست قرار بگیرند، یا هنوز همهٔ آن‌ها خوانده و کش نشده باشند. چند کلید هم می‌توانند به جست‌وجوهای متفاوت دربارهٔ یک تعریف مربوط باشند.
شکل کلید دادهٔ کش در Redis چنین است:
```text
nextbank:workflow:cache:v1:{revision}:{hash}
```
| بخش | معنی |
|---|---|
| `nextbank:workflow:cache:` | پیشوند تنظیم‌شده |
| `v1` | نسخهٔ قرارداد ساخت کلید |
| `revision` | شمارهٔ اعتبار کش |
| `hash` | مقدار محاسبه‌شده از نوع داده و کلید منطقی جست‌وجو |
اگر در ابزار Redis پوشه‌های `14`، `18` یا `28` می‌بینید، این اعداد در این ساختار Revision هستند. تعداد تعریف‌ها یا شمارهٔ نسخهٔ یک ورکفلو نیستند.
در provider فعلی، مقدار کش به‌صورت Redis Hash ذخیره می‌شود. بخش `payload` دادهٔ ذخیره‌شده را نشان می‌دهد. مقدار `[]` یا صفحه‌ای با `items` خالی، می‌تواند نتیجهٔ معتبرِ یک جست‌وجوی خالی باشد.
در ابزار Redis ممکن است این فیلدها را هم ببینید:
| فیلد یا ستون | معنی |
|---|---|
| `payload` | نتیجهٔ ذخیره‌شده، به شکل متن JSON |
| `slide` | مدت انقضای تمدیدشونده به میلی‌ثانیه؛ صفر یعنی این نوع انقضا تعیین نشده |
| `deadline` | زمان پایان انقضای ثابت؛ صفر یعنی سقف ثابت تعیین نشده |
| ستون TTL ابزار | زمان باقی‌ماندهٔ واقعی کلید در Redis |
صفر بودن `deadline` به‌تنهایی به معنی بی‌انقضا بودن کلید نیست؛ ممکن است `slide` برای آن تعیین شده باشد و Redis زمان انقضای فعال داشته باشد.
برای بررسی، مقدار `payload` و Revision را هم ببینید. شمارش کلیدها به‌تنهایی معیار صحت کش نیست. Refresh، انتخاب Database درست و محدودیت Fetch در ابزار مشاهده نیز روی چیزی که می‌بینید اثر دارند.
## ۲۲. قفل چه تفاوتی با کش دارد؟
کش به پرسش «اطلاعات را از کجا بخوانیم؟» مربوط است. قفل به پرسش «آیا الان اجازه داریم این عملیات مشترک را انجام دهیم؟» مربوط است.
فرض کنید Web و Workers هر دو بخواهند همان نمونهٔ ورکفلو را ادامه دهند. قفلِ مربوط به آن نمونه باعث هماهنگی این دو عملیات می‌شود.
```text
Web قفل نمونهٔ X را می‌گیرد.
↓
Workers برای همان قفل باید منتظر بماند یا طبق عملیاتش نتواند آن را بگیرد.
↓
Web عملیات محافظت‌شده را تمام می‌کند و قفل را آزاد می‌کند.
↓
نود دیگر می‌تواند برای گرفتن همان قفل تلاش کند.
```
در [DistributedWorkflowClient](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs)، اجرای یک نمونه و ادامهٔ اجرای آن با قفل `workflow-instance:{WorkflowInstanceId}` انجام می‌شوند.
با پیشوند تنظیم‌شده، نام نهایی این است:
```text
nextbank:workflow:locks:workflow-instance:{WorkflowInstanceId}
```
دو اجرای مستقل از یک تعریف، `WorkflowInstanceId` متفاوت دارند. این قفل برای همان نمونهٔ مشخص است؛ همهٔ اجراهای یک تعریف را زیر یک قفل عمومی قرار نمی‌دهد.
قفل در مسیرهای دیگری هم استفاده می‌شود:
| عملیات | کاربرد |
|---|---|
| انتشار تعریف | هماهنگی تغییر وضعیت انتشار همان تعریف |
| ذخیرهٔ تعریف از API مربوط | هماهنگی درخواست‌های هم‌زمان ذخیره |
| پردازش صف Bookmark | هماهنگی پردازش مشترک صف |
| بعضی taskهای دارای `SingleNodeTask` | هماهنگی اجرای بخش محافظت‌شدهٔ کار بین نودها |
`runtime.UseDistributedRuntime()` قابلیت Runtime توزیع‌شده را فعال می‌کند. `UseConfiguredDistributedLocking(...)` در میزبان یا Framework، provider قفل و تنظیمات آن را انتخاب می‌کند. `runtime.UseCache()` هم مصرف‌کنندهٔ کش Runtime را فعال می‌کند. این سه فراخوانی مسئولیت‌های جدا دارند.
## ۲۳. چرا بعد از اجرای برنامه کلید قفل نمی‌بینیم؟
ثبت provider قفل، به معنی ساخت کلید دائمی قفل نیست. کلید زمانی لازم می‌شود که عملیات محافظت‌شده قفل بگیرد.
ترتیب عادی چنین است:
```text
گرفتن قفل ← انجام عملیات ← آزادشدن قفل و حذف کلید مربوط
```
یک عملیات کوتاه ممکن است قبل از Refresh کردن ابزار Redis تمام شود. قفل‌های پس‌زمینه هم ممکن است فقط برای مدت کوتاهی دیده شوند.
اگر ورکفلو روی یک Bookmark منتظر کاربر بماند، اجرای فعلی می‌تواند برگردد و قفل آن نوبت آزاد شود. هنگام ادامهٔ اجرا دوباره قفل گرفته می‌شود. افزودن Activity تأخیر به ورکفلو لزوماً باعث نگه‌داشتن قفل در تمام مدت انتظار نمی‌شود.
`LockAcquisitionTimeout=00:00:10` یعنی حداکثر انتظار برای گرفتن قفل در عملیات Runtime مربوط ده ثانیه است. مدت نگه‌داشتن قفل یا TTL آن نیست. مدت یک‌روزهٔ کش نیز زمان قفل را تعیین نمی‌کند. بعضی عملیات دیگر، مانند API ذخیرهٔ تعریف، زمان انتظار خودشان را دارند.
### چگونه قفل را در Debug ببینیم؟
1. در `DistributedWorkflowClient` روی خط `var result = await func();`، بعد از گرفتن قفل، Breakpoint بگذارید.
2. یک ورکفلو را از مسیر Runtime توزیع‌شده اجرا کنید.
3. وقتی روی Breakpoint متوقف شد، بلافاصله Database `1` را در ابزار Redis Refresh کنید.
4. کلیدهای دارای الگوی `nextbank:workflow:locks:*` را بررسی کنید.
5. با ادامهٔ اجرا و پایان عملیات، کلید مربوط باید حذف شود.
این مشاهدهٔ Debug را کوتاه انجام دهید؛ توقف طولانی process روش مناسبی برای آزمون زمان‌بندی و نگه‌داری قفل توزیع‌شده نیست.
## ۲۴. اگر ارتباط قطع شود، چه اتفاقی می‌افتد؟
| وضعیت | رفتار مرتبط در پیاده‌سازی فعلی |
|---|---|
| خواندن کش Redis با خطای Redis مواجه شود | خطا ثبت می‌شود و نتیجه از Store اصلی بازسازی می‌شود |
| نوشتن کش Redis با خطای Redis مواجه شود | خطا ثبت می‌شود؛ دادهٔ اصلی و ثبت اعتبار SQL همچنان مرجع‌اند |
| payload کش قابل خواندن نباشد | نتیجه به‌عنوان Cache Miss بازسازی می‌شود |
| SQLِ اعتبار در دسترس نباشد | اعتبار کپی مشترک قابل اثبات نیست و مسیر مربوط می‌تواند خطا بدهد |
| قفل Redis قابل گرفتن نباشد | رفتار گرفتن قفل، انتظار، timeout یا خطای همان عملیات اعمال می‌شود |
در حالت انتخاب Redis برای کش Elsa، خطای Redis باعث جایگزینی payload با کش Memory محلی نمی‌شود؛ Store اصلی خوانده می‌شود. رفتار fallback کش عمومی NextBank، رفتار جداگانه‌ای است.
اگر در SQL تغییر ناتمام ثبت شده باشد، کش مربوط موقتاً کنار گذاشته می‌شود. ثبت ناتمام را بدون بررسی وضعیت writer و دادهٔ واقعی SQL کامل یا حذف نکنید. [مستند مفصل](workflow-cache-explained.fa.md) روش بررسی این وضعیت را توضیح می‌دهد.
هنگام وجود `Transaction.Current` نیز مسیر عمومی مدیر کش از کش عبور می‌کند و factory اصلی را اجرا می‌کند. بنابراین همهٔ خواندن‌ها الزاماً کلید Redis ایجاد نمی‌کنند.
## ۲۵. تغییرات هر پروژه را با دلیل مرور کنیم
| پروژه یا بخش | تغییر اصلی | دلیل |
|---|---|---|
| Core / Caching | قرارداد provider، تنظیمات مشترک و مدیر کش جدید | انتخاب Memory یا Redis و امکان توسعهٔ provider |
| Core / Caching.Redis | ذخیرهٔ payload، دیتابیس، پیشوند و انقضا | نگه‌داری دادهٔ قابل اشتراک در Redis |
| Core / Management | اتصال کش تعریف‌ها به provider مشترک | مصرف همان تنظیمات کش |
| Core / Runtime | کش Trigger و هماهنگی تغییر Bookmark | پوشش دادهٔ قابل کش و تازه‌شدن مسیرهای مرتبط |
| Core / HTTP | کش جست‌وجو و کنترل اعتبار جدول مسیرهای محلی | هماهنگی lookup و مسیرهای HTTP در نودها |
| Core / EF Core | ثبت پایدار اعتبار و migration SQL Server | تشخیص کش قدیمی و تغییر ناتمام |
| Elsa.Server.Web | ثبت اتصال میزبان و خواندن تنظیمات کش و قفل | اجرای لوکال Core با Redis |
| WorkflowFramework | خواندن فایل، callback کش و helper قفل | رساندن امکانات Core به برنامهٔ مصرف‌کننده |
| NextBank Web و Admin | تنظیم Development و Production و مصرف پکیج جدید | اتصال و تنظیم هماهنگ میزبان‌ها |
| NextBank Workers | همان سیاست کش و قفل و ثبت اتصال پیش از Framework | استفاده از امکانات مشترک در پردازش پس‌زمینه |
| Studio | در این مرحله تغییر جدیدی برای Redis ندارد | کش و قفل در بک‌اند مدیریت می‌شوند |
حذف `Configure<CachingOptions>` ثابت، دلیلش حفظ اولویت تنظیمات فایل و callback بود. حذف شرط ثابت `useCaching=true`، کنترل payload کش را به تنظیم `Enabled` منتقل کرد. افزودن `http.UseCache`، کش lookup HTTP ورودی را فعال کرد.
## ۲۶. برای اجرا و بررسی عملی چه کنیم؟
### اجرای Core از Visual Studio
1. Redis لوکال باید روی `localhost:6379` در دسترس باشد.
2. `Elsa.Server.Web` را startup قرار دهید و پروفایل Development را اجرا کنید.
3. اتصال SQL تنظیم‌شدهٔ Core و schema اعتبار کش باید آماده باشند؛ startup می‌تواند migration را اجرا کند.
4. از Studio یا API، یک تعریف را بخوانید و همان جست‌وجو را تکرار کنید.
5. در Redis، Database `1` و پیشوند `nextbank:workflow:cache:` را بررسی کنید.
تعریف‌ها هنگام نیاز کش می‌شوند. برای دیدن قفل، عملیات محافظت‌شده مثل اجرای نمونه لازم است و کلید آن می‌تواند سریع حذف شود.
### اجرای NextBank در لوکال
برای اجرای لوکال، تنظیمات نهایی Redis باید فقط endpoint لوکال مورد نظر را داشته باشند. ترکیب آرایهٔ Hosts را مطابق فصل ۱۶ بررسی کنید. هر سه پروژه در تنظیمات پکیج، نسخهٔ `1.1.0-preview.28` را مصرف می‌کنند.
برای ماشین دیگر یا CI، همان نسخهٔ پکیج و وابستگی‌های لازم باید در یک feed در دسترس باشند. اشاره‌کردن سورس به یک نسخه، به‌تنهایی به معنی انتشار آن نسخه روی registry نیست.
### اجرای NextBank روی سرور
همهٔ نودهای مرتبط باید نسخه‌های هماهنگ داشته باشند و به همان SQL ورکفلو، Redis مشترک، Database و پیشوندهای مربوط وصل شوند. در ساختار فعلی، فایل پایهٔ `appsettings.json` تنظیمات Sentinel را دارد؛ محیط اجرا و overrideهای نهایی باید صحیح باشند.
اگر دو برنامه به دیتابیس‌های Workflow مستقل در SQL وصل هستند، برای کش و قفل آن‌ها فضای مستقل انتخاب کنید. ممکن است دو دیتابیس مستقل شمارهٔ Revision یکسانی داشته باشند؛ شمارهٔ یکسان به معنی دادهٔ یکسان نیست.
تغییر تعریف، نمونهٔ در حال اجرای نسخهٔ قبلی را خودکار به نسخهٔ جدید مهاجرت نمی‌دهد. اعتبار کش و مهاجرت اجرای ورکفلو دو موضوع جدا هستند.
## ۲۷. عیب‌یابی با چند نشانهٔ ساده
| نشانه | چه چیزی را بررسی کنیم؟ |
|---|---|
| کلید کش نمی‌بینم | Database، پیشوند، Refresh/Fetch، provider نهایی، Enabled و اینکه خواندن قابل کش انجام شده باشد |
| Redis نصب است ولی کش محلی است | `RegisterRedis` کافی نیست؛ انتخاب `Provider=Redis` یا `UseRedis` را بررسی کنید |
| تنظیم فایل تغییر کرد ولی رفتار ثابت ماند | restart برنامه، callback صریح، منابع تنظیمات دیگر و فایل محیط انتخاب‌شده |
| در لوکال endpointهای سرور هم دیده می‌شوند | ترکیب شماره‌های آرایهٔ Hosts در فایل پایه و Development |
| تعداد کلیدها کمتر از تعریف‌هاست | نتیجهٔ query کش می‌شود؛ همهٔ تعریف‌ها در startup پیش‌بارگذاری نمی‌شوند |
| چند گروه با شماره‌های مختلف می‌بینم | Revisionهای مختلف و کلیدهای قدیمی با TTL باقی‌مانده |
| قفل نمی‌بینم | انجام عملیات محافظت‌شده، زمان کوتاه قفل و Database/پیشوند صحیح |
| فقط کلیدهایی با نتیجهٔ خالی می‌بینم | فیلتر واقعی درخواست، نسخهٔ منتشرشده یا draft و payload را بررسی کنید |
| خواندن جدید کلید ایجاد نمی‌کند | تراکنش جاری، ثبت ناتمام SQL و خطاهای Redis را هم بررسی کنید |
| خطای نبودن سرویس اتصال Redis دارم | ثبت `IConnectionMultiplexer` میزبان یا تنظیم معتبر ConnectionFactory |
| تغییر یک نود برای بقیه قابل مشاهده نیست | یکسان‌بودن SQL، Redis، نسخهٔ پکیج‌ها، Database و پیشوندها |
این جدول نقطهٔ شروع بررسی است. از روی یک نشانه، بدون دیدن تنظیمات نهایی و لاگ‌ها، علت قطعی اعلام نمی‌کنیم.
## ۲۸. اعتبارسنجی این راهنما و محدودهٔ آزمون‌ها
نمونه‌های این راهنما با مقادیر فعلی بخش‌های Redis و Workflow در فایل‌های پایهٔ Web، Admin و Workers تطبیق داده شده‌اند. فایل پایه و Development با Configuration واقعی .NET ترکیب شدند؛ این بررسی شامل اتصال شبکه به Redis یا SQL نیست. مقادیر حساس اتصال SQL در نمونه‌های آموزشی نمایش داده نشده‌اند.
در گزارش آزمون مرحلهٔ قبل با پکیج `preview.27`، سه ServiceProvider و اتصال مستقل ساخته شدند. اشتراک payload کش بین Web و Workers و جلوگیری از گرفتن همان قفل توسط Admin و Workers، تا زمان آزادشدن آن توسط Web، تأیید شد. این گزارش مربوط به پیکربندی همان مرحله است و آزمون اجرایی جدیدِ نسخهٔ `preview.28` یا تنظیمات فعلی محسوب نمی‌شود.
در مرحلهٔ تغییر زیرساخت Core نیز ۲۲ تست کش، ۹ تست Runtime و سناریوهای process مستقل موفق گزارش شدند. آزمون‌های اجرایی آن مرحله Redis ایزوله و SQLite مشترک داشتند. از این نتایج، اجرای migration روی SQL Server عملیاتی، عملکرد واقعی چندسروری یا موفقیت failover Sentinel نتیجه‌گیری نمی‌شود.
برای بررسی اجرای واقعی، تنظیمات نهایی میزبان، لاگ‌ها و آزمون‌های همان محیط را مبنا قرار دهید. جزئیات زیرساخت و آزمون‌ها در [مستند مفصل](workflow-cache-explained.fa.md) آمده است.
## ۲۹. چند تمرین برای اطمینان از فهم موضوع
| سؤال | پاسخ |
|---|---|
| فقط `cache.RegisterRedis()` نوشته‌ام. آیا Redis انتخاب شده؟ | هنوز انتخاب لازم است: `Provider=Redis` در فایل یا `UseRedis` در کد. |
| `runtime.UseCache()` بدون نام Redis دارم. از کجا provider را می‌فهمد؟ | از CacheFeature مشترک همان ماژول و مدیر کش ثبت‌شده. |
| Provider کش را Memory کردم. آیا قفل هم File می‌شود؟ | provider قفل مستقل تنظیم می‌شود. |
| Duration را سی دقیقه کرده‌ام، ولی callback یک ساعت تعیین کرده است. کدام اعمال می‌شود؟ | مقدار صریح callback که بعد از فایل اجرا می‌شود. |
| یک کلید، صفحه‌ای شامل ۲۰ تعریف دارد. آیا برای آن صفحه ۲۰ کلید لازم است؟ | همان نتیجهٔ صفحه می‌تواند در یک کلید ذخیره شود. |
| `LockAcquisitionTimeout` ده ثانیه است. آیا قفل بعد از ده ثانیهٔ عملیات حتماً آزاد می‌شود؟ | این گزینه حداکثر انتظار گرفتن قفل است؛ مدت عملیات را تعیین نمی‌کند. |
| دو node روی دو سرور هرکدام به localhost وصل‌اند. آیا کش مشترک دارند؟ | فقط اگر اتصال‌ها در نهایت به سرویس مشترک هدایت شده باشند؛ localhost عادی هر سرور به خود همان سرور اشاره می‌کند. |
## ۳۰. اگر می‌خواهید کد را بخوانید، از این فایل‌ها شروع کنید
| فایل | چه چیزی در آن ببینیم؟ |
|---|---|
| [Framework DependencyInjection](../Caspian.WorkflowFramework.Package/Config/DependencyInjection.cs) | ترتیب انتخاب کش و فعال‌کردن مصرف‌کننده‌ها |
| [CacheFeature](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Features/CacheFeature.cs) | انتخاب provider، Enabled، Duration و ثبت مدیر کش |
| [CacheConsumerFeature](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Features/CacheConsumerFeature.cs) | اتصال مصرف‌کننده‌ها به تنظیم مشترک |
| [ICacheProvider](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Contracts/ICacheProvider.cs) | قرارداد خواندن، نوشتن و حذف |
| [ProviderCacheManager](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Services/ProviderCacheManager.cs) | Cache Hit/Miss، کنترل اعتبار و ساخت کلید |
| [RedisCacheFeatureExtensions](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching.Redis/Extensions/CacheFeatureExtensions.cs) | تفاوت RegisterRedis و UseRedis |
| [RedisCacheProvider](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching.Redis/Services/RedisCacheProvider.cs) | ذخیرهٔ payload و انقضای Redis |
| [NextBank RegisterCache](../../../NextBankParsian/Repo/src/Utility/Cache/Extensions/DependencyInjection.cs) | ساخت اتصال میزبان از بخش اصلی Redis |
| [Framework helper قفل](../Caspian.WorkflowFramework.Package/Config/WorkflowRuntimeFeatureExtensions.cs) | خواندن تنظیمات قفل، دیتابیس، پیشوند و زمان انتظار |
| [Core host helper قفل](../../../Caspian.Elsa.Core/Repo/src/apps/Elsa.Server.Web/Extensions/WorkflowRuntimeFeatureExtensions.cs) | تنظیم قفل هنگام اجرای مستقیم Core |
برای افزودن provider جدیدِ کش، قرارداد `ICacheProvider` را پیاده‌سازی کنید، نام و روش تنظیم آن را در Feature ثبت کنید و رفتار ذخیره، انقضا و سازگاری با کش مشترک را بررسی کنید. مصرف‌کننده‌های `UseCache` همچنان از مسیر مشترک استفاده خواهند کرد. جزئیات توسعهٔ provider در [راهنمای مفصل](workflow-cache-explained.fa.md) آمده است.