diff --git a/src/SqlBackupTools/Restore/Native/BackupRestoreException.cs b/src/SqlBackupTools/Restore/Native/BackupRestoreException.cs index bbf4c4d..0ca1c51 100644 --- a/src/SqlBackupTools/Restore/Native/BackupRestoreException.cs +++ b/src/SqlBackupTools/Restore/Native/BackupRestoreException.cs @@ -8,5 +8,10 @@ public BackupRestoreException(string message) : base(message) { } + + public BackupRestoreException(string message, Exception e) + : base(message, e) + { + } } } diff --git a/src/SqlBackupTools/Restore/Native/NativeRestoreMethod.cs b/src/SqlBackupTools/Restore/Native/NativeRestoreMethod.cs index f83da8f..2a36400 100644 --- a/src/SqlBackupTools/Restore/Native/NativeRestoreMethod.cs +++ b/src/SqlBackupTools/Restore/Native/NativeRestoreMethod.cs @@ -72,18 +72,45 @@ private async Task InternalFullDiffLogAsync(RestoreItem item, bool st } else { - if (_state.RestoredDbs.TryGetValue(item.Name, out RestoreHistoryInfo historyInfo) && - !string.IsNullOrEmpty(historyInfo.LastBackupPath)) + if (_state.RestoredDbs.TryGetValue(item.Name, out RestoreHistoryInfo historyInfo)) { - lastRestored = new FileInfo(historyInfo.LastBackupPath); - var restoreLogsFrom = lastRestored.BackupDate(); - item.RpoRecentRestore = restoreLogsFrom; - _state.Loggger.Debug("Last restore made : path=" + historyInfo.LastBackupPath + ", date=" + - restoreLogsFrom); + if (string.IsNullOrEmpty(historyInfo.LastBackupPath)) + { + _state.Loggger.Debug("No last restore path found, but db exists, dropping " + item.Name); + // drop the db + var exc = await DropDatabaseWhileTryingRestore(item, sqlConnection); + if (exc != null) + { + return exc; + } + + // then restore full + _state.Loggger.Debug("Restoring the last backup FULL : " + item.Name); + lastRestored = await RestoreFullAsync(item, modeFileList, sqlConnection); + item.RpoRecentRestore = lastRestored.BackupDate(); + } + else + { + lastRestored = new FileInfo(historyInfo.LastBackupPath); + var restoreLogsFrom = lastRestored.BackupDate(); + item.RpoRecentRestore = restoreLogsFrom; + _state.Loggger.Debug("Last restore made : path=" + historyInfo.LastBackupPath + ", date=" + restoreLogsFrom); + } } else { - throw new BackupRestoreException("No backup full, and no database in recovering state, can't continue"); + _state.Loggger.Debug("No last restore found, but db may exists, dropping " + item.Name); + // drop the db + var exc = await DropDatabaseWhileTryingRestore(item, sqlConnection); + if (exc != null) + { + return exc; + } + + // then restore full + _state.Loggger.Debug("Restoring the last backup FULL : " + item.Name); + lastRestored = await RestoreFullAsync(item, modeFileList, sqlConnection); + item.RpoRecentRestore = lastRestored.BackupDate(); } } @@ -100,21 +127,10 @@ private async Task InternalFullDiffLogAsync(RestoreItem item, bool st item.StatsDropped++; _state.Loggger.Debug(sqle, item.Name + " : Error on first attempt, retrying from scratch"); forceRestoreFull = true; - bool singleUserMode = false; - try - { - if (_state.ActualDbs.TryGetValue(item.Name, out var databaseInfo) && databaseInfo.State != DatabaseState.RESTORING) - { - singleUserMode = true; - await sqlConnection.ExecuteAsync($"ALTER DATABASE [{item.Name}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE", commandTimeout: _state.RestoreCommand.Timeout); - } - await sqlConnection.ExecuteAsync($"DROP DATABASE [{item.Name}]", commandTimeout: _state.RestoreCommand.Timeout); - await sqlConnection.ExecuteAsync($"EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'{item.Name}'", commandTimeout: _state.RestoreCommand.Timeout); - } - catch (Exception e) + var exc = await DropDatabaseWhileTryingRestore(item, sqlConnection); + if (exc != null) { - _state.Loggger.Error(e, $"Error while trying to drop db{(singleUserMode ? " in SINGLE_USER mode" : string.Empty)}"); - return e; + return exc; } } catch (SqlException sqle) @@ -132,6 +148,29 @@ private async Task InternalFullDiffLogAsync(RestoreItem item, bool st return null; } + private async Task DropDatabaseWhileTryingRestore(RestoreItem item, SqlConnection sqlConnection) + { + bool singleUserMode = false; + try + { + if (_state.ActualDbs.TryGetValue(item.Name, out var databaseInfo) && databaseInfo.State != DatabaseState.RESTORING) + { + singleUserMode = true; + await sqlConnection.ExecuteAsync($"ALTER DATABASE [{item.Name}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE", commandTimeout: _state.RestoreCommand.Timeout); + } + await sqlConnection.ExecuteAsync($"DROP DATABASE [{item.Name}]", commandTimeout: _state.RestoreCommand.Timeout); + await sqlConnection.ExecuteAsync($"EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'{item.Name}'", commandTimeout: _state.RestoreCommand.Timeout); + } + catch (Exception e) + { + _state.Loggger.Error(e, $"Error while trying to drop db{(singleUserMode ? " in SINGLE_USER mode" : string.Empty)}"); + { + return e; + } + } + return null; + } + private async Task RestoreFullAsync(RestoreItem item, bool modeFileList, SqlConnection sqlConnection) { FileInfo fullFile = item.Full @@ -245,7 +284,8 @@ private async Task RestoreLogAsync(RestoreItem item, FileInfo lastRestored, SqlC // - adding the current one again if (lastLogFile == null) { - throw new BackupRestoreException("todo : extract last trn from backup history"); + var ex = await DropDatabaseWhileTryingRestore(item, sqlConnection); + throw new BackupRestoreException("The log in this backup set begins at LSN x, which is too recent to apply to the database. We're dropping the DB, and will try restore full on next loop.", ex); } backupLogsToRestore.Push((currentLogFile, RetryStrategy.None)); backupLogsToRestore.Push((lastLogFile, RetryStrategy.ExtractHeaders)); diff --git a/src/SqlBackupTools/Restore/RestoreState.cs b/src/SqlBackupTools/Restore/RestoreState.cs index 84024a1..9ee591e 100644 --- a/src/SqlBackupTools/Restore/RestoreState.cs +++ b/src/SqlBackupTools/Restore/RestoreState.cs @@ -263,7 +263,7 @@ public async Task LoadServerInfosAsync() ServerInfos = await SqlConnection.GetServerInfosAsync(); } - private const int _systemQueryTimeout = 10; + private const int _systemQueryTimeout = 30; public Task EnsuredIndexsExistsAsync() { @@ -293,6 +293,6 @@ public static Task> GetUserDatabasesAsync(this SqlConn sqlConnection.QueryAsync(@"select name, state from sys.databases where database_id > 4 ", commandTimeout: _systemQueryTimeout); - private const int _systemQueryTimeout = 10; + private const int _systemQueryTimeout = 30; } }