Skip to content

Full Import Flow

Last updated: 2026-09-29, JIM v0.16.0

This diagram shows how objects are imported from a Connected System into JIM's connector space. Both Full Import and Delta Import use the same processor (SyncImportTaskProcessor); the connector handles delta filtering internally via watermark/persisted data.

Since v0.7.1, the import processor uses ISyncServer for orchestration (settings, caching, reconciliation) and ISyncRepository for dedicated bulk data access (CSO writes, RPEIs).

Since #1082, Full Imports keep a stored content hash on each Connected System Object: an unchanged object (incoming hash matches the stored hash and fingerprint) is skipped before hydration and diffing, and hashes are stamped only after each batch's attribute value writes have committed. A Run Profile Verification Mode disables the skip and reports any disagreement between the stored hash and the honest comparison.

Since v0.8.0, LDAP connectors for OpenLDAP/Generic directories import using parallel connections: each container+objectType combination runs on its own dedicated LdapConnection, bypassing RFC 2696 paging cookie limitations (#72). CSO persistence uses two-phase parallel writes when writing large batches (#427). Run Profiles can optionally target a specific partition, filtering which containers are imported (#353).

Since v0.15.0:

  • Run steps on the Activity: the import enters named steps as it goes (RunPhaseKeys: connecting, importing objects with the Connector's own narrated sub-steps and object counts nested inside it, processing deletions, resolving references, saving changes, reconciling Pending Exports, recording results), so an operator sees where a run is rather than a single message.
  • Container Scope exclusions (#1255): a Connector that discards entries under an excluded Container reports how many per Container; the processor accumulates those counts across every page and records them on the Activity (never as a warning, and never failing the run).
  • Deletion-detection limits (#1618, Run Profile Safeguards): a Full Import's deletion detection resolves every candidate first, and refuses outright when the number that would be newly marked as deleted exceeds the Run Profile's MaxDetectedDeletions or MaxDetectedDeletionsPercent. See Deletion Detection below.
  • Unconfirmed Create retry (#1695): after deletion detection, a Full Import finds every exported Create Pending Export whose Pending Provisioning CSO it did not see at all, and marks it for retry, because deletion detection deliberately excludes Pending Provisioning CSOs and reconciliation only visits CSOs an import returned.
  • LDAP Delta Import change sources (#1736): the LDAP Connector reads changes through one change source per directory family (uSNChanged plus the Deleted Objects container for Active Directory and Samba AD, cn=accesslog for OpenLDAP, cn=changelog for other directories), and a Delta Import checks its change source's readiness first: a finding either stops the run (for example a change source it cannot read, #1737) or becomes the Activity's warning (for example deletions it cannot see, #1727), rather than silently importing nothing.

Since v0.16.0:

  • Watermark recorded only once staged (#1868): the watermark a call-based Connector returns with its first page is held back until the run has staged everything it read and recorded its results. A run that fails or is cancelled before then (including a cancelled run that staged nothing) keeps the watermark it started with, so the next Delta Import re-reads what was not staged rather than skipping it. Connector data returned by CloseImportConnection is still persisted even when the run fails, and wins over the page watermark.
  • Deselected Object Types leave management (#1474): a Full Import's deletion detection also walks each deselected Object Type that has an External ID, so its Connected System Objects are marked Obsolete (and disconnected on the next synchronisation). A deselected Object Type an enabled Synchronisation Rule is still bound to is held back, and the Activity's warning names the rules.
  • Confirmation on import only (#1826): import reconciliation is now the only place Pending Exports are confirmed; synchronisation runs no longer re-check them. Reconciliation never touches an Executing Pending Export (one stranded by a crash is recovered at Worker startup), and clears a Failed one only once every change it asserts is on the Connected System Object, otherwise leaving it untouched.
  • Active Directory ranged retrieval (#1853): a multi-valued attribute with more values than the domain controller's MaxValRange (1,500 by default) is answered in ranges (member;range=0-1499, then member;range=1500-* and so on); the LDAP Connector reads every range before converting the attribute, so a large group imports with every member rather than failing as a configuration error. A Delta Import stopped by a changed domain controller invocationId now says the directory was probably restored from a backup or snapshot and that a Full Import re-establishes the baseline.

Overall Import Flow

flowchart TD
    Start([PerformImportAsync]) --> ConnType{Connector<br/>type?}

    %% --- Call-based connector (e.g., LDAP) ---
    ConnType -->|IConnectorImportUsingCalls| InjectServices[Inject CertificateProvider<br/>and CredentialProtection<br/>if connector supports them]
    InjectServices --> OpenConn[OpenImportConnection<br/>with system settings]
    OpenConn --> PartFilter[GetTargetPartitions:<br/>Run Profile partition set?<br/>Use it. Otherwise all selected.]
    PartFilter --> InitPage[initialPage = true<br/>paginationTokens = empty<br/>Capture original PersistedConnectorData]

    InitPage --> PagingCheck{Connection-scoped<br/>paging? OpenLDAP/Generic}
    PagingCheck -->|Yes, parallel path| ParallelImport[Build container+objectType combos<br/>One dedicated LdapConnection per combo<br/>Concurrency capped at ImportConcurrency<br/>See Parallel LDAP Import below]
    ParallelImport --> UpdateProgressP[Update activity progress<br/>Imported N objects]
    UpdateProgressP --> CollectExtIdsP[Add external IDs<br/>to collection]
    CollectExtIdsP --> ProcessPageP[ProcessImportObjectsAsync]
    ProcessPageP --> CloseConn

    PagingCheck -->|No, AD or sequential| PageLoop{More pages?<br/>initialPage OR<br/>tokens present}
    PageLoop -->|Yes| Import[connector.ImportAsync<br/>Pass original persisted data<br/>to ensure consistent watermark]
    Import --> UpdateProgress[Update activity progress<br/>Imported N objects page M]
    UpdateProgress --> CollectExtIds[Add external IDs from this page<br/>to externalIdsImported collection]
    CollectExtIds --> CaptureWatermark{First page with<br/>new persisted data?}
    CaptureWatermark -->|Yes| SaveWatermark[Capture new watermark<br/>Don't save yet - save once staged]
    CaptureWatermark -->|No| ProcessPage
    SaveWatermark --> ProcessPage[ProcessImportObjectsAsync<br/>See Per-Object Processing below]
    ProcessPage --> PassTokens[Pass pagination tokens<br/>for next page]
    PassTokens --> PageLoop

    PageLoop -->|No| CloseConn[CloseImportConnection<br/>Returned connector data? Persist it<br/>and drop the captured watermark]

    %% Cancellation: the current page is finished, no further pages are read, and nothing is staged
    %% Change tracker: hydration is untracked; ClearChangeTracker runs after each CSO update batch

    %% --- File-based connector ---
    ConnType -->|IConnectorImportUsingFiles| FileImport[connector.ImportAsync<br/>Returns all objects at once]
    FileImport --> FileProgress[Update activity:<br/>Processing N objects]
    FileProgress --> FileCollect[Add external IDs<br/>to collection]
    FileCollect --> FileProcess[ProcessImportObjectsAsync]
    FileProcess --> RecordExclusions

    CloseConn --> RecordExclusions[Record entries discarded by<br/>excluded Containers on the Activity #1255<br/>Cancelled? Stop here, nothing persisted]
    RecordExclusions --> PostImport{Full Import<br/>and objects > 0?}

    %% --- Deletion detection ---
    PostImport -->|Yes| DeletionDetection[Deletion Detection<br/>Resolve candidates, check the<br/>Run Profile's deletion limits #1618,<br/>then mark missing CSOs Obsolete or refuse<br/>Scoped to target partition if set<br/>See Deletion Detection below]
    PostImport -->|No, Delta Import<br/>or 0 objects| RefResolution

    DeletionDetection --> RetryCreates[Retry unconfirmed exported Creates #1695<br/>Exported Create Pending Export whose<br/>Pending Provisioning CSO this run did not see:<br/>mark for retry, RPEI: ExportNotConfirmed]
    RetryCreates --> RefResolution[Reference Resolution<br/>Resolve unresolved reference strings<br/>into CSO links by external ID]

    %% --- Persist via ISyncRepository ---
    RefResolution --> PersistCreate[Batch create new CSOs<br/>via ISyncRepository<br/>Two-phase parallel write for large batches<br/>See Two-Phase CSO Persistence below]
    PersistCreate --> PersistUpdate[Batch update existing CSOs<br/>with change objects via ISyncRepository<br/>ClearChangeTracker after each batch]
    PersistUpdate --> StampHashes[Stamp import content hashes #1082<br/>per batch, only AFTER that batch's<br/>attribute value writes committed<br/>never touches LastUpdated]
    StampHashes --> Reconcile

    %% --- Reconciliation ---
    Reconcile[Reconcile Pending Exports<br/>See Confirming Import below]
    Reconcile --> ValidateRpeis[Validate RPEIs<br/>Detect orphaned create RPEIs<br/>with no CSO assigned]
    ValidateRpeis --> PersistRpeis[Flush remaining RPEIs<br/>via raw SQL bulk insert<br/>create/update batches flushed<br/>their own RPEIs as they committed]
    PersistRpeis --> RecordWatermark{Watermark captured<br/>and not overridden<br/>at close?}
    RecordWatermark -->|Yes| PersistWatermark[Update ConnectedSystem<br/>PersistedConnectorData #1868<br/>only now that everything read is staged]
    RecordWatermark -->|No| End
    PersistWatermark --> End([Import Complete<br/>Activity counters and message<br/>describe the whole run])

Per-Object Processing

For each object in an import page, within ProcessImportObjectsAsync:

flowchart TD
    Entry([For each import object]) --> ConnErr{Connector flagged<br/>an error on it?}
    ConnErr -->|Object-level| ConnErrSkip[RPEI: mapped Connector error<br/>Skip object]
    ConnErr -->|Attribute-level| ConnErrKeep[RPEI carries the error<br/>Object still processed]
    ConnErrKeep --> DupAttrs
    ConnErr -->|No| DupAttrs{Duplicate<br/>attribute names?}
    DupAttrs -->|Yes| DupAttrErr[RPEI: DuplicateImportedAttributes<br/>Skip object]

    DupAttrs -->|No| MatchType[Match string object type<br/>to schema ObjectType]
    MatchType --> TypeFound{Object type<br/>found in schema?}
    TypeFound -->|No| TypeErr[RPEI: CouldNotMatchObjectType<br/>Skip object]

    TypeFound -->|Yes| ExtractExtId[Extract external ID value<br/>from import attributes]
    ExtractExtId --> CrossPageDup{Cross-page<br/>duplicate?}
    CrossPageDup -->|Yes| CrossPageErr[RPEI: DuplicateObject<br/>Cross-page duplicate detected<br/>Skip object]

    CrossPageDup -->|No| SameBatchDup{Same-batch<br/>duplicate?}
    SameBatchDup -->|3rd+ occurrence| ThirdDupErr[RPEI: DuplicateObject<br/>Skip object]
    SameBatchDup -->|2nd occurrence| BothDupErr[Error BOTH objects:<br/>Mark current as DuplicateObject<br/>Go back and mark first as DuplicateObject<br/>Remove first CSO from create list<br/>No random winner]

    SameBatchDup -->|First occurrence| TrackExtId[Track external ID<br/>in seenExternalIds]
    TrackExtId --> HashSkip{Content-hash skip? #1082<br/>Full Import, not Verification Mode,<br/>stored hash + fingerprint match,<br/>status Normal, not a delete,<br/>no partition backfill pending}
    HashSkip -->|Yes| SkipObject[Skip hydration and diff<br/>Remove RPEI, count as skipped<br/>Not added to update list<br/>External ID already collected<br/>for deletion detection]
    HashSkip -->|No| CheckDelete{Connector says<br/>Delete?}

    %% --- Delete path ---
    CheckDelete -->|Yes| FindExisting[Find existing CSO<br/>by external ID]
    FindExisting --> ExistsDel{CSO<br/>exists?}
    ExistsDel -->|Yes| AlreadyObsoleteDel{CSO already<br/>Obsolete?}
    ExistsDel -->|No| IgnoreDel[No CSO to delete<br/>Remove RPEI, skip]
    AlreadyObsoleteDel -->|Yes| ReplayedDel[Delete already reported<br/>by an earlier import<br/>Remove RPEI, skip]
    AlreadyObsoleteDel -->|No| MarkObsolete[Set Status = Obsolete<br/>RPEI: Deleted<br/>Add to update list]

    %% --- Create/Update path ---
    CheckDelete -->|No| FindCso[Find existing CSO<br/>by external ID]
    FindCso --> CsoExists{CSO<br/>exists?}

    CsoExists -->|No| CreateCso[Create new CSO<br/>Map all import attributes<br/>to CSO attribute values<br/>RPEI: Added]
    CreateCso --> AddToCreate[Add to create list<br/>Update seenExternalIds<br/>with CSO reference]

    CsoExists -->|Yes| CheckProvisioning{CSO status =<br/>PendingProvisioning?}
    CheckProvisioning -->|Yes| TransitionNormal[Transition to Normal status<br/>Object confirmed in target system]
    CheckProvisioning -->|No| UpdateCso
    TransitionNormal --> UpdateCso[Update CSO attributes<br/>Compare each import attribute<br/>against existing CSO values<br/>Only stage actual changes<br/>usually a no-op for a CSO whose<br/>export was optimistically applied #1079]
    UpdateCso --> HasChanges{Attribute<br/>changes?}
    HasChanges -->|Yes| RpeiUpdated[RPEI: Updated]
    HasChanges -->|No| RpeiNoChange[No RPEI created<br/>CSO still added to update list<br/>for reference resolution]
    RpeiUpdated --> AddToUpdate[Add to update list<br/>Full Import: request hash stamp<br/>Delta Import with changes: request hash clear]
    RpeiNoChange --> AddToUpdate

Deletion Detection (Full Import Only)

Deletion detection runs in two phases (Run Profile Safeguards, #1618): Phase A resolves every candidate with no side effects, so Phase B can decide whether the whole run's worth of detected deletions is within the Run Profile's limits before anything is touched.

flowchart TD
    Start([For each Object Type to check<br/>every selected one, plus each deselected<br/>one with an External ID #1474<br/>held back if an enabled Synchronisation<br/>Rule is bound to it]) --> GetExisting[Get all existing CSO external IDs<br/>for this object type from database<br/>Pending Provisioning CSOs excluded<br/>Scoped to target partition if set]
    GetExisting --> GetImported[Get all imported external IDs<br/>for this object type from collection]
    GetImported --> Compare[Except: find CSO external IDs<br/>not in imported set]
    Compare --> Loop{More missing<br/>external IDs?}
    Loop -->|No| NextType([Next object type])
    Loop -->|Yes| FindCso[Find CSO by external ID<br/>and attribute ID]
    FindCso --> Found{CSO found and not<br/>already processed<br/>in this import run?}
    Found -->|No| Drop[Not a candidate<br/>Not found, or ext ID may have<br/>been updated during import]
    Drop --> Loop
    Found -->|Yes| Classify{CSO already<br/>Obsolete?}
    Classify -->|Yes| AlreadyObsolete[Candidate: already Obsolete]
    Classify -->|No| NewlyMarked[Candidate: newly marked]
    AlreadyObsolete --> Loop
    NewlyMarked --> Loop

    NextType -.->|All object types resolved| Limits{Newly marked count above<br/>MaxDetectedDeletions, or share of<br/>in-scope CSOs above<br/>MaxDetectedDeletionsPercent?}
    Limits -->|Yes| Refuse[Refuse: touch nothing<br/>not even stale Pending Export cleanup<br/>Activity.DetectedDeletionsWithheld = count<br/>Warning appended to the Activity]
    Limits -->|No, or no limits set| Apply[Apply each candidate]
    Apply --> ClearPes[Clear stale Pending Exports<br/>export evaluation does not<br/>exclude Obsolete CSOs]
    ClearPes --> WasObsolete{Already<br/>Obsolete?}
    WasObsolete -->|Yes| StillGone[Reported by an earlier import<br/>Awaiting a synchronisation run<br/>No RPEI, no status write]
    WasObsolete -->|No| Obsolete[Set CSO Status = Obsolete<br/>Set LastUpdated = UtcNow<br/>RPEI: Deleted, DeletionDetected outcome<br/>Add to update list]

Refused, not partially applied: when either limit is exceeded, no candidate is marked, because the import that fed detection may itself be wrong (a broken filter or base DN). The warning makes the Activity complete with a warning, and a run that withheld deletions never counts as a successful Full Import for the stranded-value sweep gate (FullImportSuccessEvaluator, #1605). The percentage is measured against the number of CSOs in the run's scope at the start of deletion detection.

Reported once, not once per run: a CSO stays Obsolete until a synchronisation run on its own Connected System deletes it, so every import in between finds it missing again. Only the first records a deletion; the rest change nothing and report nothing, because the object was Obsolete before the run started and Obsolete after. The Pending Export cleanup still runs each time (unless the run is refused): export evaluation does not exclude Obsolete CSOs, so a synchronisation on another Connected System can stage an export against one at any point between imports.

Safety rule: If zero objects were imported, deletion detection is skipped entirely. This prevents accidental mass-deletion when the Connected System returns no data due to connectivity issues.

Delta Import exception: Deletion detection only runs for Full Import. Delta Imports handle explicit deletes via ObjectChangeType.Deleted from the connector (e.g., LDAP tombstone/changelog entries).

Parallel LDAP Import (#72)

For OpenLDAP and Generic LDAP directories, RFC 2696 paging cookies are connection-scoped; starting a new search on the same connection invalidates all outstanding paging cursors. To work around this, the LDAP connector gives each container+objectType combination its own dedicated LdapConnection and runs them concurrently, capped by the Import Concurrency setting (default 4, max 8). Each connection fully drains all pages for its combo before being disposed.

AD directories are unaffected; they support multiple concurrent paged searches on a single connection and continue to use the original multi-combo-per-page logic.

flowchart TD
    Start([OpenLDAP/Generic<br/>directory detected]) --> BuildCombos[Build ordered list of<br/>container+objectType combos<br/>from target partitions]
    BuildCombos --> CheckFactory{Connection factory<br/>available AND<br/>concurrency > 1?}

    CheckFactory -->|No| Sequential[Sequential fallback:<br/>Drain each combo on primary<br/>connection, one at a time]
    Sequential --> Merge

    CheckFactory -->|Yes| Semaphore[Create SemaphoreSlim<br/>capped at ImportConcurrency<br/>default 4, max 8]
    Semaphore --> SpawnTasks[Spawn one Task per combo]

    SpawnTasks --> ComboTask[Each task:<br/>1. Acquire semaphore slot<br/>2. Create dedicated LdapConnection<br/>3. DrainAllPages on that connection<br/>4. Dispose connection<br/>5. Release semaphore]

    ComboTask --> WaitAll[Task.WaitAll<br/>with cancellation support]
    WaitAll --> Merge[Merge ImportObjects<br/>from all combo results]
    Merge --> Done([Return combined result<br/>No pagination tokens;<br/>processor sees single-page result])

Key properties: Each combo runs independently with its own paging cursor. The import processor receives the merged result as a single page (no cross-call pagination tokens). If any combo fails, the exception propagates and the import is aborted.

Two-Phase CSO Persistence (#427)

When the CSO create batch is large enough (>= parallelism x 50), CreateConnectedSystemObjectsAsync partitions the work across N parallel database connections. A two-phase write ensures that cross-partition FK references (e.g., a CSO in partition A referencing a CSO in partition B) succeed without post-hoc fixup.

For small batches, all CSOs and attribute values are written on a single connection in one transaction.

flowchart TD
    Start([CreateConnectedSystemObjectsAsync]) --> PreGen[Pre-generate GUIDs<br/>for all CSOs and attribute values]
    PreGen --> FixupFK[Fixup ReferenceValueId FKs<br/>within this batch +<br/>previously committed batches]
    FixupFK --> SizeCheck{Batch size >=<br/>parallelism x 50?}

    SizeCheck -->|No| SingleConn[Write all CSOs + attributes<br/>on single EF connection]
    SingleConn --> Done([Done])

    SizeCheck -->|Yes| Partition[Partition CSOs across<br/>N parallel connections]
    Partition --> Phase1[Phase 1: INSERT CSO rows<br/>across all partitions<br/>Each partition commits independently]
    Phase1 --> Phase1Done[All CSO rows now visible<br/>to all transactions]
    Phase1Done --> Phase2[Phase 2: INSERT attribute values<br/>across all partitions<br/>ReferenceValueId FKs can target<br/>any CSO in this or prior batches]
    Phase2 --> Done

Why two phases? Without the split, a CSO in partition A could have an attribute referencing a CSO in partition B. If both partitions write concurrently in one transaction, partition A's FK INSERT would fail because partition B's CSO row isn't committed yet. Phase 1 commits all CSO rows first, making them globally visible; Phase 2 then writes attribute values with full FK visibility.

Confirming Import - Pending Export Reconciliation

After CSOs are persisted, the import processor reconciles previously exported changes against the freshly imported values. This is how JIM confirms that exports actually took effect.

flowchart TD
    Start([ReconcilePendingExportsAsync]) --> LoadPE[Bulk fetch Pending Exports<br/>for updated CSOs]
    LoadPE --> Loop{More CSOs<br/>with Pending Exports?}
    Loop -->|No| Summary[Log reconciliation summary:<br/>Confirmed / Retry / Failed]
    Summary --> Done([Done])

    Loop -->|Yes| StatusGate{Pending Export<br/>status? #1826}
    StatusGate -->|Executing, or Pending<br/>with nothing written yet| Untouched[Left untouched<br/>Executing ones stranded by a crash<br/>are recovered at Worker startup]
    Untouched --> Loop
    StatusGate -->|Failed| FailedCheck{Every change it<br/>asserts now on<br/>the CSO?}
    FailedCheck -->|Yes| Delete
    FailedCheck -->|No| Untouched
    StatusGate -->|Exported or ExportNotConfirmed,<br/>or Pending with changes already<br/>written awaiting confirmation #1398| Compare[For each attribute change<br/>awaiting confirmation:<br/>Compare expected value<br/>against CSO current value]
    Compare --> PerChange{Change<br/>confirmed?}
    PerChange -->|Yes| RemoveConfirmed[Remove the confirmed change<br/>from the Pending Export<br/>RPEI: ExportConfirmed outcome]
    PerChange -->|No, retries remain| Retry[Status = ExportedNotConfirmed<br/>RPEI: ExportNotConfirmed<br/>Retried on the next export]
    PerChange -->|No, max retries exceeded| PermanentFail[Status = Failed<br/>RPEI: ExportConfirmationFailed<br/>Manual intervention required]

    RemoveConfirmed --> Remaining{Any changes<br/>remaining?}
    Retry --> Remaining
    PermanentFail --> Remaining
    Remaining -->|No| Delete[Queue Pending Export<br/>for batch deletion<br/>Export fully confirmed]
    Remaining -->|Yes| Update[A Create becomes an Update:<br/>the import proved the object exists<br/>Recompute status, queue for batch update]

    Delete --> Loop
    Update --> Loop

Key Design Decisions

  • Cross-page duplicate detection
    A HashSet<string> tracks external IDs across all pages of a paginated import. This is defence-in-depth against directory servers with faulty paging (e.g., Samba AD).

  • Same-batch duplicate handling
    When duplicates are found within a single page, BOTH objects are rejected (no "random winner" based on file order). This forces data owners to fix the source data.

  • Watermark consistency
    For paginated delta imports, the original persisted connector data (watermark/USN) is passed to every page, ensuring consistent queries. The new watermark from the first page is only saved at the very end of the run, once everything the pages returned has been staged (#1868): a run that fails or is cancelled before then keeps the watermark it started with, so the next Delta Import re-reads what it did not stage rather than skipping it.

  • RPEI list separation
    RPEIs are maintained separately from the Activity during import to avoid EF Core accidentally persisting CSOs before they're ready (EF would follow the Activity -> RPEI -> CSO navigation chain during SaveChanges).

  • Zero-import safety
    If no objects are imported, deletion detection is skipped entirely to prevent accidental mass-deletion when connectivity to the source system fails.

  • PendingProvisioning transition
    CSOs created during export (provisioning) start with PendingProvisioning status. The confirming import transitions them to Normal when the object is confirmed to exist in the target system.

  • Parallel LDAP connections (#72)
    OpenLDAP/Generic directories use connection-scoped RFC 2696 paging cookies, so each container+objectType combo gets its own LdapConnection. Concurrency is capped by the Import Concurrency setting (default 4, max 8). AD directories are unaffected; they multiplex paged searches on a single connection. When the connection factory is unavailable or concurrency is 1, the connector falls back to sequential single-connection processing.

  • Two-phase parallel write (#427)
    CSO persistence splits INSERT into two committed phases (CSO rows first, then attribute values) so that cross-partition FK references (ReferenceValueId pointing to a CSO on a different parallel connection) succeed without post-hoc fixup. Small batches (< parallelism x 50) bypass this and write on a single connection. Write parallelism defaults to Environment.ProcessorCount (minimum 2) and is tuneable via JIM_WRITE_PARALLELISM.

  • Partition-scoped imports (#353)
    Run Profiles can target a specific partition via GetTargetPartitions(). When set, only containers within that partition are imported; otherwise all selected partitions are included. This applies to both the import data collection and deletion detection scope. Deletion detection is scoped to the target partition, so CSOs in other partitions are not incorrectly marked as obsolete.

  • Cancellation safety
    When a cancellation is requested, the page being read is finished and no further pages are requested. Staging happens only after every page has been read, so a run cancelled while reading persists no Connected System Objects at all; one cancelled after deletion detection skips persistence, and one cancelled after saving skips reconciliation. In every case the run's new watermark is not recorded (#1868), so the next Delta Import re-reads anything the cancelled run did not stage.

  • Bounded change tracker
    Existing Connected System Objects are hydrated without tracking (AsNoTrackingWithIdentityResolution, #917), so reading pages adds nothing to the EF Core change tracker. ClearChangeTracker is called after each update batch, and each batch's hydrated attribute values are released once persisted, keeping memory consumption bounded regardless of total import size.

  • Optimistic export apply (#1079)
    No import-side behaviour changed for this feature; it is purely an export-side optimisation (see Pending Export Lifecycle). It changes the practical outcome of the "Update CSO attributes" step above: a CSO whose export was optimistically applied already carries the exported values, so the confirming import's set-diff typically finds nothing to stage, and no Updated RPEI is created for it.