Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/AgDatabaseMove.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@
</Reference>
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="AgDatabaseMove.Unit" />
</ItemGroup>

</Project>
69 changes: 45 additions & 24 deletions src/BackupChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace AgDatabaseMove
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Exceptions;
using SmoFacade;


Expand All @@ -19,26 +20,28 @@ public class BackupChain : IBackupChain
{
private readonly IList<BackupMetadata> _orderedBackups;

// This also handles any striped backups
private BackupChain(IList<BackupMetadata> recentBackups)
{
if (recentBackups == null || recentBackups.Count == 0) {
throw new BackupChainException("There are no recent backups to form a chain");
}

var backups = recentBackups.Distinct(new BackupMetadataEqualityComparer())
.Where(b => IsValidFilePath(b)) // A third party application caused invalid path strings to be inserted into backupmediafamily
.Where(IsValidFilePath) // A third party application caused invalid path strings to be inserted into backupmediafamily
.ToList();

var mostRecentFullBackup = MostRecentFullBackup(recentBackups);
_orderedBackups = new List<BackupMetadata> { mostRecentFullBackup };

var differentialBackup = MostRecentDifferentialBackup(backups, mostRecentFullBackup);
if(differentialBackup != null)
_orderedBackups.Add(differentialBackup);
var orderedBackups = MostRecentFullBackup(backups).ToList();
orderedBackups.AddRange(MostRecentDiffBackup(backups, orderedBackups.First()));

var mostRecentBackup = _orderedBackups.Last();
while(mostRecentBackup != null) {
mostRecentBackup = NextLogBackup(backups, mostRecentBackup);

if(mostRecentBackup != null)
_orderedBackups.Add(mostRecentBackup);
var prevBackup = orderedBackups.Last();
IEnumerable<BackupMetadata> nextLogBackups;
while((nextLogBackups = NextLogBackup(backups, prevBackup)).Any()) {
orderedBackups.AddRange(nextLogBackups);
prevBackup = orderedBackups.Last();
}

_orderedBackups = orderedBackups;
}

/// <summary>
Expand All @@ -56,26 +59,44 @@ public BackupChain(Database database) : this(database.RecentBackups()) { }
/// </summary>
public IEnumerable<BackupMetadata> OrderedBackups => _orderedBackups;

private BackupMetadata MostRecentFullBackup(IList<BackupMetadata> backups)
private static IEnumerable<BackupMetadata> MostRecentFullBackup(IEnumerable<BackupMetadata> backups)
{
return backups.Where(b => b.BackupType == BackupFileTools.BackupType.Full).OrderByDescending(d => d.CheckpointLsn)
.First();
var fullBackupsOrdered = backups
.Where(b => b.BackupType == BackupFileTools.BackupType.Full)
.OrderByDescending(d => d.CheckpointLsn).ToList();

if(!fullBackupsOrdered.Any()) {
throw new BackupChainException("Could not find any full backups");
}

var targetCheckpointLsn = fullBackupsOrdered.First().CheckpointLsn;
// get all the stripes of this backup
return fullBackupsOrdered.Where(fullBackup => fullBackup.CheckpointLsn == targetCheckpointLsn);
}

private BackupMetadata MostRecentDifferentialBackup(IList<BackupMetadata> backups, BackupMetadata lastFullBackup)
private static IEnumerable<BackupMetadata> MostRecentDiffBackup(IEnumerable<BackupMetadata> backups, BackupMetadata lastFullBackup)
{
return backups.Where(b => b.BackupType == BackupFileTools.BackupType.Diff &&
b.DatabaseBackupLsn == lastFullBackup.CheckpointLsn)
.OrderByDescending(b => b.LastLsn).FirstOrDefault();
var diffBackupsOrdered = backups
.Where(b => b.BackupType == BackupFileTools.BackupType.Diff &&
b.DatabaseBackupLsn == lastFullBackup.CheckpointLsn)
.OrderByDescending(b => b.LastLsn).ToList();

if (!diffBackupsOrdered.Any()) {
return new List<BackupMetadata>();
}
var targetLastLsn = diffBackupsOrdered.First().LastLsn;
// get all the stripes of this backup
return diffBackupsOrdered.Where(diffBackup => diffBackup.LastLsn == targetLastLsn);
}

private BackupMetadata NextLogBackup(IList<BackupMetadata> backups, BackupMetadata prevBackup)
private static IEnumerable<BackupMetadata> NextLogBackup(IEnumerable<BackupMetadata> backups, BackupMetadata prevBackup)
{
return backups.Where(b => b.BackupType == BackupFileTools.BackupType.Log)
.SingleOrDefault(d => prevBackup.LastLsn >= d.FirstLsn && prevBackup.LastLsn + 1 < d.LastLsn);
// also gets all the stripes of the next backup
return backups.Where(b => b.BackupType == BackupFileTools.BackupType.Log &&
prevBackup.LastLsn >= b.FirstLsn && prevBackup.LastLsn + 1 < b.LastLsn);
}

private bool IsValidFilePath(BackupMetadata meta)
private static bool IsValidFilePath(BackupMetadata meta)
{
if(BackupFileTools.IsUrl(meta.PhysicalDeviceName))
return true;
Expand Down
12 changes: 10 additions & 2 deletions src/BackupMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ public bool Equals(BackupMetadata x, BackupMetadata y)
return x.LastLsn == y.LastLsn &&
x.FirstLsn == y.FirstLsn &&
x.BackupType == y.BackupType &&
x.DatabaseName == y.DatabaseName;
x.DatabaseName == y.DatabaseName &&
x.PhysicalDeviceName == y.PhysicalDeviceName;
}

public int GetHashCode(BackupMetadata obj)
{
var hashCode = -1277603921;
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(obj.DatabaseName);
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(obj.PhysicalDeviceName);
hashCode = hashCode * -1521134295 +
EqualityComparer<BackupFileTools.BackupType>.Default.GetHashCode(obj.BackupType);
hashCode = hashCode * -1521134295 + obj.FirstLsn.GetHashCode();
Expand All @@ -35,7 +37,7 @@ public int GetHashCode(BackupMetadata obj)
/// <summary>
/// Metadata about backups from msdb.dbo.backupset and msdb.dbo.backupmediafamily
/// </summary>
public class BackupMetadata
public class BackupMetadata : ICloneable
{
public decimal CheckpointLsn { get; set; }
public decimal DatabaseBackupLsn { get; set; }
Expand All @@ -52,5 +54,11 @@ public class BackupMetadata
/// https://docs.microsoft.com/en-us/sql/relational-databases/system-tables/backupset-transact-sql?view=sql-server-2017
/// </summary>
public BackupFileTools.BackupType BackupType { get; set; }

// used during testing
public object Clone()
{
return MemberwiseClone();
}
}
}
11 changes: 9 additions & 2 deletions src/SmoFacade/Database.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,15 @@ public List<BackupMetadata> RecentBackups()
"s.database_backup_lsn, s.checkpoint_lsn, s.[type] AS backup_type, s.server_name, s.recovery_model " +
"FROM msdb.dbo.backupset s " +
"INNER JOIN msdb.dbo.backupmediafamily m ON s.media_set_id = m.media_set_id " +
"WHERE s.last_lsn >= (SELECT MAX(last_lsn) FROM msdb.dbo.backupset WHERE [type] = 'D' and database_name = @dbName) " +
"AND s.database_name = @dbName ORDER BY s.backup_start_date DESC, backup_finish_date";
"WHERE s.last_lsn >= (" +
"SELECT MAX(last_lsn) FROM msdb.dbo.backupset " +
"WHERE [type] = 'D' " +
"AND database_name = @dbName " +
"AND is_copy_only = 0" +
") " +
"AND s.database_name = @dbName " +
"AND is_copy_only = 0" +
"ORDER BY s.backup_start_date DESC, backup_finish_date";

using var cmd = _server.SqlConnection.CreateCommand();
cmd.CommandText = query;
Expand Down
3 changes: 1 addition & 2 deletions src/SmoFacade/Listener.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("AgDatabaseMove.Unit")]
namespace AgDatabaseMove.SmoFacade
{
using System;
Expand Down Expand Up @@ -147,7 +146,7 @@ private static Server AgInstanceNameToServer(ref SqlConnectionStringBuilder conn

/// <summary>
/// Resolves 'agReplicaInstanceName' to a FQDN
/// However on Unix OS, when 'val' in 'Dns.GetHostEntry(val)' is not a complete domain (i.e is just "abc", instead of "abc.def.com"), it fails intermittently
/// However on Unix OS, when 'val' in 'Dns.GetHostEntry(val)' is not a FQDN, it fails intermittently
/// Therefore, if dns lookup on just the instance name fails, we retry after appending the domain fragments from the listener to the instance name
/// </summary>
/// <param name="agReplicaInstanceName"> The name for an instance within the AG (for which we are trying to get the FQDN)</param>
Expand Down
Loading