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

1220 lines
103 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.
# راهنمای آموزشی تغییرات کش ورکفلو: Memory، Redis و اجرای چندنودی
برای شروع از مفاهیم پایه و توضیح مرحله‌به‌مرحلهٔ `UseCaching` و `UseCache`، [آموزش مخصوص مبتدی‌ها](workflow-cache-beginner.fa.md) را بخوانید. این سند جزئیات فنی و فهرست تغییرات را ادامه می‌دهد.
**مخاطب:** توسعه‌دهنده‌ای که تازه با کش، Redis، تنظیم سرویس‌ها و اجرای چندنودی آشنا شده است.
**تاریخ آغاز مستند:** ۷ اکتبر ۲۰۲۶. **آخرین تطبیق تنظیمات و راهنمای آموزشی با سورس:** ۱۰ اکتبر ۲۰۲۶.
**مبنای متن:** کد نهایی موجود در ریپوها، فایل‌های تنظیمات، خروجی پکیج‌ها و گزارش تست‌ها. مثال‌های عددی برای آموزش هستند؛ شمارهٔ واقعی نسخهٔ اعتبار کش ممکن است متفاوت باشد.
**نسخه‌های فعلی سورس و مصرف:** ماژول‌های تغییرکردهٔ Core برابر `1.1.0-preview.10` و پکیج WorkflowFramework برابر `1.1.0-preview.28` هستند.
این فایل توضیح می‌دهد چه چیزی تغییر کرده، چرا تغییر کرده، چگونه تنظیم می‌شود و چه محدودیت‌هایی دارد. راهنمای کوتاه عملیاتی در [workflow-cache.md](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` است.
برای آموزش از ابتدا، [راهنمای مبتدی](workflow-cache-beginner.fa.md) را بخوانید. در ادامه، قراردادها، اعتبار 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 باقی می‌مانند.
## ۳. نقش پروژه‌ها در این مسیر
```mermaid
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 دو فراخوانی وجود داشت:
```csharp
management.UseCache();
runtime.UseCache();
```
اما مدیر کش Elsa، یعنی `CacheManager`، مستقیماً از `IMemoryCache` استفاده می‌کرد. بنابراین فعال‌کردن `UseCache` کش را در حافظهٔ همان process قرار می‌داد.
در NextBank نیز `RegisterCache` اتصال Redis و سرویس کش عمومی خود NextBank را ثبت می‌کرد. آن سرویس با مدیر کش Elsa یکی نبود. داشتن یک تنظیم به نام `Redis` در فایل تنظیمات، به‌خودی‌خود Elsa را Redis-backed نمی‌کرد.
```mermaid
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 ساخته می‌شوند.
```mermaid
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 پیاده‌سازی مناسب را انتخاب می‌کند و سرویس‌های مربوط را ثبت می‌کند.
مثال آشنای دیتابیس:
```csharp
management.UseEntityFrameworkCore(ef =>
{
ef.UseSqlServer(connectionString);
});
```
همین ایده برای کش استفاده شده است:
```csharp
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 یک تابع است که می‌گوید «این سرویس را چگونه پیدا یا ایجاد کن». مثلاً:
```csharp
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
ساختار اعلام‌شده برای سرورها به این شکل است:
```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 }
]
}
}
```
این نمونه ساختار ارائه‌شدهٔ شماست؛ اتصال واقعی به این سرورها از این محیط آزمایش نشده است. در تنظیم اولیهٔ شما Port رشته بود؛ در این مثال برای خوانایی به‌صورت عدد آمده است.
`ServiceName` نام سرویسی است که Sentinel برای پیدا کردن Redis اصلی می‌شناسد. Sentinel دادهٔ تعریف‌ها را ذخیره نمی‌کند؛ Redis اصلی را معرفی می‌کند و در تغییر نقش سرورها کمک می‌کند. کد موجود NextBank این تنظیم را برای ساخت `IConnectionMultiplexer` مصرف می‌کند.
### ۸.۲. سیاست کش و قفل ورکفلو
بخش زیر به فایل‌های Web، Admin و Workers اضافه شده است:
```json
{
"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 فعال می‌کنند:
```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:"
}
}
}
}
```
فایل واقعی این تنظیمات [appsettings.Development.json](../../../Caspian.Elsa.Core/Repo/src/apps/Elsa.Server.Web/appsettings.Development.json) است. پروفایل Visual Studio باید محیط `Development` را انتخاب کند و Redis روی `localhost:6379` اجرا باشد. `ServiceName` خالی یعنی اتصال مستقیم به Redis؛ در این نمونه Sentinel استفاده نمی‌شود.
در [Program.cs](../../../Caspian.Elsa.Core/Repo/src/apps/Elsa.Server.Web/Program.cs)، علاوه بر انتخاب provider کش، `runtime.UseConfiguredDistributedLocking(configuration)` فراخوانی می‌شود. [helper تنظیم قفل](../../../Caspian.Elsa.Core/Repo/src/apps/Elsa.Server.Web/Extensions/WorkflowRuntimeFeatureExtensions.cs) تنظیمات قدیمی `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 قفل](../Caspian.WorkflowFramework.Package/Config/WorkflowRuntimeFeatureExtensions.cs) تنظیمات قدیمی `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](../../../NextBankParsian/Repo/src/Web/appsettings.json)، [Admin](../../../NextBankParsian/Repo/src/Admin/appsettings.json) و [Workers](../../../NextBankParsian/Repo/src/Workers/appsettings.json). فایل 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 قابل استفاده است:
```json
{
"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
```csharp
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:";
});
});
```
متد دوپارامتری قبلی نیز باقی مانده است:
```csharp
builder.AddWorkflowFramework(sqlConnectionString);
```
این متد تنظیمات را از فایل میزبان می‌خواند. متد جدید یک callback اضافه دارد؛ callback همان قطعه‌کدی است که اجازه می‌دهد بعضی تنظیمات را صریحاً تغییر دهید.
### ۹.۲. در مصرف مستقیم Core
```csharp
management.UseCache(cache =>
{
cache.UseRedis(redis =>
{
redis.ConnectionFactory =
sp => sp.GetRequiredService<IConnectionMultiplexer>();
redis.Database = 1;
redis.KeyPrefix = "nextbank:workflow:cache:";
});
});
runtime.UseCache();
```
Runtime از انتخاب مشترک همان ماژول استفاده می‌کند. برای حالت Memory:
```csharp
management.UseCache(cache => cache.UseMemory());
runtime.UseCache();
```
در حالت Redis باید Store پایدار اعتبار نیز فراهم باشد. Feature مدیریت EF Core این Store را برای provider مشترک متصل می‌کند. انتخاب Redis روی یک ماژول بدون Store پایدار مناسب، خطای واضح می‌دهد.
### ۹.۳. فایل و کد با هم
برای مصرف مستقیم Core، provider قابل انتخاب از فایل را یک‌بار ثبت می‌کنیم:
```csharp
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` و خواندن فایل، داخل خود پکیج انجام می‌شود؛ مصرف‌کننده لازم نیست این دو مرحله را دوباره بنویسد.
ترتیب معمول اعمال تنظیمات:
```text
پیش‌فرض‌ها → فایل تنظیمات → مقادیر صریح 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. فقط اگر اعتبار عوض نشده و تغییر ناتمام نداریم، نتیجه در کش نوشته می‌شود.
```mermaid
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 دقیقاً چه شکلی دارد؟
پیشوند مورد توافق:
```text
nextbank:workflow:cache:
```
کلید واقعی تقریباً این ساختار را دارد:
```text
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()` فعال بود. در پکیج جدید:
```csharp
.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 تنظیم شده و فضای کلید مستقلی دارد:
```text
nextbank:workflow:cache: ← payload کش
nextbank:workflow:locks: ← قفل‌ها
```
`PrefixedDistributedLockProvider` پیشوند را به نام قفل اضافه می‌کند و ساخت قفل را به provider اصلی واگذار می‌کند.
Publish نیز اگر provider قفل در سرویس‌ها وجود داشته باشد، قفلی با نام منطقی زیر می‌گیرد:
```text
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 زیر فقط وضعیت را می‌خواند:
```sql
SELECT Id, MutationId, Scope, OwnerInstance, CreatedAt
FROM Elsa.CacheMutations
WHERE IsCompleted = 0
ORDER BY Id;
```
`OwnerInstance` کمک می‌کند ماشین و process مربوط به ثبت را پیدا کنیم. بخش تصادفیِ شناسه، اجرای فعلی process را از اجرای بعدی با PID مشابه متمایز می‌کند.
قبل از رفع ثبت باید مشخص شود writer واقعاً پایان یافته، وضعیت دادهٔ اصلی چیست و عملیات هنوز فعال یا InDoubt نیست. گذشت زمان به‌تنهایی اثبات نمی‌کند تغییر تمام شده است.
پس از بررسی، مسیر تکمیل باید از API مدیر کش انجام شود:
```csharp
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 مشترک رعایت کنید.
نمونهٔ مفهومی ثبت انتخاب از فایل:
```csharp
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](../../../Caspian.Elsa.Core/Repo/Directory.Packages.props) | نسخهٔ مرکزی Configuration Binder اضافه شد تا provider بتواند Options را از فایل bind کند |
| [Elsa.sln](../../../Caspian.Elsa.Core/Repo/Elsa.sln) | ماژول Redis و پروژه‌های تست به Solution اضافه شدند |
### ۲۴.۲. هستهٔ کش
مسیر پایهٔ این بخش: `Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching`.
| فایل | تغییر و دلیل |
|---|---|
| [ICacheManager.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Contracts/ICacheManager.cs) | قرارداد انتخاب رفتار مشترک، وضعیت اعتبار، ثبت تغییر و API مستقل از Memory اضافه شد؛ API قبلی حفظ شد |
| [ICacheProvider.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Contracts/ICacheProvider.cs) | قرارداد storage و تشخیص miss از cached null ایجاد شد |
| [ICacheConsistencyStore.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Contracts/ICacheConsistencyStore.cs) | قرارداد نسخهٔ اعتبار و تغییر ناتمام، مستقل از payload ایجاد شد |
| [CacheEntryOptions.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Models/CacheEntryOptions.cs) | تنظیم Absolute، Sliding و Tags به قرارداد مشترک منتقل شد |
| [CacheFeature.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Features/CacheFeature.cs) | انتخاب provider، registry، خواندن فایل، validation و ثبت مدیر مشترک اضافه شد |
| [CacheConsumerFeature.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Features/CacheConsumerFeature.cs) | مصرف‌کننده‌ها به تنظیم مشترک همان ماژول متصل شدند |
| [MemoryCacheFeature.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs) | ثبت حافظهٔ محلی از انتخاب مدیر جدید جدا شد؛ fallback سازگار با TryAdd حفظ شد |
| [ProviderCacheManager.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Services/ProviderCacheManager.cs) | سیاست خواندن، بررسی دوباره، کلید نسخه‌دار، رفتار تراکنش و سازگاری API قبلی متمرکز شد |
| [MemoryCacheProvider.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Services/MemoryCacheProvider.cs) | Memory پشت قرارداد provider قرار گرفت و Tags به expiration token متصل شدند |
| [MemoryCacheConsistencyStore.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Services/MemoryCacheConsistencyStore.cs) | اعتبار و وضعیت ناتمام محلی برای حالت Memory ایجاد شد |
| [Extensions/CacheFeatureExtensions.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Extensions/CacheFeatureExtensions.cs) | ورودی مشترک `UseCaching` برای تنظیم ماژول اضافه شد |
| [CacheMutationExtensions.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Extensions/CacheMutationExtensions.cs) | ترتیب ثبت آغاز، اجرای عملیات و تکمیل آن در helper مشترک قرار گرفت |
| [Elsa.Caching.csproj](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching/Elsa.Caching.csproj) | dependencyهای لازم و نسخهٔ پکیج به‌روزرسانی شدند |
کلاس قبلی `CacheManager` همچنان برای مسیر سازگاری Memory وجود دارد. انتخاب provider جدید از `CacheFeature` انجام می‌شود و لازم نیست نیازهای محلی C# و JavaScript حذف شوند.
### ۲۴.۳. ماژول Redis
مسیر پایه: `src/modules/Elsa.Caching.Redis`؛ این ماژول جدید است.
| فایل | تغییر و دلیل |
|---|---|
| [Elsa.Caching.Redis.csproj](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching.Redis/Elsa.Caching.Redis.csproj) | Redis در پکیج مستقل قرار گرفت |
| [RedisCacheOptions.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching.Redis/Options/RedisCacheOptions.cs) | تنظیم اتصال، دیتابیس و Prefix تعریف شد؛ factory صریح تشخیص داده می‌شود |
| [RedisCacheFeature.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching.Redis/Features/RedisCacheFeature.cs) | سرویس Redis و مالکیت اتصال اختصاصی ثبت و تنظیمات بررسی می‌شوند |
| [Extensions/CacheFeatureExtensions.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching.Redis/Extensions/CacheFeatureExtensions.cs) | `UseRedis` برای Feature مشترک و مصرف‌کننده‌ها و `RegisterRedis` برای فایل ایجاد شد |
| [RedisCacheProvider.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching.Redis/Services/RedisCacheProvider.cs) | storage واقعی، serialization، scriptهای عمر کش و رفتار RedisException پیاده شد |
| [FodyWeavers.xml](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Caching.Redis/FodyWeavers.xml) | تنظیم ساخت ماژول با الگوی ابزارهای موجود پروژه هماهنگ شد |
### ۲۴.۴. مدیریت تعریف‌ها
مسیر پایه: `src/modules/Elsa.Workflows.Management`.
| فایل | تغییر و دلیل |
|---|---|
| [Extensions/ModuleExtensions.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs) | overload تنظیم از IConfigurationSection همراه callback اضافه شد |
| [CachingWorkflowDefinitionsFeature.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Management/Features/CachingWorkflowDefinitionsFeature.cs) | وابستگی به Feature مشترک کش و تنظیم قابل انتخاب اضافه شد |
| [CachingWorkflowDefinitionStore.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs) | API مستقل از Memory، کلیدهای دقیق‌تر و ثبت تغییر دور Save/Delete اضافه شد |
| [CachingWorkflowDefinitionService.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs) | گراف در provider مشترک مستقیماً کش نمی‌شود؛ دادهٔ تعریف با options و tag مشترک خوانده می‌شود |
| [WorkflowDefinitionCacheManager.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionCacheManager.cs) | Tenant به کلیدهای سرویس تعریف افزوده شد |
| [EvictWorkflowDefinitionServiceCache.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Management/Handlers/Notifications/EvictWorkflowDefinitionServiceCache.cs) | ابطال به اعلان‌های پس از Publish، Retract، Delete و Update منتقل شد |
| [WorkflowDefinitionPublisher.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs) | قفلِ در دسترس برای همان تعریف، ثبت دور Publish و تکمیل پس از انتشار موفق اضافه شد |
| [Elsa.Workflows.Management.csproj](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj) | نسخهٔ پکیج تغییر کرد |
### ۲۴.۵. Runtime و Bookmark
مسیر پایه: `src/modules/Elsa.Workflows.Runtime`.
| فایل | تغییر و دلیل |
|---|---|
| [Extensions/ModuleExtensions.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs) | overload تنظیم از فایل همراه callback اضافه شد |
| [CachingWorkflowRuntimeFeature.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Runtime/Features/CachingWorkflowRuntimeFeature.cs) | تنظیم مشترک کش و decorator هماهنگی Bookmark متصل شد |
| [CachingTriggerStore.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs) | تغییرها fenced شدند؛ ابطال پس از عملیات انجام می‌شود؛ صفحه‌بندی و ترتیب در کلید اصلاح شد |
| [BookmarkConsistencyStore.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Runtime/Stores/BookmarkConsistencyStore.cs) | Bookmark در Store اصلی می‌ماند ولی تغییرش نسخهٔ مسیرها را جلو می‌برد |
| [Elsa.Workflows.Runtime.csproj](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj) | نسخهٔ پکیج تغییر کرد |
### ۲۴.۶. EF Core و migration
| فایل | تغییر و دلیل |
|---|---|
| [CacheMutation.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.EntityFrameworkCore/Modules/Management/CacheMutation.cs) | مدل ثبت پایدار و شناسهٔ owner ایجاد شد |
| [Management/DbContext.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.EntityFrameworkCore/Modules/Management/DbContext.cs) | DbSet، mapping، طول ستون‌ها و indexهای مدل اضافه شد |
| [EFCoreCacheConsistencyStore.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.EntityFrameworkCore/Modules/Management/EFCoreCacheConsistencyStore.cs) | خواندن اعتبار از SQL، آغاز مستقل، تکمیل اتمیک و پاک‌سازی امن پیاده شد |
| [WorkflowDefinitionPersistenceFeature.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs) | برای provider مشترک، Store اعتبار EF و برای provider محلی، Store محلی انتخاب شد |
| [Elsa.EntityFrameworkCore.csproj](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.EntityFrameworkCore/Elsa.EntityFrameworkCore.csproj) | نسخهٔ پکیج تغییر کرد |
| [20261007000000_AddCacheMutationFences.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Management/20261007000000_AddCacheMutationFences.cs) | migration جدول SQL Server و indexها اضافه شد |
| [ManagementElsaDbContextModelSnapshot.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs) | snapshot مدل با جدول جدید هماهنگ شد تا migration بعدی آن را دوباره اضافه نکند |
| [Elsa.EntityFrameworkCore.SqlServer.csproj](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.EntityFrameworkCore.SqlServer/Elsa.EntityFrameworkCore.SqlServer.csproj) | نسخهٔ پکیج دارای migration تغییر کرد |
### ۲۴.۷. HTTP
مسیر پایه: `src/modules/Elsa.Http`.
| فایل | تغییر و دلیل |
|---|---|
| [IRouteTable.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Contracts/IRouteTable.cs) | قرارداد Replace snapshot اضافه شد |
| [RouteTable.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Services/RouteTable.cs) | dictionary آماده به‌صورت یک‌باره جایگزین می‌شود |
| [DefaultRouteTableUpdater.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Services/DefaultRouteTableUpdater.cs) | جمع‌آوری کامل مسیرها، حذف مسیرهای قدیمی با Replace و خواندن Tenant-agnostic اضافه شد |
| [RouteTableRevisionRefresher.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Services/RouteTableRevisionRefresher.cs) | نسخهٔ محلی، gate بازسازی و بررسی اعتبار SQL اضافه شد |
| [HttpWorkflowsMiddleware.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs) | پیش از تطبیق مسیر، تازه‌بودن جدول بررسی می‌شود |
| [CachingHttpWorkflowLookupService.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Services/CachingHttpWorkflowLookupService.cs) | DTO قابل ذخیره جای گراف مشترک را گرفت و هنگام مصرف گراف بازسازی می‌شود |
| [HttpFeature.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Features/HttpFeature.cs) | سرویس refresher و وضعیت محلی مشترک در همان process ثبت شد |
| [HttpCacheFeature.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Features/HttpCacheFeature.cs) | تنظیمات کش HTTP به Feature مصرف‌کنندهٔ مشترک وصل شد |
| [Extensions/ModuleExtensions.cs](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Extensions/ModuleExtensions.cs) | overload تنظیم از فایل همراه callback اضافه شد |
| [Elsa.Http.csproj](../../../Caspian.Elsa.Core/Repo/src/modules/Elsa.Http/Elsa.Http.csproj) | نسخهٔ پکیج تغییر کرد |
### ۲۴.۸. تست‌ها و اسکریپت
| فایل | دلیل ایجاد |
|---|---|
| [Elsa.Caching.UnitTests.csproj](../../../Caspian.Elsa.Core/Repo/test/unit/Elsa.Caching.UnitTests/Elsa.Caching.UnitTests.csproj) | پروژهٔ تست رفتار cache و providerها |
| [CacheTestHost.cs](../../../Caspian.Elsa.Core/Repo/test/unit/Elsa.Caching.UnitTests/CacheTestHost.cs) | ساخت hostهای مستقل، Redis ایزوله و Store اعتبار SQLite |
| [CacheProviderTests.cs](../../../Caspian.Elsa.Core/Repo/test/unit/Elsa.Caching.UnitTests/CacheProviderTests.cs) | تست‌های تنظیم، serialization، تراکنش، restart و هم‌زمانی |
| [Elsa.Caching.ProcessTests.csproj](../../../Caspian.Elsa.Core/Repo/test/integration/Elsa.Caching.ProcessTests/Elsa.Caching.ProcessTests.csproj) | برنامهٔ اجرایی برای process مستقل |
| [ProcessTests/Program.cs](../../../Caspian.Elsa.Core/Repo/test/integration/Elsa.Caching.ProcessTests/Program.cs) | حالت‌های نوشتن، خواندن، تغییر، ثبت ناتمام و bypass |
| [Test-WorkflowCache.ps1](../../../Caspian.Elsa.Core/Repo/scripts/Test-WorkflowCache.ps1) | اجرای تکرارپذیر تست‌ها و چند process با فضای کلید تصادفی |
### ۲۴.۹. WorkflowFramework
| فایل | تغییر و دلیل |
|---|---|
| [DependencyInjection.cs](../Caspian.WorkflowFramework.Package/Config/DependencyInjection.cs) | callback تنظیم کش، خواندن فایل، ثبت Redis، فعال‌کردن کش HTTP و انتخاب مستقل قفل اضافه شد |
| [PrefixedDistributedLockProvider.cs](../Caspian.WorkflowFramework.Package/Config/PrefixedDistributedLockProvider.cs) | پیشوند مستقل به نام قفل اضافه می‌شود |
| [Caspian.WorkflowFramework.Package.csproj](../Caspian.WorkflowFramework.Package/Caspian.WorkflowFramework.Package.csproj) | وابستگی Redis و نسخه‌های تغییرکردهٔ Core متصل شدند؛ نسخهٔ پکیج به ۲۶ رسید |
| [NuGet.Config](../NuGet.Config) | feed محلی workspace برای بررسی پکیج‌های تولیدشده اضافه شد؛ منابع قبلی حفظ شدند |
| [workflow-cache.md](workflow-cache.md) | راهنمای کوتاه تنظیم و استقرار ایجاد و متن آن با نتیجهٔ نهایی هماهنگ شد |
| همین فایل | آموزش مفاهیم، رفتار و دلیل تمام تغییرات |
### ۲۴.۱۰. NextBankParsian
| فایل | تغییر و دلیل |
|---|---|
| [Directory.Packages.props](../../../NextBankParsian/Repo/Directory.Packages.props) | مصرف WorkflowFramework روی preview.28 تنظیم شده است |
| [NuGet.Config](../../../NextBankParsian/Repo/NuGet.Config) | feed محلی workspace برای restore نسخهٔ جدید اضافه شد |
| [Web/appsettings.json](../../../NextBankParsian/Repo/src/Web/appsettings.json) | سیاست Redis کش و قفل ورکفلو برای Web اضافه شد |
| [Admin/appsettings.json](../../../NextBankParsian/Repo/src/Admin/appsettings.json) | همان تنظیم هماهنگ برای Admin اضافه شد |
| [Workers/appsettings.json](../../../NextBankParsian/Repo/src/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 کامل نمی‌شود؛ مسیر ذخیره، ترتیب پایان تغییر، تراکنش و اشیای محلی وابسته باید در نظر گرفته شوند.