Skip to content

Commit 3a50780

Browse files
fix: optimize ownership context imports (#358)
1 parent be5d667 commit 3a50780

3 files changed

Lines changed: 65 additions & 9 deletions

File tree

docs/admin-ownership-import.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ If you already saved mapping profiles for the same files, pass them with `-Mappi
281281

282282
During a multi-source join, ShareSurfer now prints progress in phases: selected CSVs, source rows being processed, merged ownership rows, OBS context merge, AD lookup attempts, output files, and the final matched/source-only/ambiguous counts. Long AD-backed imports should continue to print a heartbeat instead of sitting silently after the header interview.
283283

284+
Large project, OBS, path, or group context files can legitimately create many `ownership_context.csv` and `ownership_relationships.csv` rows. Current optimized builds aggregate OBS-only context and apply it by OBS bucket instead of merging every context row into every matching identity as the file is read. If a large context import is crawling for hours, stop the older run and move to the optimized release before trying again.
285+
284286
If an older ShareSurfer run has been silent for a long time, it is safe to stop it with `Ctrl+C` and rerun with the saved `ownership-import.definition.json` or `ownership-enrichment-rerun.ps1`. The import reads CSVs and AD, then writes local output files; it does not modify AD, shares, or permissions.
285287

286288
### Project, OBS, Path, And Group Context Files

src/ShareSurfer/Public/Join-ShareSurferOwnershipSources.ps1

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,6 @@ function Join-ShareSurferOwnershipSources {
245245
$obsContextRows[$obsKey] = $incoming
246246
}
247247

248-
if ($mergedKeysByObsKey.ContainsKey($obsKey)) {
249-
foreach ($existingKey in @($mergedKeysByObsKey[$obsKey].Keys)) {
250-
$mergedRows[$existingKey] = Merge-ShareSurferOwnershipEnrichmentRow -Existing $mergedRows[$existingKey] -Incoming $incoming
251-
}
252-
}
253-
254248
$elapsedSeconds = [int][Math]::Floor($progressClock.Elapsed.TotalSeconds)
255249
$rowIntervalDue = ($ProgressRowInterval -gt 0 -and (($sourceProcessed % $ProgressRowInterval) -eq 0))
256250
$timeIntervalDue = ($ProgressIntervalSeconds -gt 0 -and (($elapsedSeconds - $lastProgressSecond) -ge $ProgressIntervalSeconds))
@@ -312,22 +306,31 @@ function Join-ShareSurferOwnershipSources {
312306
Write-ShareSurferOwnershipImportStatus -Message ('CSV merge complete: {0} merged ownership row(s), {1} OBS-only context row(s).' -f $mergedRows.Count, $obsContextRows.Count) -Quiet:$Quiet
313307

314308
$obsContextProcessed = 0
309+
$obsContextStrongRowMergeCount = 0
310+
$obsContextOrphanRowCount = 0
315311
foreach ($obsKey in @($obsContextRows.Keys)) {
316312
$obsContextProcessed++
317313
$hasMatchingStrongRow = ($mergedKeysByObsKey.ContainsKey($obsKey) -and @($mergedKeysByObsKey[$obsKey].Keys).Count -gt 0)
318314

319-
if (-not $hasMatchingStrongRow) {
315+
if ($hasMatchingStrongRow) {
316+
foreach ($existingKey in @($mergedKeysByObsKey[$obsKey].Keys)) {
317+
$mergedRows[$existingKey] = Merge-ShareSurferOwnershipEnrichmentRow -Existing $mergedRows[$existingKey] -Incoming $obsContextRows[$obsKey]
318+
$obsContextStrongRowMergeCount++
319+
}
320+
}
321+
else {
320322
$mergedRows[$obsKey] = $obsContextRows[$obsKey]
321323
Add-ShareSurferOwnershipObsMergeIndex -Index $mergedKeysByObsKey -ObsKey $obsKey -MergeKey $obsKey
324+
$obsContextOrphanRowCount++
322325
}
323326

324327
$elapsedSeconds = [int][Math]::Floor($progressClock.Elapsed.TotalSeconds)
325328
$rowIntervalDue = ($ProgressRowInterval -gt 0 -and (($obsContextProcessed % $ProgressRowInterval) -eq 0))
326329
$timeIntervalDue = ($ProgressIntervalSeconds -gt 0 -and (($elapsedSeconds - $lastProgressSecond) -ge $ProgressIntervalSeconds))
327330
if ($rowIntervalDue -or $timeIntervalDue -or $obsContextProcessed -eq $obsContextRows.Count) {
328331
$lastProgressSecond = $elapsedSeconds
329-
Write-ShareSurferOwnershipImportProgress -Activity 'ShareSurfer ownership OBS context merge' -Status ('{0}/{1} OBS context row(s)' -f $obsContextProcessed, $obsContextRows.Count) -CurrentOperation ('Merged rows: {0}' -f $mergedRows.Count) -Processed $obsContextProcessed -Total $obsContextRows.Count -Quiet:$Quiet
330-
Write-ShareSurferOwnershipImportStatus -Message ('OBS context merge: processed {0}/{1}; merged rows {2}; elapsed {3}.' -f $obsContextProcessed, $obsContextRows.Count, $mergedRows.Count, (Get-ShareSurferOwnershipElapsedText -Stopwatch $progressClock)) -Quiet:$Quiet
332+
Write-ShareSurferOwnershipImportProgress -Activity 'ShareSurfer ownership OBS context merge' -Status ('{0}/{1} OBS context row(s)' -f $obsContextProcessed, $obsContextRows.Count) -CurrentOperation ('Applied to strong rows: {0}; orphan OBS rows: {1}; merged rows: {2}' -f $obsContextStrongRowMergeCount, $obsContextOrphanRowCount, $mergedRows.Count) -Processed $obsContextProcessed -Total $obsContextRows.Count -Quiet:$Quiet
333+
Write-ShareSurferOwnershipImportStatus -Message ('OBS context merge: processed {0}/{1}; applied to {2} strong row(s); orphan OBS rows {3}; merged rows {4}; elapsed {5}.' -f $obsContextProcessed, $obsContextRows.Count, $obsContextStrongRowMergeCount, $obsContextOrphanRowCount, $mergedRows.Count, (Get-ShareSurferOwnershipElapsedText -Stopwatch $progressClock)) -Quiet:$Quiet
331334
}
332335
}
333336
Write-ShareSurferOwnershipImportProgress -Activity 'ShareSurfer ownership OBS context merge' -Completed -Quiet:$Quiet
@@ -425,6 +428,8 @@ function Join-ShareSurferOwnershipSources {
425428
PotentialServiceAccountCount = @($enrichedRows | Where-Object { [string]$_.PotentialServiceAccount -eq 'True' }).Count
426429
ForbiddenOu = (@($selectedForbiddenOus) -join '; ')
427430
ObsAttribute = $ObsAttribute
431+
ObsContextStrongRowMergeCount = $obsContextStrongRowMergeCount
432+
ObsContextOrphanRowCount = $obsContextOrphanRowCount
428433
AdLookupAttemptCount = $lookupAttemptCount
429434
DirectoryLookupCacheHitCount = $lookupCacheHitCount
430435
ElapsedSeconds = [Math]::Round($progressClock.Elapsed.TotalSeconds, 3)

tests/Invoke-ShareSurferTests.ps1

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2721,6 +2721,55 @@ $tests = @(
27212721
Assert-True (Test-Path -LiteralPath $rerunSummary.RelationshipOutputPath) 'Definition rerun should reproduce relationship output.'
27222722
}
27232723
},
2724+
@{
2725+
Name = 'Join-ShareSurferOwnershipSources defers OBS context fan-out until bucket merge'
2726+
Body = {
2727+
Import-Module $moduleManifest -Force
2728+
$identityPath = Join-Path ([System.IO.Path]::GetTempPath()) ('ShareSurferOwnershipFanoutIdentity-' + [guid]::NewGuid().ToString('N') + '.csv')
2729+
$contextPath = Join-Path ([System.IO.Path]::GetTempPath()) ('ShareSurferOwnershipFanoutContext-' + [guid]::NewGuid().ToString('N') + '.csv')
2730+
$outputPath = Join-Path ([System.IO.Path]::GetTempPath()) ('ShareSurferOwnershipFanoutOutput-' + [guid]::NewGuid().ToString('N') + '.csv')
2731+
$ownershipContextPath = Join-Path ([System.IO.Path]::GetTempPath()) ('ShareSurferOwnershipFanoutContextOut-' + [guid]::NewGuid().ToString('N') + '.csv')
2732+
$relationshipPath = Join-Path ([System.IO.Path]::GetTempPath()) ('ShareSurferOwnershipFanoutRelationships-' + [guid]::NewGuid().ToString('N') + '.csv')
2733+
$manifestPath = Join-Path ([System.IO.Path]::GetTempPath()) ('ShareSurferOwnershipFanoutManifest-' + [guid]::NewGuid().ToString('N') + '.csv')
2734+
2735+
$identityRows = for ($index = 1; $index -le 5; $index++) {
2736+
[pscustomobject]@{
2737+
employee_id = 'E{0:0000}' -f $index
2738+
display_name = 'User {0}' -f $index
2739+
obs = 'CORP.FIN.AP'
2740+
business_unit = 'Finance'
2741+
}
2742+
}
2743+
$contextRows = for ($index = 1; $index -le 10; $index++) {
2744+
[pscustomobject]@{
2745+
obs = 'CORP.FIN.AP'
2746+
project_code = 'AP-{0:0000}' -f $index
2747+
project = 'Accounts Payable {0}' -f $index
2748+
business_unit = 'Finance'
2749+
data_owner = 'Finance Operations'
2750+
}
2751+
}
2752+
$identityRows | Export-Csv -LiteralPath $identityPath -NoTypeInformation -Encoding UTF8
2753+
$contextRows | Export-Csv -LiteralPath $contextPath -NoTypeInformation -Encoding UTF8
2754+
2755+
$summary = Join-ShareSurferOwnershipSources `
2756+
-Path @($identityPath, $contextPath) `
2757+
-OutputPath $outputPath `
2758+
-ContextOutputPath $ownershipContextPath `
2759+
-RelationshipOutputPath $relationshipPath `
2760+
-ManifestOutputPath $manifestPath `
2761+
-IncludeContextGraph `
2762+
-AdLookupMode DirectoryOnly `
2763+
-Quiet `
2764+
-Force
2765+
$rows = @(Import-Csv -LiteralPath $outputPath)
2766+
2767+
Assert-Equal $summary.RowCount 5 'Strong identity rows should remain the output ownership rows.'
2768+
Assert-Equal $summary.ObsContextStrongRowMergeCount 5 'OBS context should be applied once per matching strong row, not once per context row per strong row.'
2769+
Assert-Equal $summary.ObsContextOrphanRowCount 0 'Matching OBS context should not create orphan ownership rows.'
2770+
Assert-True (@($rows | Where-Object { $_.ProjectCode -eq 'AP-0001' }).Count -eq 5) 'Strong rows should still receive aggregated OBS context.'
2771+
}
2772+
},
27242773
@{
27252774
Name = 'Join-ShareSurferOwnershipSources reruns from ownership import definition'
27262775
Body = {

0 commit comments

Comments
 (0)