Розподілені розгортання

Обслуговування одного документа з кількох вузлів

Коли кілька вузлів обслуговують одні й ті ж документи, вузол, який рендерить документ, не обов'язково є тим вузлом, який відповідає на запити браузера щодо сторінки. Це випуск розділяє цю задачу на дві чіткі частини — write side, який публікує артефакти документа у спільне сховище, та read side, який обслуговує їх з будь-якого вузла.

Якщо ваш застосунок зараз викликає UseDoconutWebFarm(...), ця сторінка є його заміною. Ні UseDoconutWebFarm, ні WebFarmOptions у цьому випуску не існує.

Ваша топологія не змінюється. Вкажіть спільний кореневий шлях на той самий UNC‑шлях, який ви використовуєте сьогодні, і кожен вузол продовжуватиме працювати так, як зараз. Змінюється лише те, що публікація — це операція, яку ви викликаєте, а не побічний ефект рендерингу.

Що замінило middleware web-farm

Раніше один middleware робив усе: він обслуговував попередньо відрендерені зображення з спільної папки і опитував, поки інший вузол ще записував їх. Очікування було сліпим бюджетом — «блокувати до трьох хвилин в надії, що сторінка з'явиться».

Тепер та сама задача — це два виклики, які ви можете бачити і розуміти:

text
node A            OpenDocument request
                      -> PublishAsync(...)   writes pages, document info,
                                             search index and metadata
                                             into  RootPath/{token}/
                      -> returns the token

node B            ?token=...&page=7
                      -> UseDoconutCloud<FileShareCloudHandler>
                      -> reads RootPath/{token}/ and serves the page

Спільна конфігурація, потрібна обом частинам

Публікатор записує за допомогою FileShareConfig, а обробник читання обслуговує його за допомогою такого ж. Якщо RootPath або UsePerTokenSubfolders відрізняються між ними, інший вузол відповідає 404. Створюйте конфігурацію в одному місці і використовуйте її з обох сторін — референсний застосунок зберігає один SharedStore.Config(...) допоміжний засіб з цієї причини:

csharp
internal static class SharedStore
{
    public static FileShareConfig Config(string rootPath, bool asyncPublish) => new()
    {
        Location              = CloudLocation.FileShare,
        RootPath              = rootPath,               // наприклад \\fileserver\doconut
        UsePerTokenSubfolders = true,
        ReadReadyPollMs       = asyncPublish ? 250 : 0,
    };
}

FileShareConfig знаходиться у Doconut.Clouds разом з AzureConfig, AmazonConfig, GoogleCloudConfig, DropBoxConfig, RedisConfig, CDNConfig та FTPConfig. У попередній бібліотеці вони були у Doconut.Configs.Cloud; назви класів не змінилися.

ВластивістьПризначення
LocationCloudLocation.FileShare для UNC/локального сховища
RootPathСпільна папка, до якої може дістатися кожен вузол
UsePerTokenSubfoldersЗберігайте артефакти кожного документа у власній папці {token}
ReadReadyPollMsЯк довго read side чекає на сторінку, яка ще записується; 0 вимикає polling

Write side — публікація документа

Впровадьте DistributedDocumentPublisher і публікуйте замість відкриття локальної сесії. Один виклик рендерить і записує повний набір артефактів — сторінки, інформацію про документ, індекс пошуку та метадані:

csharp
var shareConfig = SharedStore.Config(rootPath, asyncPublish: false);

var documentOptions = new DocOptions { TimeOut = 60 };
var config          = new PdfConfig { AllowSearch = true };

var publishOptions = new DistributedDocumentPublishOptions
{
    CleanUpStagingDirectory = true,
    FirstPagePriority       = true,
};

DistributedDocumentPublishResult result = await _publisher.PublishAsync(
    path, shareConfig, config, documentOptions, publishOptions, ct);

return Content(result.Token);

DistributedDocumentPublishResult містить Token, ArtifactPath, TotalPages та Format. Токен передається браузеру точно так само, як звичайний токен переглядача.

Цей один виклик замінює попередню пару — відкриття документа у локальну сесію та потім вивантаження зображень у спільну папку за допомогою ExportToPng(sharedPath). ExportToPng та ExportToCloud тут не мають еквіваленту.

Сторона читання — обслуговування з спільного сховища

Підключіть обробник FileShare до тієї ж гілки, яку вже викликає клієнт:

csharp
app.UseDoconutResources();

app.MapWhen(
    ctx => ctx.Request.Path.Value?.EndsWith("DocImage.axd", StringComparison.OrdinalIgnoreCase) == true,
    branch => branch.UseDoconutCloud<FileShareCloudHandler>(options =>
    {
        options.Location          = CloudLocation.FileShare;
        options.CloudUploadConfig = SharedStore.Config(rootPath, asyncPublish: false);
    }, "/"));

UseDoconutCloud є загальним у цьому випуску: провайдер — це аргумент типу, а не значення Location, яке перемикається всередині middleware. Обробники названі <Provider>CloudHandler — FileShareCloudHandler, AzureCloudHandler, AmazonS3CloudHandler, GoogleCloudHandler, DropBoxCloudHandler, RedisCloudHandler, FTPCloudHandler та CDNCloudHandler.

Обробник CDN є тільки для читання — він обслуговує те, що вже є на CDN; він не завантажує.

Пріоритет першої сторінки та HTTP 202

FirstPagePriority = true робить сторінку 1 доступною одразу. Пізніші сторінки відповідають HTTP 202 Accepted, доки вони не будуть записані, замість блокування запиту.

Це переносить очікування на клієнта, де воно обмежене та видиме. Увімкніть повторення у віджеті:

javascript
const objViewer = $('#div_ctlDoc').docViewer({
    BasePath:   '/',
    ResPath:    'doconut-res/images',
    retryOn409: true      // повторні спроби 202 (і застарілий 409) відповіді готовності
});

Клієнт, який не повторює запити, показує пошкоджені плитки для сторінок, які ще рендеряться. Бюджет повторень та його типові значення (retryInitialDelayMs, retryBackoffFactor, retryMaxDelayMs, retryMaxAttempts, retryMaxTotalMs) задокументовані у Конфігурація переглядача.

Ця пара — FirstPagePriority плюс опитування сторони читання — замінює попередні налаштування PageWaitTimeSeconds та StartWaitFromPage. Жодне з них більше не існує.

Необов’язково: публікація у фоні

За замовчуванням PublishAsync рендериться перед поверненням. Щоб повернутися одразу після створення токену і дозволити розміщеному воркеру рендерити у ту ж папку, зареєструйте чергу opt-in:

csharp
builder.Services.AddDoconut(options => { /* … */ });
builder.Services.AddDoconutDistributedAsyncPublish();

Потім ставте в чергу замість публікації в рядку та встановіть Async = true у параметрах публікації. Черга обмежена: коли вона переповнюється, EnqueueAsync генерує DistributedPublishQueueFullException. Відповідайте на це за допомогою 503 та заголовка Retry-After, щоб клієнти відступали, а не зависали:

csharp
try
{
    var token = await _publishQueue.EnqueueAsync(
        path, shareConfig, config, documentOptions, publishOptions, ct);
    return Content(token);
}
catch (DistributedPublishQueueFullException)
{
    Response.Headers.RetryAfter = "5";
    Response.StatusCode = 503;
    return Content("The publish queue is full. Please retry.");
}

Без цієї реєстрації нічого не реєструється і публікація залишається синхронною — шлях за замовчуванням ідентичний побітово.

Коли фонова публікація увімкнена, задайте стороні читання ненульове ReadReadyPollMs, щоб вона коротко чекала на сторінку, яку воркер ще не записав.

Безпека між вузлами

Токени, прив’язані до сесії, передбачають, що обслуговуючий вузол бачив початковий запит. У фермі це не так, тому перевірка per-session відхиляла б легітимні запити. Ось чому розподілене довідкове застосування встановлює UnsafeMode = true.

UnsafeMode = true не є моделлю безпеки — це видалення тієї, що більше не застосовується. Замініть її підписаними квитками доступу, які кожен вузол може перевіряти незалежно:

csharp
builder.Services.AddDoconutDistributedDocumentSecurity(options => { /* … */ });

Ніколи не переносіть UnsafeMode = true назад у розгортання з одним вузлом і ніколи не використовуйте його як скорочення для невдалої перевірки сесії на одному вузлі — там правильне рішення — AddSession() / UseSession(). Дивіться Сесії та безпека.

Закриття опублікованого документа

CloseDocument(token) на інжектованому Viewer звільняє локальну сесію. Щоб видалити опубліковані артефакти документа з спільного ресурсу, видавець надає:

text
bool CloseDistributedDocument(FileShareConfig fileShareConfig, string token)

Передайте той самий FileShareConfig, яким документ був опублікований.

Відповідність налаштування за налаштуванням

ПопереднєТепер
UseDoconutWebFarm(new WebFarmOptions { … })PublishAsync(...) на стороні запису плюс UseDoconutCloud<FileShareCloudHandler>(...) на стороні читання
WebFarmOptions.PathFileShareConfig.RootPath
PageWaitTimeSeconds / StartWaitFromPageDistributedDocumentPublishOptions.FirstPagePriority плюс FileShareConfig.ReadReadyPollMs
viewer.OpenDocument(...) потім viewer.ExportToPng(share)один PublishAsync(...) виклик
viewer.ExportToCloud(cloudConfig)PublishAsync(...) з конфігурацією провайдера
UseDoconutCloud(new CloudOptions { Location = … })UseDoconutCloud<THandler>(options => …, pathPrefix)
DocOptions.IsWebfarm / WebfarmPathВсе ще присутні, написані IsWebFarm / WebFarmPath — не потрібні на шляху публікації
Doconut.Configs.CloudDoconut.Clouds

Чекліст

  1. Створіть FileShareConfig в одному допоміжному модулі та викликайте його з обох частин.
  2. Налаштуйте RootPath на спільний ресурс, який ви вже використовуєте.
  3. Публікуйте за допомогою PublishAsync(...); видаліть будь‑які виклики ExportToPng / ExportToCloud.
  4. Обслуговуйте за допомогою UseDoconutCloud<FileShareCloudHandler>(...) на гілці, яку викликає клієнт.
  5. Увімкніть FirstPagePriority і retryOn409 одночасно, або залиште обидва вимкненими.
  6. Замініть перевірку на сеанс підписаними квитками доступу — не розповсюджуйте UnsafeMode = true окремо.
  7. Перевірте, відкривши документ на одному вузлі та запросивши пізню сторінку з іншого.

Чи була ця сторінка корисною?