diff --git a/Caspian.WorkflowFramework.Package/Caspian.WorkflowFramework.Package.csproj b/Caspian.WorkflowFramework.Package/Caspian.WorkflowFramework.Package.csproj index 4ceae86..5ee9b47 100644 --- a/Caspian.WorkflowFramework.Package/Caspian.WorkflowFramework.Package.csproj +++ b/Caspian.WorkflowFramework.Package/Caspian.WorkflowFramework.Package.csproj @@ -1,11 +1,11 @@ - + net9.0 enable enable Caspian.WorkflowFramework - 1.1.0-preview.25 + 1.1.0-preview.28 Behzad Alizadeh Caspian Caspian.WorkflowFramework @@ -23,55 +23,59 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Caspian.WorkflowFramework.Package/Config/DependencyInjection.cs b/Caspian.WorkflowFramework.Package/Config/DependencyInjection.cs index 78905a6..6683551 100644 --- a/Caspian.WorkflowFramework.Package/Config/DependencyInjection.cs +++ b/Caspian.WorkflowFramework.Package/Config/DependencyInjection.cs @@ -1,6 +1,9 @@ -using Caspian.WorkflowFramework.Contracts; +using Caspian.WorkflowFramework.Contracts; using Caspian.WorkflowFramework.Services; using Elsa.Caching.Options; +using Elsa.Caching.Features; +using Microsoft.Extensions.Configuration; +using Caspian.WorkflowFramework.Config; using Elsa.Common.RecurringTasks; using Elsa.Common.Serialization; using Elsa.EntityFrameworkCore.Extensions; @@ -36,6 +39,10 @@ namespace Caspian.WorkflowFramework; public static class DependencyInjection { public static void AddWorkflowFramework(this IHostApplicationBuilder builder, string sqlServerConnectionString) + => builder.AddWorkflowFramework(sqlServerConnectionString, null); + + public static void AddWorkflowFramework(this IHostApplicationBuilder builder, string sqlServerConnectionString, + Action? configureCache) { //var configuration = new ConfigurationBuilder() // .AddJsonFile(appsettingName, optional: false, reloadOnChange: true) @@ -45,7 +52,7 @@ public static class DependencyInjection var sqlDatabaseProvider = Enum.Parse("SqlServer"); const bool useDbContextPooling = false; const bool runEFCoreMigrations = true; - const bool useCaching = true; + const bool useReadOnlyMode = false; const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed; const bool disableVariableWrappers = false; @@ -63,6 +70,13 @@ public static class DependencyInjection builder.Services.AddElsa(elsa => { elsa + .UseCaching(cache => + { + cache.Duration = TimeSpan.FromDays(1); + cache.RegisterRedis(); + cache.UseConfiguration(builder.Configuration.GetSection("Workflow:Cache")); + configureCache?.Invoke(cache); + }) .UseWorkflows(workflows => { workflows.WithDefaultWorkflowExecutionPipeline(pipeline => pipeline.UseWorkflowExecutionTracing()); @@ -81,8 +95,7 @@ public static class DependencyInjection ef.UseSqlServer(sqlServerConnectionString); }); - if (useCaching) - management.UseCache(); + management.UseCache(); management.SetDefaultLogPersistenceMode(LogPersistenceMode.Inherit); management.UseReadOnlyMode(useReadOnlyMode); @@ -106,8 +119,8 @@ public static class DependencyInjection runtime.UseDistributedRuntime(); } - if (useCaching) - runtime.UseCache(); + runtime.UseCache(); + runtime.UseConfiguredDistributedLocking(builder.Configuration); }) .UseCSharp(options => { @@ -128,7 +141,7 @@ public static class DependencyInjection //}); }) // برای ثبت Activityهایی مانند Elsa.SendHttpRequest - .UseHttp() + .UseHttp(http => http.UseCache()) .UseWorkflowContexts(); }); @@ -143,7 +156,7 @@ public static class DependencyInjection builder.Services.Configure(options => { options.InactivityThreshold = TimeSpan.FromSeconds(15); }); builder.Services.Configure(options => options.Ttl = TimeSpan.FromSeconds(10)); - builder.Services.Configure(options => options.CacheDuration = TimeSpan.FromDays(1)); + builder.Services.Configure(options => options.DefaultIncidentStrategy = typeof(ContinueWithIncidentsStrategy)); //builder.Services.AddHealthChecks(); diff --git a/Caspian.WorkflowFramework.Package/Config/PrefixedDistributedLockProvider.cs b/Caspian.WorkflowFramework.Package/Config/PrefixedDistributedLockProvider.cs new file mode 100644 index 0000000..480012f --- /dev/null +++ b/Caspian.WorkflowFramework.Package/Config/PrefixedDistributedLockProvider.cs @@ -0,0 +1,8 @@ +using Medallion.Threading; + +namespace Caspian.WorkflowFramework.Config; + +internal class PrefixedDistributedLockProvider(IDistributedLockProvider provider, string prefix) : IDistributedLockProvider +{ + public IDistributedLock CreateLock(string name) => provider.CreateLock(prefix + name); +} diff --git a/Caspian.WorkflowFramework.Package/Config/WorkflowRuntimeFeatureExtensions.cs b/Caspian.WorkflowFramework.Package/Config/WorkflowRuntimeFeatureExtensions.cs new file mode 100644 index 0000000..f519b27 --- /dev/null +++ b/Caspian.WorkflowFramework.Package/Config/WorkflowRuntimeFeatureExtensions.cs @@ -0,0 +1,44 @@ +using Elsa.Workflows.Runtime.Features; +using Medallion.Threading.Redis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using StackExchange.Redis; + +namespace Caspian.WorkflowFramework.Config; + +internal static class WorkflowRuntimeFeatureExtensions +{ + internal static void UseConfiguredDistributedLocking(this WorkflowRuntimeFeature runtime, IConfiguration configuration) + { + var legacySettings = configuration.GetSection("Runtime:DistributedLocking"); + var settings = configuration.GetSection("Workflow:Runtime:DistributedLocking"); + runtime.DistributedLockingOptions = options => + { + legacySettings.Bind(options); + settings.Bind(options); + }; + + var providerName = settings["Provider"] ?? legacySettings["Provider"]; + switch (providerName?.ToUpperInvariant()) + { + case "REDIS": + { + var database = settings.GetValue("Database") + ?? legacySettings.GetValue("Database") + ?? configuration.GetValue("Redis:Database") ?? -1; + var prefix = settings["KeyPrefix"] ?? legacySettings["KeyPrefix"] ?? "nextbank:workflow:locks:"; + if (database < -1) throw new InvalidOperationException("Workflow lock database must be -1 or a non-negative number."); + if (string.IsNullOrWhiteSpace(prefix)) throw new InvalidOperationException("Workflow lock KeyPrefix must not be empty."); + runtime.DistributedLockProvider = sp => new PrefixedDistributedLockProvider( + new RedisDistributedSynchronizationProvider(sp.GetRequiredService().GetDatabase(database)), prefix); + break; + } + case null: + case "": + case "FILE": + break; + default: + throw new InvalidOperationException($"Unsupported workflow lock provider '{providerName}'. Configure File or Redis."); + } + } +} diff --git a/Caspian.WorkflowFramework.sln b/Caspian.WorkflowFramework.sln index 036f439..893df09 100644 --- a/Caspian.WorkflowFramework.sln +++ b/Caspian.WorkflowFramework.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.13.35818.85 +# Visual Studio Version 18 +VisualStudioVersion = 18.9.12009.208 insiders MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Caspian.WorkflowFramework.Package", "Caspian.WorkflowFramework.Package\Caspian.WorkflowFramework.Package.csproj", "{36F9F180-CF48-46E9-8BA7-DD82BC4BFE61}" EndProject diff --git a/NuGet.Config b/NuGet.Config index 2610c12..583a08c 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -6,9 +6,7 @@ - - - + \ No newline at end of file diff --git a/docs/workflow-cache-beginner.fa.md b/docs/workflow-cache-beginner.fa.md new file mode 100644 index 0000000..729edb8 --- /dev/null +++ b/docs/workflow-cache-beginner.fa.md @@ -0,0 +1,961 @@ +# آموزش کش ورکفلو از ابتدا: از 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(); +``` + +مدیر کش، یعنی `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 قبلی حذف شد؟ + +قبلاً مدت کش را جداگانه ثابت می‌کردیم: + +```csharp +builder.Services.Configure( + 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(); + + 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(); + + 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` ثابت، دلیلش حفظ اولویت تنظیمات فایل و 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) آمده است. diff --git a/docs/workflow-cache-explained.fa.md b/docs/workflow-cache-explained.fa.md new file mode 100644 index 0000000..3696c63 --- /dev/null +++ b/docs/workflow-cache-explained.fa.md @@ -0,0 +1,1219 @@ +# راهنمای آموزشی تغییرات کش ورکفلو: 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() +``` + +این تابع اتصال ثبت‌شدهٔ میزبان را پیدا می‌کند. با هر درخواست، اتصال تازه‌ای نمی‌سازد. + +### اجزای اصلی جدید + +| جزء | وظیفه | دلیل ایجاد | +|---|---|---| +| `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(); + 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(); + 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: +``` + +- `nextbank:workflow:cache:` پیشوند تنظیم‌شده است. +- `v1` نسخهٔ قالب داخلی کلید است. +- `153` در این مثال نسخهٔ اعتبار کش است؛ عدد واقعی از ثبت‌های SQL به دست می‌آید. +- بخش آخر hash نوع کامل داده و کلید منطقی است و در کد به‌شکل ۶۴ کاراکتر hexadecimal ساخته می‌شود. + +در مصرف‌کننده‌های فعلی، کلید منطقی اطلاعات لازم مثل Tenant، نوع نتیجه و ورودی جست‌وجو را دارد. برای بعضی عملیات، نام عملیات، صفحه‌بندی و ترتیب نیز در ساخت hash لحاظ می‌شود. + +این تفاوت‌ها مهم‌اند؛ نتیجهٔ صفحهٔ اول نباید به‌جای صفحهٔ دوم برگردد و `Page` نباید با `Page` یکی فرض شود. به همین دلیل نوع کامل داده به‌جای نام کوتاه آن استفاده شده و ورودی صفحه‌بندی و ترتیب 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: +``` + +این کار انتشارهای همان تعریف را تحت قفل قرار می‌دهد. دور جریان موفق انتشار هم ثبت تغییر نگه‌داری می‌شود و ابطال سرویس تعریف‌ها به اعلان‌های پس از تغییر منتقل شده است. + +اگر 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` دو چیز را جدا نگه می‌دارد: آیا ورودی پیدا شد و مقدار آن چیست. به این ترتیب 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` در پروژهٔ فعلی وجود ندارد؛ این مثال فقط محل اتصال 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 کامل نمی‌شود؛ مسیر ذخیره، ترتیب پایان تغییر، تراکنش و اشیای محلی وابسته باید در نظر گرفته شوند. diff --git a/docs/workflow-cache.md b/docs/workflow-cache.md new file mode 100644 index 0000000..2be2278 --- /dev/null +++ b/docs/workflow-cache.md @@ -0,0 +1,154 @@ +# کش ورکفلو در چند نود؛ راهنمای سریع + +این راهنما برای مرور سریع است. [آموزش مبتدی با مثال و تمرین](workflow-cache-beginner.fa.md) و [جزئیات فنی و سورس](workflow-cache-explained.fa.md) موضوع را کامل‌تر توضیح می‌دهند. + +## خلاصهٔ کاربردی + +| بخش | مسئولیت | +|---|---| +| `UseCaching` | انتخاب مشترک provider، مدت و تنظیمات کش | +| `management.UseCache()` | کش تعریف‌ها و نتایج جست‌وجوی آن‌ها | +| `runtime.UseCache()` | کش Triggerها و هماهنگی تغییرهای مرتبط با Runtime | +| `http.UseCache()` | کش lookup ورکفلو برای HTTP ورودی | +| `DistributedLocking` | هماهنگی عملیات هم‌زمان نودها، مستقل از کش | + +نام Redis در سه `UseCache` لازم نیست تکرار شود؛ مصرف‌کننده‌ها مدیر کش مشترک همان ماژول را دریافت می‌کنند. `RegisterRedis` قابلیت انتخاب را ثبت می‌کند؛ `Provider=Redis` در فایل یا `UseRedis` در کد آن را انتخاب می‌کند. پیش‌فرض عمومی Core برابر Memory است. + +نسخهٔ فعلی Framework در سورس و مصرف NextBank برابر `1.1.0-preview.28` و ماژول‌های تغییرکردهٔ Core برابر `1.1.0-preview.10` هستند. + +## تنظیم فایل پایهٔ NextBank + +در هر سه میزبان Web، Admin و Workers، بخش‌های زیر داخل `appsettings.json` پایه قرار دارند. سایر تنظیمات برنامه مانند اتصال SQL باید حفظ شوند. فایل Production جداگانه‌ای وجود ندارد: + +```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](../../../NextBankParsian/Repo/src/Web/appsettings.json)، [Admin](../../../NextBankParsian/Repo/src/Admin/appsettings.json) و [Workers](../../../NextBankParsian/Repo/src/Workers/appsettings.json). + +اتصال از بخش اصلی `Redis` توسط میزبان ثبت می‌شود. کش و قفل، `IConnectionMultiplexer` میزبان را مصرف می‌کنند. هر process نمونهٔ سرویس خودش را دارد، ولی به Redis مشترک وصل می‌شود. + +مدت کش یک روز و حداکثر انتظار گرفتن قفل Runtime ده ثانیه است. این دو مقدار مدت نگه‌داشتن قفل را تعیین نمی‌کنند. قفل پس از آزادشدن می‌تواند از 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 اعمال نشود. + + +## تنظیم از کد + +در Framework ترتیب اولویت عبارت است از پیش‌فرض، فایل، سپس مقدار صریح callback: + +```csharp +builder.AddWorkflowFramework(connectionString, cache => +{ + cache.Duration = TimeSpan.FromMinutes(30); + cache.UseRedis(redis => + { + redis.ConnectionFactory = + sp => sp.GetRequiredService(); + redis.Database = 1; + redis.KeyPrefix = "nextbank:workflow:cache:"; + }); +}); +``` + +ثبت اتصال میزبان و دریافت اتصال معتبر SQL باید طبق کد برنامه انجام شده باشند. بدون callback، `builder.AddWorkflowFramework(connectionString)` تنظیمات فایل را مصرف می‌کند. + +در داخل Framework تنظیم مشترک چنین است: + +```csharp +.UseCaching(cache => +{ + cache.Duration = TimeSpan.FromDays(1); + cache.RegisterRedis(); + cache.UseConfiguration( + builder.Configuration.GetSection("Workflow:Cache")); + configureCache?.Invoke(cache); +}) +``` + +ثبت ثابتِ جداگانهٔ `Configure` حذف شده تا مقدار فایل یا callback دوباره به یک روز برنگردد. خود CacheFeature مقدار نهایی را در Options ثبت می‌کند. `runtime.UseCache` قبلاً هم فعال بود؛ شرط ثابت `useCaching=true` حذف شد و کنترل payload از `Enabled` انجام می‌شود. + +برای Memory، `Workflow:Cache:Provider` را `Memory` قرار دهید. با `Enabled=false`، خواندن و نوشتن payload کش کنار گذاشته می‌شود؛ هماهنگی تغییرات در حالت Redis ادامه دارد. این تغییرها provider قفل را خودکار عوض نمی‌کنند. پس از تغییر provider، برنامه را دوباره اجرا کنید. + +## داده و اعتبار + +تعریف‌ها، نتایج جست‌وجوی تعریف‌ها، Triggerها و اطلاعات قابل ذخیرهٔ lookup HTTP می‌توانند در Redis باشند. گراف اجرایی، کد کامپایل‌شده، اسکریپت و قالب آمادهٔ اجرا در process ساخته یا نگه‌داری می‌شوند. Bookmarkها، وضعیت اجرا و لاگ‌ها در SQL باقی می‌مانند. + +جدول `Elsa.CacheMutations` اعتبار کش را در SQL ثبت می‌کند. خواندن نسخهٔ جدید باعث استفاده از کلیدهای آن نسخه می‌شود. هنگام تغییر ناتمام یا تراکنش جاری، مسیر مربوط از payload کش عبور می‌کند. migration آمادهٔ SQL Server، `20261007000000_AddCacheMutationFences` است؛ schema باید پیش از مصرف کش مشترک فراهم باشد. + +تعداد کلیدهای Redis با تعداد تعریف‌ها برابر نیست. هر کلید می‌تواند نتیجهٔ یک query، یک صفحه شامل چند تعریف یا نتیجهٔ خالی باشد. کلیدها هنگام خواندن ایجاد می‌شوند و کلیدهای Revision قدیمی تا پایان TTL ممکن است باقی بمانند. + +برای چندنودی، SQL ورکفلو، Redis، دیتابیس Redis و پیشوندهای میزبان‌های مرتبط باید هماهنگ باشند. کش عمومی NextBank و Redis Streamها مصرف‌کننده‌های جدا هستند، هرچند ممکن است همان اتصال Redis را استفاده کنند. + +## بررسی و محدودیت نتایج + +نمونه‌های JSON و لینک‌های سورس در بازسازی این مستند بررسی شده‌اند. فایل‌های پایه و Development با Configuration واقعی .NET ترکیب شده‌اند؛ این بررسی اتصال شبکه یا اجرای migration نیست. + +گزارش آزمون قبلی با `preview.27` اشتراک payload و رفتار قفل را بین سه ServiceProvider مستقل تأیید کرده است. آزمون‌های زیرساخت Core نیز شامل تست کش، Runtime و process مستقل بوده‌اند. این نتایج مربوط به همان مراحل‌اند؛ آزمون اجرایی جدید `preview.28`، اتصال واقعی Sentinel و failover سرورها در این بازسازی انجام نشده‌اند.