Loading

IncrementalSyncOrchestrator

IncrementalSyncOrchestrator<T> coordinates a full incremental sync workflow with a primary and secondary channel. It handles strategy selection, ingestion routing, alias swapping, reindexing, and cleanup.

When StartAsync is called, the orchestrator bootstraps both channels and determines whether to use reindex or multiplex mode:

No Yes Yes No No Yes StartAsync Run pre-bootstrap tasks Bootstrap primary channel Primary templatehash changed? Multiplex mode Secondary aliasexists? Bootstrap secondary channel Secondary templatehash changed? Reindex mode
  • Reindex mode: both schemas are unchanged and the secondary index exists. Only the primary channel receives writes; the secondary is updated via _reindex after drain.
  • Multiplex mode: a schema has changed or the secondary index doesn't exist. Both channels receive every document simultaneously.
loop Documents StartAsync() Run pre-bootstrap tasks Create + Bootstrap Compare template hash (matches) HEAD secondary alias (exists) Create + Bootstrap IngestStrategy.Reindex TryWrite(doc) TryWrite(doc) CompleteAsync() Drain + Refresh + Apply aliases _reindex (updates: last_updated >= batch timestamp) _reindex + delete script (old docs) _delete_by_query (cleanup primary) Alias swap Alias swap complete Only primary receives writes App Orchestrator Primary Channel Secondary Channel Elasticsearch App Orchestrator Primary Channel Secondary Channel Elasticsearch

In reindex mode:

  1. Only the primary channel receives writes (better performance)
  2. After drain, documents modified since the batch timestamp are reindexed from primary to secondary
  3. Old documents (before batch timestamp) are handled via a delete script
  4. Both channels get their aliases swapped
  5. Old documents are cleaned up from the primary
loop Documents StartAsync() Bootstrap (hash changed) Bootstrap IngestStrategy.Multiplex TryWrite(doc) TryWrite(doc) TryWrite(doc) CompleteAsync() Drain + Refresh + Apply aliases Drain + Refresh + Apply aliases _delete_by_query (old primary docs) complete Schema change detected Both channels receive writes App Orchestrator Primary Channel Secondary Channel Elasticsearch App Orchestrator Primary Channel Secondary Channel Elasticsearch

In multiplex mode:

  1. Both channels receive every document (handles schema differences)
  2. After drain, both channels are refreshed and aliases are applied
  3. Old documents are cleaned up from the primary
var orchestrator = new IncrementalSyncOrchestrator<TEvent>(
    ITransport transport,
    ElasticsearchTypeContext primary,
    ElasticsearchTypeContext secondary
);
		
Property Type Default Description
LastUpdatedField string "last_updated" Elasticsearch field name used for range queries when reindexing updates
BatchIndexDateField string "batch_index_date" Elasticsearch field name used for range queries when deleting old documents
ConfigurePrimary Action<IngestChannelOptions<T>>? null Callback to customize primary channel options before creation
ConfigureSecondary Action<IngestChannelOptions<T>>? null Callback to customize secondary channel options before creation
OnPostComplete Func<OrchestratorContext<T>, CancellationToken, Task>? null Hook that runs after CompleteAsync finishes all operations
Strategy IngestStrategy Read-only The resolved strategy after StartAsync
BatchTimestamp DateTimeOffset Read-only Timestamp captured at orchestrator creation
Method Description
AddPreBootstrapTask(Func<ITransport, CancellationToken, Task>) Add a task that runs before channel bootstrap (returns this for chaining)
StartAsync(BootstrapMethod, string?, CancellationToken) Bootstrap channels and determine strategy
TryWrite(TEvent) Non-blocking write (routes based on strategy)
WaitToWriteAsync(TEvent, CancellationToken) Async write with backpressure
TryWriteMany(IEnumerable<TEvent>) Batch non-blocking write
WaitToWriteManyAsync(IEnumerable<TEvent>, CancellationToken) Batch async write
CompleteAsync(TimeSpan?, CancellationToken) Drain, refresh, reindex/multiplex, alias swap, cleanup
Dispose() Dispose primary and secondary channels

This example mirrors examples/Elastic.Ingest.MultiChannel/:

// Document type
public class KnowledgeArticle
{
    [Id] [Keyword]
    public string Url { get; set; }

    [Text(Analyzer = "standard")]
    public string Title { get; set; }

    [Text(Analyzer = "standard")]
    public string Body { get; set; }

    [ContentHash] [Keyword]
    public string Hash { get; set; }

    [Timestamp] [JsonPropertyName("@timestamp")]
    public DateTimeOffset UpdatedAt { get; set; }
}

// Mapping context with two variants
[ElasticsearchMappingContext]
[Index<KnowledgeArticle>(
    Name = "knowledge-lexical",
    WriteAlias = "knowledge-lexical",
    ReadAlias = "knowledge-lexical-search",
    DatePattern = "yyyy.MM.dd.HHmmss"
)]
[Index<KnowledgeArticle>(
    Name = "knowledge-semantic",
    Variant = "Semantic",
    WriteAlias = "knowledge-semantic",
    ReadAlias = "knowledge-semantic-search",
    DatePattern = "yyyy.MM.dd.HHmmss"
)]
public static partial class ExampleMappingContext;
		
var transport = new DistributedTransport(
    new TransportConfiguration(new Uri("http://localhost:9200"))
);

using var orchestrator = new IncrementalSyncOrchestrator<KnowledgeArticle>(
    transport,
    primary: ExampleMappingContext.KnowledgeArticle.Context,
    secondary: ExampleMappingContext.KnowledgeArticleSemantic.Context
);

// Optional: configure channel options
orchestrator.ConfigurePrimary = opts =>
{
    opts.BufferOptions = new BufferOptions { OutboundBufferMaxSize = 5_000 };
};

// Optional: add pre-bootstrap tasks
orchestrator.AddPreBootstrapTask(async (t, ctx) =>
{
    // Create synonym sets, query rules, etc.
});

// Optional: add post-complete hook
orchestrator.OnPostComplete = async (context, ctx) =>
{
    Console.WriteLine($"Strategy: {context.Strategy}, Batch: {context.BatchTimestamp}");
};

// Start: bootstrap and determine strategy
var strategy = await orchestrator.StartAsync(BootstrapMethod.Failure);
Console.WriteLine($"Using strategy: {strategy}");

// Write documents
foreach (var article in await GetArticles())
    orchestrator.TryWrite(article);

// Complete: drain, reindex/multiplex, alias swap, cleanup
var success = await orchestrator.CompleteAsync(drainMaxWait: TimeSpan.FromSeconds(30));
Console.WriteLine($"Completed: {success}");