@page "/admin/migrate"
@page "/admin/migrate/{DatabaseStr}"
@page "/admin/migrate/{DatabaseStr}/{DatabaseInstanceStr}"
@attribute [Authorize]
@implements IDisposable
@inject NavigationManager NavigationManager
@inject IUserSession UserSession
@inject IJSRuntime JsRuntime
@inject IMigrationEngine MigrationEngine
@*
* dbMango
*
* Copyright 2025 Deutsche Bank AG
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*@
Migrate data to @Database
@if (!string.IsNullOrWhiteSpace(Error))
{
@Error
}
Explanation of Columns
-
ReadDPS (Read Documents Per Second):
This column shows the rate at which documents are being read from the source database. It is a single-threaded metric, reflecting the performance of a single thread responsible for reading documents.
-
WriteDPS (Write Documents Per Second):
This column shows the rate at which documents are being written to the destination database. Like ReadDPS, it is a single-threaded metric, representing the performance of a single thread responsible for writing documents.
-
DPS (Documents Per Second):
This column represents the effective multithreaded overall documents per second rate. It aggregates the performance of all threads involved in the migration process, providing a holistic view of the system's throughput.
Key Difference: ReadDPS and WriteDPS focus on individual thread performance for reading and writing, respectively, while DPS reflects the total throughput of the migration process, leveraging multithreading to achieve higher performance.
@code {
[CascadingParameter] public IModalService Modal { get; set; } = null!;
[Parameter] public string? DatabaseStr { get; set; }
[Parameter] public string? DatabaseInstanceStr { get; set; }
private bool IsReady { get; set; }
private string Error { get; set; } = "";
private List RunningJobs { get; set; } = [];
private EditContext EditContext => _editContext!;
private bool IsReadyToCancel => IsReady && !SelectedJob.Complete;
private EditContext? _editContext;
private readonly CancellationTokenSource _refreshLoopCts = new();
private string Database
{
get => UserSession.Database;
set
{
if (UserSession.Database == value)
return;
UserSession.Database = value;
SyncUrl();
}
}
private string DatabaseInstance
{
get => UserSession.DatabaseInstance;
set
{
if (UserSession.DatabaseInstance == value)
return;
UserSession.DatabaseInstance = value;
SyncUrl();
}
}
private MigrationJob SelectedJob
{
get;
set
{
if (field.JobId == value.JobId)
return;
field = value;
StateHasChanged();
}
} = new() { Complete = true };
protected override void OnInitialized()
{
_editContext = new(this);
}
protected override void OnAfterRender(bool firstRender)
{
if (!firstRender)
return;
if (string.IsNullOrWhiteSpace(DatabaseStr))
DatabaseStr = Database;
else
Database = DatabaseStr;
if (string.IsNullOrWhiteSpace(DatabaseInstanceStr))
DatabaseInstanceStr = DatabaseInstance;
else
DatabaseInstance = DatabaseInstanceStr;
RunningJobs = MigrationEngine.List().Where(x => x.Type == MigrationJob.JobType.Copy).ToList();
if (RunningJobs.Count > 0)
SelectedJob = RunningJobs[0];
IsReady = true;
_ = Task.Run(() => RefreshLoop(_refreshLoopCts.Token), _refreshLoopCts.Token);
SyncUrl();
StateHasChanged();
}
private async Task RefreshLoop(CancellationToken token)
{
// percents are always increasing, so it safe to compare sum()
var prevState = -1.0;
var prevJobId = SelectedJob.JobId;
var oldComplete = false;
while (!token.IsCancellationRequested)
{
if (prevJobId != SelectedJob.JobId)
{
prevJobId = SelectedJob.JobId;
prevState = -1.0;
oldComplete = false;
await InvokeAsync(StateHasChanged);
}
else
{
var newState = SelectedJob.Status.Sum(x => x.Count + x.Copied + (x.Cleared ?? 0));
if (SelectedJob.Complete != oldComplete || Math.Abs(newState - prevState) > 0.01)
{
prevState = newState;
oldComplete = SelectedJob.Complete;
await InvokeAsync(StateHasChanged);
}
}
await Task.Delay(TimeSpan.FromSeconds(1), token);
}
}
private Task Display(Exception e)
{
Error = e.ToString();
return InvokeAsync(StateHasChanged);
}
private void SyncUrl()
{
var url = NavigationManager.BaseUri + $"admin/migrate/{Database}";
if (!string.IsNullOrWhiteSpace(DatabaseInstance))
url += $"/{DatabaseInstance}";
JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url);
}
private async Task CancelJob()
{
if (SelectedJob.Complete)
return;
var info = GetInfo(SelectedJob);
var res = await ModalDialogUtils.ShowConfirmationDialog(
Modal,
"Migration",
$"Are you sure to cancel migration of {SelectedJob.Status.Count} collection(s) from {SelectedJob.SourceDatabase} to {SelectedJob.DestinationDatabase}?",
info
);
if (res.Cancelled)
return;
await MigrationEngine.Cancel(SelectedJob, UserSession.User.GetUser());
await InvokeAsync(StateHasChanged);
}
private async Task NewMigration()
{
var res = await MigrationJobControl.ShowDialog(Modal);
if (res.Cancelled)
return;
if (res.Data is not MigrationJob newJob)
return;
var info = GetInfo(newJob);
res = await ModalDialogUtils.ShowConfirmationDialog(
Modal,
"Migration",
$"Are you sure to start migration of {newJob.Status.Count} collection(s) from {newJob.SourceDatabase} to {newJob.DestinationDatabase}?",
info
);
if (res.Cancelled)
return;
await RegisterNewJob(newJob);
}
private Task CanExecuteCommand()
=> Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal);
private async Task RegisterNewJob(MigrationJob newJob)
{
try
{
var ticket = await CanExecuteCommand();
if (string.IsNullOrWhiteSpace(ticket))
return;
newJob.Ticket = ticket;
await MigrationEngine.Add(newJob, UserSession.User.GetUser());
SelectedJob = newJob;
RunningJobs = MigrationEngine.List();
}
catch (Exception e)
{
await Display(e);
}
await InvokeAsync(StateHasChanged);
}
private async Task NewTransformation()
{
var res = await TransformJobControl.ShowDialog(Modal);
if (res.Cancelled)
return;
if (res.Data is not MigrationJob newJob)
return;
var info = GetInfo(newJob);
res = await ModalDialogUtils.ShowConfirmationDialog(
Modal,
"Transformation",
$"Are you sure to start transformation of {newJob.Status[0].SourceCollection} from {newJob.SourceDatabase} to {newJob.Status[0].DestinationCollection} of {newJob.DestinationDatabase}?",
info
);
if (res.Cancelled)
return;
await RegisterNewJob(newJob);
}
private Dictionary GetInfo(MigrationJob job)
{
var info = new Dictionary
{
["from"] = job.SourceDatabase,
["to"] = job.DestinationDatabase,
["upsert"] = job.Upsert.ToString(),
["clearDestBefore"] = job.ClearDestinationBefore.ToString(),
["batchSize"] = job.BatchSize.ToString(),
};
return info;
}
public void Dispose()
{
_refreshLoopCts.Cancel();
_refreshLoopCts.Dispose();
}
}