Skip to content

Commit ecc7fe9

Browse files
authored
fix(restore): polly retry on Azure blob storage single file handle issue (#66)
* fix(move): lsn null overwrite * fix(restore): retry restore exception azure single file handle * test(build): azure pipeline * fix(retry): custom retry policy time * fix(retry): custom retry policy time * Update Server.cs
1 parent 07890f8 commit ecc7fe9

11 files changed

Lines changed: 109 additions & 115 deletions

File tree

src/AgDatabase.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public interface IAgDatabase
2626
List<BackupMetadata> RecentBackups();
2727
void JoinAg();
2828

29-
void Restore(IEnumerable<BackupMetadata> backupOrder, Func<string, string> fileRelocation = null);
29+
void Restore(IEnumerable<BackupMetadata> backupOrder, Func<int, TimeSpan> retryDurationProvider,
30+
Func<string, string> fileRelocation = null);
3031

3132
void CopyLogins(IEnumerable<LoginProperties> logins);
3233
IEnumerable<LoginProperties> AssociatedLogins();
@@ -100,10 +101,12 @@ public void LogBackup()
100101
/// We suggest using <see cref="AgDatabaseMove" /> to assist with restores.
101102
/// </summary>
102103
/// <param name="backupOrder">An ordered list of backups to restore.</param>
104+
/// <param name="retryDurationProvider">Retry duration function.</param>
103105
/// <param name="fileRelocation">A method to generate the new file location when moving the database.</param>
104-
public void Restore(IEnumerable<BackupMetadata> backupOrder, Func<string, string> fileRelocation = null)
106+
public void Restore(IEnumerable<BackupMetadata> backupOrder, Func<int, TimeSpan> retryDurationProvider,
107+
Func<string, string> fileRelocation = null)
105108
{
106-
_listener.ForEachAgInstance(s => s.Restore(backupOrder, Name, fileRelocation));
109+
_listener.ForEachAgInstance(s => s.Restore(backupOrder, Name, retryDurationProvider, fileRelocation));
107110
}
108111

109112
/// <summary>

src/AgDatabaseMove.Cli/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ internal class MoveArgs
1515
public bool DeleteSource { get; set; } = false;
1616
}
1717

18+
1819
internal class Program
1920
{
2021
private static void Main(string[] args)
@@ -30,7 +31,7 @@ private static void Main(string[] args)
3031
Overwrite = arguments.Overwrite,
3132
Finalize = arguments.Finalize,
3233
CopyLogins = arguments.CopyLogins,
33-
DeleteSource = arguments.DeleteSource,
34+
RetryDuration = attemptNumber => TimeSpan.FromSeconds(10 * attemptNumber),
3435
FileRelocator = filename =>
3536
RestoreFileRelocator(arguments.From.DatabaseName, arguments.To.DatabaseName, filename)
3637
});

src/AgDatabaseMove.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ public class MoveOptions
1717
public IAgDatabase Source { get; set; }
1818
public IAgDatabase Destination { get; set; }
1919
public bool Overwrite { get; set; }
20-
public bool DeleteSource { get; set; } = true;
2120
public bool Finalize { get; set; }
2221
public bool CopyLogins { get; set; }
22+
public Func<int, TimeSpan> RetryDuration { get; set; }
2323
public Func<string, string> FileRelocator { get; set; }
2424
}
2525

@@ -44,6 +44,7 @@ internal LoginProperties UpdateDefaultDb(LoginProperties loginProperties)
4444
return loginProperties;
4545
}
4646

47+
4748
/// <summary>
4849
/// AgDatabaseMove the database to all instances of the availability group.
4950
/// To join the AG, Finalize must be set.
@@ -55,13 +56,13 @@ public decimal Move(decimal? lastLsn = null)
5556
if(!_options.Overwrite && _options.Destination.Exists() && !_options.Destination.Restoring)
5657
throw new ArgumentException("Database exists and overwrite option is not set");
5758

59+
if(_options.Overwrite && lastLsn == null)
60+
_options.Destination.Delete();
61+
5862
if(lastLsn == null && _options.Destination.Restoring)
5963
throw new
6064
ArgumentException("lastLsn parameter can only be used if the Destination database is in a restoring state");
6165

62-
if(_options.Overwrite)
63-
_options.Destination.Delete();
64-
6566
_options.Source.LogBackup();
6667

6768
var backupChain = new BackupChain(_options.Source);
@@ -73,17 +74,14 @@ public decimal Move(decimal? lastLsn = null)
7374
if(!backupList.Any())
7475
throw new BackupChainException("No backups found to restore");
7576

76-
_options.Destination.Restore(backupList, _options.FileRelocator);
77+
_options.Destination.Restore(backupList, _options.RetryDuration, _options.FileRelocator);
7778

7879
if(_options.CopyLogins)
7980
_options.Destination.CopyLogins(_options.Source.AssociatedLogins().Select(UpdateDefaultDb).ToList());
8081

8182
if(_options.Finalize)
8283
_options.Destination.JoinAg();
8384

84-
if(_options.DeleteSource)
85-
_options.Source.Delete();
86-
8785
return backupList.Max(bl => bl.LastLsn);
8886
}
8987
}

src/BackupChain.cs

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
namespace AgDatabaseMove
22
{
3-
using System;
43
using System.Collections.Generic;
5-
using System.IO;
64
using System.Linq;
75
using Exceptions;
86
using SmoFacade;
@@ -23,9 +21,8 @@ public class BackupChain : IBackupChain
2321
// This also handles any striped backups
2422
private BackupChain(IList<BackupMetadata> recentBackups)
2523
{
26-
if (recentBackups == null || recentBackups.Count == 0) {
24+
if(recentBackups == null || recentBackups.Count == 0)
2725
throw new BackupChainException("There are no recent backups to form a chain");
28-
}
2926

3027
var backups = recentBackups.Distinct(new BackupMetadataEqualityComparer())
3128
.Where(IsValidFilePath) // A third party application caused invalid path strings to be inserted into backupmediafamily
@@ -64,35 +61,33 @@ private static IEnumerable<BackupMetadata> MostRecentFullBackup(IEnumerable<Back
6461
var fullBackupsOrdered = backups
6562
.Where(b => b.BackupType == BackupFileTools.BackupType.Full)
6663
.OrderByDescending(d => d.CheckpointLsn).ToList();
67-
68-
if(!fullBackupsOrdered.Any()) {
69-
throw new BackupChainException("Could not find any full backups");
70-
}
64+
65+
if(!fullBackupsOrdered.Any()) throw new BackupChainException("Could not find any full backups");
7166

7267
var targetCheckpointLsn = fullBackupsOrdered.First().CheckpointLsn;
7368
// get all the stripes of this backup
74-
return fullBackupsOrdered.Where(fullBackup => fullBackup.CheckpointLsn == targetCheckpointLsn);
69+
return fullBackupsOrdered.Where(fullBackup => fullBackup.CheckpointLsn == targetCheckpointLsn);
7570
}
7671

77-
private static IEnumerable<BackupMetadata> MostRecentDiffBackup(IEnumerable<BackupMetadata> backups, BackupMetadata lastFullBackup)
72+
private static IEnumerable<BackupMetadata> MostRecentDiffBackup(IEnumerable<BackupMetadata> backups,
73+
BackupMetadata lastFullBackup)
7874
{
7975
var diffBackupsOrdered = backups
8076
.Where(b => b.BackupType == BackupFileTools.BackupType.Diff &&
8177
b.DatabaseBackupLsn == lastFullBackup.CheckpointLsn)
8278
.OrderByDescending(b => b.LastLsn).ToList();
8379

84-
if (!diffBackupsOrdered.Any()) {
85-
return new List<BackupMetadata>();
86-
}
80+
if(!diffBackupsOrdered.Any()) return new List<BackupMetadata>();
8781
var targetLastLsn = diffBackupsOrdered.First().LastLsn;
8882
// get all the stripes of this backup
89-
return diffBackupsOrdered.Where(diffBackup => diffBackup.LastLsn == targetLastLsn);
83+
return diffBackupsOrdered.Where(diffBackup => diffBackup.LastLsn == targetLastLsn);
9084
}
9185

92-
private static IEnumerable<BackupMetadata> NextLogBackup(IEnumerable<BackupMetadata> backups, BackupMetadata prevBackup)
86+
private static IEnumerable<BackupMetadata> NextLogBackup(IEnumerable<BackupMetadata> backups,
87+
BackupMetadata prevBackup)
9388
{
9489
// also gets all the stripes of the next backup
95-
return backups.Where(b => b.BackupType == BackupFileTools.BackupType.Log &&
90+
return backups.Where(b => b.BackupType == BackupFileTools.BackupType.Log &&
9691
prevBackup.LastLsn >= b.FirstLsn && prevBackup.LastLsn + 1 < b.LastLsn);
9792
}
9893

src/SmoFacade/BackupFileTools.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System;
44
using System.IO;
55
using System.Linq;
6-
using System.Text.RegularExpressions;
76

87

98
public static class BackupFileTools
@@ -53,11 +52,9 @@ public static BackupType BackupTypeAbbrevToType(string type)
5352
public static bool IsValidFilePath(string path)
5453
{
5554
// A quick check before leaning on exceptions
56-
if(Path.GetInvalidPathChars().Any(path.Contains)) {
57-
return false;
58-
}
55+
if(Path.GetInvalidPathChars().Any(path.Contains)) return false;
5956

60-
try {
57+
try {
6158
// This will throw an argument exception if the path is invalid
6259
Path.GetFullPath(path);
6360
// A relative path won't help us much if the destination is another server. It needs to be rooted.

src/SmoFacade/Database.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ public List<BackupMetadata> RecentBackups()
7171
"FROM msdb.dbo.backupset s " +
7272
"INNER JOIN msdb.dbo.backupmediafamily m ON s.media_set_id = m.media_set_id " +
7373
"WHERE s.last_lsn >= (" +
74-
"SELECT MAX(last_lsn) FROM msdb.dbo.backupset " +
75-
"WHERE [type] = 'D' " +
76-
"AND database_name = @dbName " +
77-
"AND is_copy_only = 0" +
74+
"SELECT MAX(last_lsn) FROM msdb.dbo.backupset " +
75+
"WHERE [type] = 'D' " +
76+
"AND database_name = @dbName " +
77+
"AND is_copy_only = 0" +
7878
") " +
7979
"AND s.database_name = @dbName " +
8080
"AND is_copy_only = 0" +

src/SmoFacade/Listener.cs

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace AgDatabaseMove.SmoFacade
55
using System.Data.SqlClient;
66
using System.Linq;
77
using System.Net;
8+
using System.Net.Sockets;
89
using System.Threading.Tasks;
910

1011

@@ -133,21 +134,20 @@ private static string AgListenerName(string dataSource)
133134
private static Server AgInstanceNameToServer(ref SqlConnectionStringBuilder connBuilder, string agInstanceName,
134135
string credentialName)
135136
{
136-
try
137-
{
137+
try {
138138
connBuilder.DataSource = ResolveDnsHostNameForInstance(agInstanceName, connBuilder.DataSource);
139139
return new Server(connBuilder.ToString(), credentialName);
140140
}
141-
catch (Exception e)
142-
{
141+
catch(Exception e) {
143142
throw new ArgumentException($"agInstanceName param {agInstanceName} cannot be resolved by DNS", e);
144143
}
145144
}
146145

147146
/// <summary>
148147
/// Resolves 'agReplicaInstanceName' to a FQDN
149148
/// However on Unix OS, when 'val' in 'Dns.GetHostEntry(val)' is not a FQDN, it fails intermittently
150-
/// Therefore, if dns lookup on just the instance name fails, we retry after appending the domain fragments from the listener to the instance name
149+
/// Therefore, if dns lookup on just the instance name fails, we retry after appending the domain fragments from the
150+
/// listener to the instance name
151151
/// </summary>
152152
/// <param name="agReplicaInstanceName"> The name for an instance within the AG (for which we are trying to get the FQDN)</param>
153153
/// <param name="agListenerDomain"> The complete domain for the AG listener</param>
@@ -159,12 +159,10 @@ private static string ResolveDnsHostNameForInstance(string agReplicaInstanceName
159159
var (instanceName, instancePortOrNamedInstance) = SplitDomainAndPort(agReplicaInstanceName);
160160
var preferredPortOrNamedInstance = GetPreferredPort(instancePortOrNamedInstance, listenerPortOrNamedInstance);
161161

162-
try
163-
{
162+
try {
164163
return $"{Dns.GetHostEntry(instanceName).HostName}{preferredPortOrNamedInstance}";
165164
}
166-
catch (System.Net.Sockets.SocketException)
167-
{
165+
catch(SocketException) {
168166
// Re-try by appending the domain fragments from listener to the instance name
169167
// However, we don't need the listener's "host name" (first fragment), so we need to strip that off
170168
// eg: if listener is "abc.def.ghi" we want to append only ".def.ghi" to the instance name
@@ -181,12 +179,10 @@ private static string ResolveDnsHostNameForInstance(string agReplicaInstanceName
181179
// If both are same type, then prioritize instance over listener (in almost all cases they should be identical)
182180
internal static string GetPreferredPort(string instancePortOrNamedInstance, string listenerPortOrNamedInstance)
183181
{
184-
if (string.IsNullOrEmpty(instancePortOrNamedInstance) || string.IsNullOrEmpty(listenerPortOrNamedInstance))
185-
{
186-
return (instancePortOrNamedInstance ?? listenerPortOrNamedInstance);
187-
}
188-
return instancePortOrNamedInstance.StartsWith("\\") && listenerPortOrNamedInstance.StartsWith(",")
189-
? listenerPortOrNamedInstance
182+
if(string.IsNullOrEmpty(instancePortOrNamedInstance) || string.IsNullOrEmpty(listenerPortOrNamedInstance))
183+
return instancePortOrNamedInstance ?? listenerPortOrNamedInstance;
184+
return instancePortOrNamedInstance.StartsWith("\\") && listenerPortOrNamedInstance.StartsWith(",")
185+
? listenerPortOrNamedInstance
190186
: instancePortOrNamedInstance;
191187
}
192188

@@ -196,17 +192,13 @@ internal static (string domain, string port) SplitDomainAndPort(string domainAnd
196192
var domain = domainAndPort;
197193
var splitValue = domainAndPort.Contains(',') ? "," : domainAndPort.Contains('\\') ? "\\" : null;
198194

199-
if(splitValue == null)
200-
{
201-
return (domain, null);
202-
}
195+
if(splitValue == null) return (domain, null);
203196

204197
var fragments = domainAndPort.Split(splitValue.ToCharArray(), 2);
205198
domain = fragments[0];
206199
var port = $"{splitValue}{fragments[1]}";
207200

208201
return (domain, port);
209202
}
210-
211203
}
212204
}

src/SmoFacade/Server.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace AgDatabaseMove.SmoFacade
99
using Exceptions;
1010
using Microsoft.SqlServer.Management.Common;
1111
using Microsoft.SqlServer.Management.Smo;
12+
using Polly;
1213

1314

1415
/// <summary>
@@ -125,10 +126,20 @@ public Database Database(string dbName)
125126
/// </summary>
126127
/// <param name="backupOrder">An ordered list of backups to apply.</param>
127128
/// <param name="databaseName">Database to restore to.</param>
129+
/// <param name="retryDurationProvider">Retry duration function. Retry 10 times, input retry number, output timespan to wait</param>
128130
/// <param name="fileRelocation">Option for renaming files during the restore.</param>
129131
public void Restore(IEnumerable<BackupMetadata> backupOrder, string databaseName,
132+
Func<int, TimeSpan> retryDurationProvider,
130133
Func<string, string> fileRelocation = null)
131134
{
135+
var policy = Policy
136+
.Handle<ExecutionFailureException>(e => e.InnerException != null
137+
&& e.InnerException is SqlException
138+
&& e.InnerException.Message
139+
.Contains("The process cannot access the file because it is being used by another process"))
140+
.WaitAndRetry(10, retryDurationProvider);
141+
142+
132143
var restore = new Restore { Database = databaseName, NoRecovery = true };
133144

134145
foreach(var backup in backupOrder) {
@@ -142,7 +153,8 @@ public void Restore(IEnumerable<BackupMetadata> backupOrder, string databaseName
142153
var defaultFileLocations = DefaultFileLocations();
143154
if(defaultFileLocations != null) {
144155
restore.RelocateFiles.Clear();
145-
foreach(var file in restore.ReadFileList(_server).AsEnumerable()) {
156+
var fileList = policy.Execute(() => restore.ReadFileList(_server).AsEnumerable());
157+
foreach(var file in fileList) {
146158
var physicalName = (string)file["PhysicalName"];
147159
var fileName = Path.GetFileName(physicalName) ??
148160
throw new InvalidBackupException($"Physical name in backup is incomplete: {physicalName}");
@@ -161,7 +173,7 @@ public void Restore(IEnumerable<BackupMetadata> backupOrder, string databaseName
161173

162174
_server.ConnectionContext.StatementTimeout = 86400; // 60 * 60 * 24 = 24 hours
163175

164-
restore.SqlRestore(_server);
176+
policy.Execute(() => restore.SqlRestore(_server));
165177
restore.Devices.Remove(backupDeviceItem);
166178
}
167179
}
@@ -227,4 +239,4 @@ public void EnsureLogins(IEnumerable<LoginProperties> newLogins)
227239
}
228240
}
229241
}
230-
}
242+
}

0 commit comments

Comments
 (0)