Related Issue
This was discovered while investigating #96, and reproduces with that exact bug as the trigger, but is a distinct, more general bug in the manifest failure-logging path itself.
Environment
- Tableau Migration SDK version: 6.1.0
- Language: Python
Summary
When resuming a migration from a previous manifest (previousManifest / manifest resume), if an item that previously failed fails again during the resumed run, recording that failure throws an unhandled NullReferenceException from inside the SDK's own manifest logging code. This happens after the real failure has occurred, while the SDK is simply trying to log/record it — so the original failure reason for the item is masked, and the resumed migration run itself may be aborted/disrupted by this secondary crash.
This does not occur on a fresh migration with no previous manifest — only on resume.
Steps to Reproduce
- Run a migration where at least one item fails (in our case, a workbook publish failing with
System.InvalidOperationException: Sequence contains more than one matching element from TableauFileEditor.GetXmlStream() — see linked issue). The manifest records this item with "Status": "Error".
- Resume the migration using that manifest as the previous manifest (
previousManifest parameter).
- Let the item attempt migration again in the resumed run (it will since
ResetStatus() is called on all copied entries — see below) and fail for the same reason (the underlying data issue is unresolved).
- Observe the resumed run crashes with a
NullReferenceException originating from LoggingMigrationManifestContentTypePartition.MigrationFailed, rather than cleanly recording the item's failure as it would on a first run.
Actual Behavior
[ERROR][migration_logger.py] An error occurred during migration. Error: System.NullReferenceException: Object reference not set to an instance of an object.
at Tableau.Migration.Engine.Manifest.Logging.LoggingMigrationManifestContentTypePartition.MigrationFailed(IMigrationManifestEntryEditor entry)
at Tableau.Migration.Engine.Manifest.MigrationManifestEntry.SetFailed(IEnumerable`1 errors)
at Tableau.Migration.Engine.Migrators.Batch.ContentBatchMigratorBase`3.MigrateBatchItemAsync(ContentMigrationItem`1 item, ContentMigrationBatch`2 batch)
at Tableau.Migration.Engine.Migrators.Batch.ParallelContentBatchMigratorBatchBase`3.<>c__DisplayClass2_0.<<MigrateBatchAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at System.Threading.Tasks.Parallel.<>c__53`1.<<ForEachAsync>b__53_0>d.MoveNext()
--- End of stack trace from previous location ---
at Tableau.Migration.Engine.Migrators.Batch.ParallelContentBatchMigratorBatchBase`3.MigrateBatchAsync(ContentMigrationBatch`2 batch)
at Tableau.Migration.Engine.Migrators.Batch.ContentBatchMigratorBase`3.MigrateAsync(ImmutableArray`1 itemBatch, CancellationToken migrationCancel)
at Tableau.Migration.Engine.Migrators.Batch.ContentBatchMigratorBase`3.MigrateAsync(ImmutableArray`1 itemBatch, CancellationToken migrationCancel)
at Tableau.Migration.Engine.Migrators.ContentMigrator`1.MigrateAsync(CancellationToken cancel)
at Tableau.Migration.Engine.Actions.MigrateContentAction`1.ExecuteAsync(CancellationToken cancel)
at Tableau.Migration.Engine.Pipelines.MigrationPipelineRunner.ExecuteAsync(IMigrationPipeline pipeline, CancellationToken cancel)
at Tableau.Migration.Engine.Migrators.Migrator.ExecuteAsync(IMigrationPlan plan, IMigrationManifest previousManifest, CancellationToken cancel)
The item's manifest entry (from the previous manifest, before resume) recorded:
"Errors": [
{
"ClassName": "System.InvalidOperationException",
"Exception": {
"Message": "Sequence contains more than one matching element",
"Type": "System.InvalidOperationException",
"Source": "System.Linq",
...
}
}
]
What's still open: One of entry.Source, error, or error.Data is null at this point for my repro, but I was not able to conclusively identify which via static analysis. Notes from investigation that may help narrow it down:
- The failing item's error is a plain
System.InvalidOperationException, thrown live (fresh) during the retry attempt by TableauFileEditor.GetXmlStream() — not a deserialized/reconstructed exception object at the point of failure, since ResetStatus() clears the old deserialized errors before the retry.
- Confirmed
entry.Source should already be populated and validated (via Guard.AgainstNull/VerifyDeseralization()) at manifest-load time, and would have thrown a different, earlier exception at load time if null — so a null Source reaching this specific line is unexpected but not fully ruled out (e.g. if the item is matched/re-parented via a different code path than the "matching against live source items" path, its Source could still be sourced from the deserialized previous manifest without going through that validation).
- Ruled out
PythonExceptionConverter mishandling as the cause, since it only targets entries whose ClassName is literally PythonException (confirmed the entry in question has ClassName: "System.InvalidOperationException").
Related Issue
This was discovered while investigating #96, and reproduces with that exact bug as the trigger, but is a distinct, more general bug in the manifest failure-logging path itself.
Environment
Summary
When resuming a migration from a previous manifest (
previousManifest/ manifest resume), if an item that previously failed fails again during the resumed run, recording that failure throws an unhandledNullReferenceExceptionfrom inside the SDK's own manifest logging code. This happens after the real failure has occurred, while the SDK is simply trying to log/record it — so the original failure reason for the item is masked, and the resumed migration run itself may be aborted/disrupted by this secondary crash.This does not occur on a fresh migration with no previous manifest — only on resume.
Steps to Reproduce
System.InvalidOperationException: Sequence contains more than one matching elementfromTableauFileEditor.GetXmlStream()— see linked issue). The manifest records this item with"Status": "Error".previousManifestparameter).ResetStatus()is called on all copied entries — see below) and fail for the same reason (the underlying data issue is unresolved).NullReferenceExceptionoriginating fromLoggingMigrationManifestContentTypePartition.MigrationFailed, rather than cleanly recording the item's failure as it would on a first run.Actual Behavior
The item's manifest entry (from the previous manifest, before resume) recorded:
What's still open: One of
entry.Source,error, orerror.Datais null at this point for my repro, but I was not able to conclusively identify which via static analysis. Notes from investigation that may help narrow it down:System.InvalidOperationException, thrown live (fresh) during the retry attempt byTableauFileEditor.GetXmlStream()— not a deserialized/reconstructed exception object at the point of failure, sinceResetStatus()clears the old deserialized errors before the retry.entry.Sourceshould already be populated and validated (viaGuard.AgainstNull/VerifyDeseralization()) at manifest-load time, and would have thrown a different, earlier exception at load time if null — so a nullSourcereaching this specific line is unexpected but not fully ruled out (e.g. if the item is matched/re-parented via a different code path than the "matching against live source items" path, itsSourcecould still be sourced from the deserialized previous manifest without going through that validation).PythonExceptionConvertermishandling as the cause, since it only targets entries whoseClassNameis literallyPythonException(confirmed the entry in question hasClassName: "System.InvalidOperationException").