Distributed Deployments
Serve one document from several nodes
When several nodes serve the same documents, the node that renders a document is not necessarily the node that answers the browser's page requests. This release splits that job into two explicit halves — a write side that publishes a document's artifacts into shared storage, and a read side that serves them from any node.
If your application currently calls UseDoconutWebFarm(...), this page is your replacement. Neither UseDoconutWebFarm nor WebFarmOptions exists in this release.
Your topology does not change. Point the shared root at the same UNC path you use today and every node keeps working the way it does now. What changes is that publishing is an operation you call, instead of a side effect of rendering.
What replaced the web-farm middleware
Previously one middleware did everything: it served pre-rendered images out of a shared folder and polled while another node was still writing them. The wait was a blind budget — "block up to three minutes hoping the page appears".
Now the same job is two calls you can see and reason about:
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 pageThe shared configuration both halves need
The publisher writes with a FileShareConfig and the read handler serves with one. If RootPath or UsePerTokenSubfolders differ between them, the other node answers 404. Build the config in exactly one place and call it from both sides — the reference application keeps a single SharedStore.Config(...) helper for this reason:
internal static class SharedStore
{
public static FileShareConfig Config(string rootPath, bool asyncPublish) => new()
{
Location = CloudLocation.FileShare,
RootPath = rootPath, // e.g. \\fileserver\doconut
UsePerTokenSubfolders = true,
ReadReadyPollMs = asyncPublish ? 250 : 0,
};
}FileShareConfig lives in Doconut.Clouds, together with AzureConfig, AmazonConfig, GoogleCloudConfig, DropBoxConfig, RedisConfig, CDNConfig, and FTPConfig. In the previous library these were in Doconut.Configs.Cloud; the class names did not change.
| Property | Purpose |
|---|---|
Location | CloudLocation.FileShare for a UNC/local share |
RootPath | The shared folder every node can reach |
UsePerTokenSubfolders | Keep each document's artifacts in its own {token} folder |
ReadReadyPollMs | How long the read side waits for a page that is still being written; 0 disables polling |
Write side — publish the document
Inject DistributedDocumentPublisher and publish instead of opening a local session. One call renders and writes the complete artifact set — pages, document info, search index, and metadata:
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(
pathToFile, shareConfig, config, documentOptions, publishOptions, ct);
return Content(result.Token);DistributedDocumentPublishResult carries Token, ArtifactPath, TotalPages, and Format. The token goes to the browser exactly as a normal viewer token does.
This single call replaces the previous pair — opening the document into a local session and then dumping images onto the share with ExportToPng(sharedPath). ExportToPng and ExportToCloud have no equivalent here.
Read side — serve from the share
Mount the FileShare handler on the same branch the client already calls:
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 is generic in this release: the provider is the type argument rather than a Location value switched on inside the middleware. Handlers are named <Provider>CloudHandler — FileShareCloudHandler, AzureCloudHandler, AmazonS3CloudHandler, GoogleCloudHandler, DropBoxCloudHandler, RedisCloudHandler, FTPCloudHandler, and CDNCloudHandler.
The CDN handler is read-only — it serves what is already on the CDN; it does not upload.
First-page priority and HTTP 202
FirstPagePriority = true makes page 1 servable immediately. Later pages answer HTTP 202 Accepted until they are written, instead of blocking the request.
That moves the waiting to the client, where it is bounded and visible. Enable retry on the widget:
const objViewer = $('#div_ctlDoc').docViewer({
BasePath: '/',
ResPath: 'doconut-res/images',
retryOn409: true // retries 202 (and legacy 409) readiness responses
});A client that does not retry shows broken tiles for pages that are still rendering. The retry budget and its defaults (retryInitialDelayMs, retryBackoffFactor, retryMaxDelayMs, retryMaxAttempts, retryMaxTotalMs) are documented in ViewerConfig.
This pair — FirstPagePriority plus the read-side poll — is what replaces the previous PageWaitTimeSeconds and StartWaitFromPage settings. Neither exists any more.
Optional: publish in the background
By default PublishAsync renders before it returns. To return as soon as the token is minted and let a hosted worker render into the same folder, register the opt-in queue:
builder.Services.AddDoconut(options => { /* … */ });
builder.Services.AddDoconutDistributedAsyncPublish();Then enqueue instead of publishing inline, and set Async = true on the publish options. The queue is bounded: when it is saturated, EnqueueAsync throws DistributedPublishQueueFullException. Answer it with a 503 and a Retry-After header so clients back off rather than stall:
try
{
var token = await _publishQueue.EnqueueAsync(
pathToFile, 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.");
}Without this registration nothing is registered and publishing stays synchronous — the default path is byte-for-byte identical.
When background publishing is on, give the read side a non-zero ReadReadyPollMs so it waits briefly for a page that the worker has not written yet.
Security across nodes
Session-bound tokens assume the serving node saw the opening request. In a farm it did not, so the per-session check would reject legitimate requests. That is why the distributed reference application sets UnsafeMode = true.
UnsafeMode = true is not the security model — it is the removal of one that no longer applies. Replace it with signed access tickets, which every node can verify independently:
builder.Services.AddDoconutDistributedDocumentSecurity(options => { /* … */ });Never carry UnsafeMode = true back into a single-node deployment, and never treat it as a shortcut for a failing session check on one node — there the correct fix is AddSession() / UseSession(). See Sessions & Security.
Closing a published document
CloseDocument(token) on the injected Viewer disposes a local session. To remove a document's published artifacts from the share, the publisher exposes:
bool CloseDistributedDocument(FileShareConfig fileShareConfig, string token)Pass the same FileShareConfig the document was published with.
Setting-by-setting mapping
| Previous | Now |
|---|---|
UseDoconutWebFarm(new WebFarmOptions { … }) | PublishAsync(...) on the write side plus UseDoconutCloud<FileShareCloudHandler>(...) on the read side |
WebFarmOptions.Path | FileShareConfig.RootPath |
PageWaitTimeSeconds / StartWaitFromPage | DistributedDocumentPublishOptions.FirstPagePriority plus FileShareConfig.ReadReadyPollMs |
viewer.OpenDocument(...) then viewer.ExportToPng(share) | one PublishAsync(...) call |
viewer.ExportToCloud(cloudConfig) | PublishAsync(...) with the provider's config |
UseDoconutCloud(new CloudOptions { Location = … }) | UseDoconutCloud<THandler>(options => …, pathPrefix) |
DocOptions.IsWebfarm / WebfarmPath | Still present, spelled IsWebFarm / WebFarmPath — not needed on the publish path |
Doconut.Configs.Cloud | Doconut.Clouds |
Checklist
- Build the
FileShareConfigin one helper and call it from both halves. - Point
RootPathat the share you already use. - Publish with
PublishAsync(...); delete anyExportToPng/ExportToCloudcalls. - Serve with
UseDoconutCloud<FileShareCloudHandler>(...)on the branch the client calls. - Turn on
FirstPagePriorityandretryOn409together, or leave both off. - Replace the per-session check with signed access tickets — do not ship
UnsafeMode = truealone. - Verify by opening a document on one node and requesting a late page from another.
Apakah halaman ini membantu?