Skip to content

'NullReferenceException' in LoggingMigrationManifestContentTypePartition.MigrationFailed() when an item fails again on a resumed migration (masks the real error) #97

Description

@jyoung-slalom

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

  1. 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".
  2. Resume the migration using that manifest as the previous manifest (previousManifest parameter).
  3. 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).
  4. 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").

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions