Skip to content

[Migration Engine Part 1] Implement the foundation for migration engine - #5894

Draft
warwickschroeder wants to merge 11 commits into
masterfrom
warwick/migration-engine-1
Draft

warwickschroeder wants to merge 11 commits into
masterfrom
warwick/migration-engine-1

Conversation

@warwickschroeder

@warwickschroeder warwickschroeder commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

What this adds

The foundation for moving an error instance's data from RavenDB to SQL Server or PostgreSQL. Nothing is copied yet, and the migration is not switched on anywhere.

  • A read-only RavenDB source. RavenMigrationSource opens both RavenDB databases, embedded or external, through a client that refuses anything outside a read allow-list (OnBeforeRequest, non-tracking sessions), so a patch, a bulk insert or a HiLo range throws in-process rather than reaching the database. It can describe itself and count every collection; Count, Read and ReadBody for a category all throw NotSupportedException, so no reader exists yet.
  • --migration-source-report. Prints the source's RavenDB version, where it is, both database names with the setting each came from, and a row count per collection. It reads the instance's own RavenDB settings; ServiceControl/Migration/SourcePersistenceType defaults to RavenDB, and Settings.MigrationSourcePersistenceType now appears in the approved platform sample settings.
  • Two persisters in one process. PersistenceFactory.CreateMigrationSource loads the source persister in its own AssemblyLoadContext, beside the configured one, and OpenMigrationSource opens it.
  • The migration engine, against fakes. Store-neutral seams in ServiceControl.Persistence/DataMigration (IMigrationSource, IMigrationTarget, IMigrationCheckpointStore, the category registry) and MigrationEngine, which copies a category in batches with checkpoint resume, ordering between categories, body read retries, throttling, skip reasons and a halt threshold. A checkpoint carries a Version and the target extends it inside the transaction that writes the rows, so a save that loses a race throws MigrationCheckpointConflictException and leaves the other writer's row standing rather than halting the category. There is no SQL target yet; it runs against in-memory fakes.
  • Docs and CI. docs/migration/ holds the overview, a system design diagram, and short instructions for what can be run today, linked from the README. .github/workflows/ci.yml gains a Migration test category, which also downloads the RavenDB server.

Tests

  • ServiceControl.UnitTests/Migration: engine, registry, options and the fakes.
  • ServiceControl.UnitTests/Hosting: --migration-source-report argument parsing.
  • ServiceControl.UnitTests/ApprovalFiles: the platform sample settings, which gained the new setting.
  • ServiceControl.Persistence.Tests.RavenDB/DataMigration: the read-only source lifecycle.
  • ServiceControl.Migration.Tests (new): the source report command, and both persisters loaded in one process.

…n, clarifying strategies, goals, and migration limitations.
…on source reporting

- Implemented MigrationEngineFailurePathTests to validate behavior during write failures and halts.
- Created MigrationEngineHaltTests to ensure categories halt correctly on systemic failures.
- Added MigrationEngineOptionsTests to verify default and environment variable configurations.
- Developed MigrationEngineOrderingTests to check execution order of migration categories.
- Introduced MigrationEngineResumeTests to confirm no duplicates or gaps after restarts.
- Added MigrationEngineRunCategoriesTests to ensure all categories run in specified order.
- Implemented MigrationEngineSkipReasonTests to validate skip reasons and their aggregation.
- Enhanced MigrationEngineThrottleTests to verify pause behavior for optional categories.
- Updated MigrationSourceReportCommand for improved output and clarity.
- Modified Help.txt and HostArguments.cs for better command descriptions.
- Refined Settings.cs and PersistenceFactory.cs for clearer migration source configuration.

@rbev rbev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review - adding as comments for now.

| `ServiceControl/RavenDB/ClientCertificatePath` or `ServiceControl/RavenDB/ClientCertificateBase64`, with `ServiceControl/RavenDB/ClientCertificatePassword` | `SERVICECONTROL_RAVENDB_CLIENTCERTIFICATEPATH` and so on | A secured external server's client certificate |
| `ServiceControl/ErrorRetentionPeriod` | `SERVICECONTROL_ERRORRETENTIONPERIOD` | Required. Don't change it during the move |

`ServiceControl/Migration/SourcePersistenceType` defaults to `RavenDB` and needs no setting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So given the abstractions does this technically allow two way migration? or will that be missing the migrationsource implementation?

If this defaults to the only permissible value should it even be a configuration knob?

@warwickschroeder warwickschroeder Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The abstraction is designed to allow for possible "any direction" migrations, but there will not be an implementation for SQL as the source or Raven as the target for this version.

Yep - I'll remove that setting as it is redundant at the moment. I did think about removing this..

Comment thread docs/migration/ravendb-to-sql-migration-overview.md
Comment thread docs/migration/ravendb-to-sql-migration-overview.md Outdated
Comment thread docs/migration/ravendb-to-sql-migration-overview.md Outdated
Comment thread src/ServiceControl.Persistence/DataMigration/IMigrationTarget.cs
Comment thread src/ServiceControl.Persistence/DataMigration/IMigrationTarget.cs Outdated

var result = await target.Write(category, batchToWrite, checkpointAfterBatch, cancellationToken);

checkpoint = checkpointAfterBatch with

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the Write call writes checkpointAfterBatch as given (as described in the interface specification) then how do these extra counts that are being added on from the result survive a crash/reboot before the next batch commits?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 909e4bf#diff-14df616bb97776e63067960a909ea5a788d33570699a00737b6053d4708b9f34.

I've added a sequence diagram to the overview doc to clearly show what the migration engine does and how the checkpoint/resume functionality works.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added more tests for all paths and edge cases of the checkpoint/resume logic

processedThisRun += bodySkips + result.Copied + result.Skipped + result.AlreadyPresent;
skippedThisRun += bodySkips + result.Skipped;

if (HaltThreshold.Exceeded(skippedThisRun, processedThisRun, options.HaltThresholdPercent, options.HaltThresholdMinimum))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Halt threshold is based on percent of progress rather than percent of total, does this cause a problem if there are a small number of invalid records at the start of the migration that would otherwise be ok?

@warwickschroeder warwickschroeder Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small number cannot halt anything, because the floor gates the percentage entirely. But a cluster of more than 100 bad rows near the start does halt a category, which is probably correct.

One bug I did notice though is that legitimate skips (past retention, orphaned group comments, etc) are being counted towards a halt. These types of skips should not count towards it. Will fix.

I've also added a section to the overview doc with a diagram to illustrate this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have also added some more tests to test all paths of the halting logic

@warwickschroeder
warwickschroeder added this pull request to stack #5898 September 16, 2026 04:25
@warwickschroeder warwickschroeder changed the title Implement the foundation for migration engine [Migration Engine Part 2] Implement the foundation for migration engine Sep 16, 2026
@warwickschroeder warwickschroeder changed the title [Migration Engine Part 2] Implement the foundation for migration engine [Migration Engine Part 1] Implement the foundation for migration engine Sep 16, 2026
Comment thread src/ServiceControl.UnitTests/Migration/MigrationEngineCategorySelectionTests.cs Outdated
Comment thread src/ServiceControl.UnitTests/Migration/MigrationEngineCategorySelectionTests.cs Outdated

@johnsimons johnsimons left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few question and comments but overall, lets keep going

Comment on lines +14 to +19
| `ServiceControl/RavenDB/ConnectionString` | `SERVICECONTROL_RAVENDB_CONNECTIONSTRING` | An external RavenDB server. Leave unset for an embedded database |
| `ServiceControl/DbPath` | `SERVICECONTROL_DBPATH` | The embedded database's data directory |
| `ServiceControl/RavenDB/DatabaseName` | `SERVICECONTROL_RAVENDB_DATABASENAME` | The primary database, `primary` by default |
| `LicensingComponent/RavenDB/ThroughputDatabaseName` | `LICENSINGCOMPONENT_RAVENDB_THROUGHPUTDATABASENAME` | The throughput database, `throughput` by default |
| `ServiceControl/RavenDB/ClientCertificatePath` or `ServiceControl/RavenDB/ClientCertificateBase64`, with `ServiceControl/RavenDB/ClientCertificatePassword` | `SERVICECONTROL_RAVENDB_CLIENTCERTIFICATEPATH` and so on | A secured external server's client certificate |
| `ServiceControl/ErrorRetentionPeriod` | `SERVICECONTROL_ERRORRETENTIONPERIOD` | Required. Don't change it during the move |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So are these exclusively being used for the migration? And if so, should then be something like ServiceControl/Migration\....?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These just list the current settings for raven, which the migration engine uses to set its "Source", but yes, any new setting thats migration specific will be "ServiceControl/Migration/xxxxx"

- The fifteen RavenDB index definitions, which map to a much smaller set of ordinary SQL indexes, and two of which are dead already
- The transient in-flight collections, which are empty when nothing is running: `RetryBatches`, `RetryBatchNowForwardings`, `FailedMessageRetries`, `ArchiveOperations` and `UnarchiveOperations`
- `ArchiveBatches` and `UnarchiveBatches`, which exist only because of how RavenDB works
- `ConnectedApplications`, which only versions 6.0 and 6.1 wrote and nothing has read since

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used for when the user is using the MassTransitConnector and this data is passed to ServicePulse to toggle features on/off

Comment thread docs/migration/ravendb-to-sql-migration-overview.md Outdated
- The transient in-flight collections, which are empty when nothing is running: `RetryBatches`, `RetryBatchNowForwardings`, `FailedMessageRetries`, `ArchiveOperations` and `UnarchiveOperations`
- `ArchiveBatches` and `UnarchiveBatches`, which exist only because of how RavenDB works
- `ConnectedApplications`, which only versions 6.0 and 6.1 wrote and nothing has read since
- Integration events still waiting to be sent when you switch over are never sent

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anyway to mitigate this?

### Optional

- Archived and resolved failed messages: the biggest category by far, and most of the copying time
- The event log, without which the ServicePulse activity feed starts empty

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I still see beneficial of bringing in at least the last 7 days or something like that


- Archived and resolved failed messages: the biggest category by far, and most of the copying time
- The event log, without which the ServicePulse activity feed starts empty
- Custom checks, which cost almost nothing to skip because every check re-reports on its next interval

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this related to heartbeats?
I am asking becasue there is no mention of those?
One thing to watch out, is an instance is down, we will not get a heartbeat so we want to ensure that we somehow assume that everything is ok.

- Archived and resolved failed messages: the biggest category by far, and most of the copying time
- The event log, without which the ServicePulse activity feed starts empty
- Custom checks, which cost almost nothing to skip because every check re-reports on its next interval
- Failed error imports, the record of errors that could not be ingested

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know is we can ignore these at all, at the end of the day they are error messages that could not be ingested, we may need to notify the customer about them!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth just marking them as errors in the pre-flight and force them to resolve before migrate!

- The event log, without which the ServicePulse activity feed starts empty
- Custom checks, which cost almost nothing to skip because every check re-reports on its next interval
- Failed error imports, the record of errors that could not be ingested
- Group comments, **copied last of everything**, after archived and resolved messages. A comment survives only once the failed messages its group is built from have arrived, so on a large archive the comments are the last thing to appear. An empty comment field partway through a migration is the copy still running, not data loss

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is important to bring in, as soon as we create the group, becasue there may be info there about informing things like "do not retry this group...."

- Custom checks, which cost almost nothing to skip because every check re-reports on its next interval
- Failed error imports, the record of errors that could not be ingested
- Group comments, **copied last of everything**, after archived and resolved messages. A comment survives only once the failed messages its group is built from have arrived, so on a large archive the comments are the last thing to appear. An empty comment field partway through a migration is the copy still running, not data loss
- Failed message edits

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class EditFailedMessagesDataStore(IRavenSessionProvider sessionProvider, ExpirationManager expirationManager) : IEditFailedMessagesDataStore

I've expanded the dot point to add more clarity.

- Introduced `BenignSkipped` property in `MigrationWriteResult` to track rows that the target would have deleted anyway.
- Updated `MigrationEngine` logic to ensure benign skips do not count towards the halt threshold.
- Added tests to verify that benign skips are handled correctly and do not cause category halts.
- Updated skip reasons to include `PastRetention` for better clarity on skipped rows.
- Created approval files for category selection tests to ensure correct order and configuration.
- Improved error logging in MigrationEngine to provide clearer context on exceptions.
- Added functionality to InMemoryMigrationSource to simulate shutdown behavior during body reads.
- Enhanced InMemoryMigrationTarget to allow custom exceptions for failure scenarios.
- Introduced TimerRecordingTimeProvider to facilitate testing of timer-related functionality.
- Expanded HaltThresholdTests to cover edge cases involving mixed benign and fault skips.
- Updated MigrationEngineBodyRetryTests to ensure proper handling of shutdowns during body reads.
- Added tests to verify behavior when categories are empty or already completed.
- Implemented checks for checkpoint conflicts and cancellation scenarios in MigrationEngineFailurePathTests.
- Enhanced MigrationEngineHaltTests to evaluate mixed skip scenarios and their impact on halting.
- Improved MigrationEngineOrderingTests to ensure blocked categories behave correctly.
- Added tests to MigrationEngineResumeTests to verify that restart behavior maintains original start time.
- Updated MigrationEngineRunCategoriesTests to handle cases where no categories are selected.
- Enhanced MigrationEngineSkipReasonTests to ensure accurate recording of multiple skip reasons.
- Added tests to MigrationEngineThrottleTests to verify behavior during pauses and shutdowns.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants