Export Execution Flow¶
Last updated: 2026-04-02, JIM v0.8.1
This diagram shows how pending exports are executed against connected systems via connectors. The export processor (SyncExportTaskProcessor) uses ISyncServer to delegate to ExportExecutionServer for the core execution logic, and ISyncRepository for bulk data access. Supports batching, parallelism, deferred reference resolution, and retry with backoff.
Export Task Processing¶
flowchart TD
Start([PerformExportAsync]) --> CountPE[Count pending exports<br/>for connected system]
CountPE --> HasExports{Pending exports<br/>> 0?}
HasExports -->|No| NoWork[Update activity:<br/>No exports to process]
NoWork --> Done([Return])
HasExports -->|Yes| CheckConnector{Connector supports<br/>export?}
CheckConnector -->|No| FailActivity[FailActivityWithErrorAsync:<br/>Connector does not support export]
FailActivity --> Done
CheckConnector -->|Yes| CheckCancel{Cancellation<br/>requested?}
CheckCancel -->|Yes| CancelMsg[Update activity:<br/>Cancelled before export]
CancelMsg --> Done
CheckCancel -->|No| Execute[ExportExecutionServer.ExecuteExportsAsync<br/>See Export Execution below]
Execute --> ProcessResult[ProcessExportResultAsync<br/>Create RPEIs for each export:<br/>- Create --> Exported<br/>- Update --> Exported<br/>- Delete --> Deprovisioned<br/>- Failed --> UnhandledError with retry count]
ProcessResult --> CheckContainers{New containers<br/>created during export?}
CheckContainers -->|Yes| AutoSelect[Auto-select new containers<br/>Refresh and select containers<br/>by created external IDs<br/>Ensures they appear in future imports]
CheckContainers -->|No| Done
AutoSelect --> Done
Export Execution (ExportExecutionServer)¶
flowchart TD
Start([ExecuteExportsAsync]) --> Reconcile[Pre-export CREATE to DELETE<br/>reconciliation: cancel contradictory<br/>pairs persisted across sync runs<br/>CREATE+DELETE cancels both<br/>UPDATE+DELETE cancels UPDATE]
Reconcile --> GetExecutable[Get executable pending exports<br/>Database filter: Status, NextRetryAt, ErrorCount<br/>In-memory filter: has exportable attribute changes<br/>Delete exports already exported are skipped]
GetExecutable --> HasExports{Exports<br/>found?}
HasExports -->|No| EmptyResult([Return empty result])
HasExports -->|Yes| CheckPreview{Run mode =<br/>PreviewOnly?}
CheckPreview -->|Yes| PreviewResult[Return export IDs<br/>without executing]
PreviewResult --> Done([Return result])
CheckPreview -->|No| ConnectorType{Connector<br/>export type?}
ConnectorType -->|IConnectorExportUsingCalls| PrepareConnector[Inject CertificateProvider<br/>and CredentialProtection]
PrepareConnector --> OpenExport[OpenExportConnection<br/>with system settings]
OpenExport --> SplitExports[Split into:<br/>- Immediate exports: no unresolved references<br/>- Deferred exports: have unresolved references]
%% --- Immediate exports ---
SplitExports --> HasImmediate{Immediate<br/>exports?}
HasImmediate -->|Yes| BatchImmediate[Create batches<br/>of configurable size]
BatchImmediate --> ParallelCheck{MaxParallelism > 1<br/>and factories provided?}
ParallelCheck -->|Yes| ParallelBatch[Process batches in parallel<br/>Each batch gets own:<br/>- DbContext<br/>- Connector instance<br/>Progress serialised via SemaphoreSlim]
ParallelCheck -->|No| SequentialBatch[Process batches sequentially<br/>Using existing connector + DbContext]
ParallelBatch --> HasDeferred
SequentialBatch --> HasDeferred
HasImmediate -->|No| HasDeferred{Deferred<br/>exports?}
%% --- Deferred exports ---
HasDeferred -->|Yes| BulkFetchRefs[Bulk pre-fetch all<br/>referenced CSOs by MVO IDs<br/>in single query]
BulkFetchRefs --> ResolveRefs[For each deferred export:<br/>Try to resolve MVO references<br/>to target system CSO external IDs]
ResolveRefs --> Resolved{References<br/>resolved?}
Resolved -->|Yes| ExportResolved[Batch export resolved<br/>exports same as immediate]
Resolved -->|No| MarkDeferred[Mark as deferred<br/>Will be retried next run]
HasDeferred -->|No| CaptureContainers
ExportResolved --> CaptureContainers
MarkDeferred --> CaptureContainers
CaptureContainers[Capture created container<br/>external IDs from connector]
CaptureContainers --> CloseExport[CloseExportConnection]
CloseExport --> SecondPass[Second pass: retry deferred<br/>references that may now<br/>be resolvable]
SecondPass --> Done
ConnectorType -->|IConnectorExportUsingFiles| FileExport[File-based export<br/>with batching]
FileExport --> Done
Batch Execution Detail¶
Each batch follows this sequence, whether processed sequentially or in parallel:
flowchart TD
Start([Process batch]) --> MarkExecuting[Mark all exports in batch<br/>as Status = Executing]
MarkExecuting --> CallConnector[connector.ExportAsync<br/>Send batch to connector<br/>Returns List of ExportResult]
CallConnector --> ProcessResults[For each export + result pair]
ProcessResults --> CheckResult{Export<br/>succeeded?}
CheckResult -->|Yes, Create| HandleCreate[Record Exported<br/>Capture new external ID<br/>from ExportResult<br/>Set Status = Exported]
CheckResult -->|Yes, Update| HandleUpdate[Record Exported<br/>Set Status = Exported]
CheckResult -->|Yes, Delete| HandleDelete[Record Deprovisioned<br/>Delete pending export<br/>Delete CSO]
CheckResult -->|Failed| HandleFail[Increment ErrorCount<br/>Set error message<br/>Calculate NextRetryAt<br/>with exponential backoff]
HandleCreate --> Persist
HandleUpdate --> Persist
HandleDelete --> Persist
HandleFail --> CheckMaxRetries{ErrorCount >=<br/>MaxRetries?}
CheckMaxRetries -->|Yes| MarkFailed[Set Status = Failed<br/>Permanent failure<br/>Requires manual intervention]
CheckMaxRetries -->|No| SetRetry[Set Status = ExportNotConfirmed<br/>Set NextRetryAt = backoff time]
MarkFailed --> Persist
SetRetry --> Persist
Persist[Batch persist via ParallelBatchWriter<br/>CSO updates, RPEIs, pending export status<br/>split across N concurrent PostgreSQL connections]
Persist --> CaptureItems[Capture ProcessedExportItems<br/>for RPEI creation by caller]
CaptureItems --> Done([Batch complete])
LDAP Export Consolidation and Chunking¶
For LDAP connectors, individual attribute changes are consolidated and chunked before being sent to the directory server. This ensures RFC 4511 compliance and prevents server rejection of oversized modify requests.
flowchart TD
Input([Pending Export<br/>with attribute changes]) --> Consolidate[ConsolidateModifications:<br/>Group changes by attribute name<br/>and operation type]
Consolidate --> Example1[Example: 200 individual<br/>member Add changes]
Example1 --> Merged[Consolidated into single<br/>DirectoryAttributeModification<br/>with 200 values]
Merged --> CheckSize{Values ><br/>batch size?}
CheckSize -->|No| SingleRequest[Single ModifyRequest<br/>with all values]
CheckSize -->|Yes| Chunk[ChunkModifyRequests:<br/>Split into batches<br/>of configurable size<br/>Default: 100]
Chunk --> MultiRequest[Multiple ModifyRequests<br/>sent sequentially<br/>e.g., 2 requests of 100 values]
SingleRequest --> Send([Send to LDAP server])
MultiRequest --> Send
Batch size is configurable via the "Modify Batch Size" connector setting (default: 100, range: 10-5000).
Parallel Batch Architecture¶
When MaxParallelism > 1, batches are distributed across concurrent tasks. Each task is fully isolated to avoid EF Core thread-safety issues.
flowchart TD
Caller[Export Processor<br/>caller context] --> Semaphore[SemaphoreSlim<br/>MaxParallelism]
Semaphore --> B1[Batch 1<br/>Own DbContext<br/>Own Connector<br/>Re-loads PEs by ID]
Semaphore --> B2[Batch 2<br/>Own DbContext<br/>Own Connector<br/>Re-loads PEs by ID]
Semaphore --> B3[Batch N<br/>Own DbContext<br/>Own Connector<br/>Re-loads PEs by ID]
B1 --> ResultLock[Result Lock<br/>thread-safe aggregation]
B2 --> ResultLock
B3 --> ResultLock
B1 --> ProgressSem[Progress Semaphore<br/>serialised via SemaphoreSlim 1,1<br/>protects caller DbContext]
B2 --> ProgressSem
B3 --> ProgressSem
- Batch IDs are captured before dispatching - each parallel task re-loads its exports from its own DbContext by ID
- Progress reporting is serialised via
SemaphoreSlim(1,1)to protect the caller's shared DbContext - Result aggregation uses a lock for thread-safe counter updates
- Connector instances are created per-batch via factory to avoid shared connection state
Key Design Decisions¶
-
Pre-export CREATE→DELETE reconciliation (#218): Before fetching executable exports,
ReconcileCreateDeletePairsAsyncscans all pending exports for contradictory pairs targeting the same CSO. CREATE+DELETE pairs cancel both (object was never exported), UPDATE+DELETE cancels the UPDATE (deletion makes it redundant). This catches pairs persisted across different sync runs; the flush-time reconciliation inSyncTaskProcessorBasehandles same-page pairs. -
Two-pass export: Exports without unresolved references are executed first (immediate). Exports with unresolved MVO references are deferred, with references bulk-resolved in a single query, then executed in a second pass.
-
Retry with backoff: Failed exports are retried with exponential backoff via
NextRetryAt. AfterMaxRetriesattempts, the export is marked as permanentlyFailed. -
No-net-change detection: Before exports are created during sync, the system checks if the target CSO already has the expected values. This happens upstream in
EvaluateExportRulesWithNoNetChangeDetectionAsync, not during export execution. -
Container auto-selection: When exports create new containers (e.g., OUs in LDAP), their external IDs are captured and auto-selected so they appear in future imports without manual configuration.
-
Preview mode:
SyncRunMode.PreviewOnlyreturns the list of exports that would be processed without executing them, enabling dry-run functionality. -
Per-batch isolation: Each parallel batch gets its own
DbContextand connector instance. EF Core is not thread-safe, so sharing a context across batches would cause data corruption. -
ParallelBatchWriter (#394): The persistence phase of each batch (CSO updates, RPEI persistence, pending export status updates) is split across N concurrent PostgreSQL connections via
ParallelBatchWriter. This parallelises the bulk database writes that were previously sequential, significantly reducing batch persistence time. -
LDAP consolidation: Multiple changes to the same attribute with the same operation type (e.g., 200 individual "member Add" operations) are consolidated into a single
DirectoryAttributeModificationbefore sending to the directory server. This is the correct RFC 4511 pattern and dramatically reduces the number of LDAP modify requests. -
LDAP chunking: Consolidated modifications that exceed the configurable batch size (default: 100) are split into multiple
ModifyRequestobjects sent sequentially. This prevents LDAP server rejection of oversized requests, which is important for large group membership changes.