Skip to content

Commit 155b54d

Browse files
committed
Retry only for a limited number of times on encountering inaccessible file.
1 parent 8e2a1c7 commit 155b54d

File tree

6 files changed

+83
-41
lines changed

6 files changed

+83
-41
lines changed

AsyncToSyncConverter.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,14 @@ public static async Task AsyncFileUpdated(Context context)
127127
{
128128
string fileData;
129129
try
130-
{
131-
fileData = await FileExtensions.ReadAllTextAsync(Extensions.GetLongPath(context.Event.FullName), context.Token);
130+
{
131+
long maxFileSize = Global.MaxFileSizeMB * (1024 * 1024);
132+
fileData = await FileExtensions.ReadAllTextAsync(Extensions.GetLongPath(context.Event.FullName), context.Token, maxFileSize: maxFileSize, retryCount: Global.RetryCountOnSrcFileOpenError);
133+
134+
if (fileData == null)
135+
{
136+
await ConsoleWatch.AddMessage(ConsoleColor.Red, $"Error synchronising updates from file {context.Event.FullName}", context);
137+
}
132138
}
133139
catch (FileNotFoundException) //file was removed by the time queue processing got to it
134140
{

BinaryFileExtensions.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,24 @@ namespace AsyncToSyncCodeRoundtripSynchroniserMonitor
1717
{
1818
public static partial class FileExtensions
1919
{
20-
public static int MaxByteArraySize = 0x7FFFFFC7; //https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcallowverylargeobjects-element?redirectedfrom=MSDN#remarks
20+
public static long MaxByteArraySize = 0x7FFFFFC7; //https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcallowverylargeobjects-element?redirectedfrom=MSDN#remarks
2121

2222
//https://stackoverflow.com/questions/18472867/checking-equality-for-two-byte-arrays/
2323
public static bool BinaryEqual(Binary a, Binary b)
2424
{
2525
return a.Equals(b);
2626
}
2727

28-
public static async Task<Tuple<byte[], long>> ReadAllBytesAsync(string path, CancellationToken cancellationToken = default(CancellationToken), long maxFileSize = 0)
28+
public static async Task<Tuple<byte[], long>> ReadAllBytesAsync(string path, CancellationToken cancellationToken = default(CancellationToken), long maxFileSize = 0, int retryCount = 0)
2929
{
3030
if (path == null)
3131
throw new ArgumentNullException(nameof(path));
3232
if (path.Length == 0)
3333
throw new ArgumentException("Argument_EmptyPath: {0}", nameof(path));
3434

35-
while (true)
35+
36+
retryCount = Math.Max(0, retryCount);
37+
for (int i = -1; i < retryCount; i++)
3638
{
3739
if (cancellationToken.IsCancellationRequested)
3840
return await Task.FromCanceled<Tuple<byte[], long>>(cancellationToken);
@@ -67,21 +69,19 @@ ex is IOException
6769
)
6870
{
6971
//retry after delay
70-
try
72+
73+
if (i + 1 < retryCount) //do not sleep after last try
7174
{
7275
#if !NOASYNC
7376
await Task.Delay(1000, cancellationToken); //TODO: config file?
7477
#else
7578
cancellationToken.WaitHandle.WaitOne(1000);
7679
#endif
7780
}
78-
catch (TaskCanceledException)
79-
{
80-
//do nothing here
81-
return await Task.FromCanceled<Tuple<byte[], long>>(cancellationToken);
82-
}
8381
}
8482
}
83+
84+
return new Tuple<byte[], long>(null, -1);
8585
}
8686

8787
public static async Task WriteAllBytesAsync(string path, byte[] contents, bool createTempFileFirst, CancellationToken cancellationToken = default(CancellationToken), int writeBufferKB = 0, int bufferWriteDelayMs = 0)
@@ -147,6 +147,7 @@ ex is IOException
147147
catch (IOException)
148148
{
149149
//retry after delay
150+
150151
#if !NOASYNC
151152
await Task.Delay(1000, cancellationToken); //TODO: config file?
152153
#else

FileExtensions.cs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ namespace AsyncToSyncCodeRoundtripSynchroniserMonitor
2020
{
2121
public static partial class FileExtensions
2222
{
23+
public static long MaxCharArraySize = 0x7FEFFFFF; //https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcallowverylargeobjects-element?redirectedfrom=MSDN#remarks
24+
2325
//adapted from https://github.com/dotnet/runtime/blob/5ddc873d9ea6cd4bc6a935fec3057fe89a6932aa/src/libraries/System.IO.FileSystem/src/System/IO/File.cs
2426

2527
//internal const int DefaultBufferSize = 4096;
@@ -41,10 +43,10 @@ private static StreamReader AsyncStreamReader(string path, Encoding encoding)
4143
return new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true);
4244
}
4345

44-
public static Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default(CancellationToken))
45-
=> ReadAllTextAsync(path, UTF8NoBOM, cancellationToken);
46+
public static Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default(CancellationToken), long maxFileSize = 0, int retryCount = 0)
47+
=> ReadAllTextAsync(path, UTF8NoBOM, cancellationToken, maxFileSize, retryCount);
4648

47-
public static async Task<string> ReadAllTextAsync(string path, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken))
49+
public static async Task<string> ReadAllTextAsync(string path, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken), long maxFileSize = 0, int retryCount = 0)
4850
{
4951
if (path == null)
5052
throw new ArgumentNullException(nameof(path));
@@ -53,38 +55,38 @@ private static StreamReader AsyncStreamReader(string path, Encoding encoding)
5355
if (path.Length == 0)
5456
throw new ArgumentException("Argument_EmptyPath: {0}", nameof(path));
5557

56-
while (true) //roland
58+
59+
retryCount = Math.Max(0, retryCount);
60+
for (int i = -1; i < retryCount; i++) //roland
5761
{
5862
try //roland
5963
{
6064
return cancellationToken.IsCancellationRequested
6165
? await Task.FromCanceled<string>(cancellationToken)
62-
: await InternalReadAllTextAsync(path, encoding, cancellationToken);
66+
: await InternalReadAllTextAsync(path, encoding, cancellationToken, maxFileSize);
6367
}
6468
catch (Exception ex) when ( //roland
6569
ex is IOException
6670
|| ex is UnauthorizedAccessException //can happen when a folder was just created //TODO: abandon retries after a certain number of attempts in this case
6771
)
6872
{
6973
//retry after delay
70-
try
74+
75+
if (i + 1 < retryCount) //do not sleep after last try
7176
{
7277
#if !NOASYNC
7378
await Task.Delay(1000, cancellationToken); //TODO: config file?
7479
#else
7580
cancellationToken.WaitHandle.WaitOne(1000);
7681
#endif
7782
}
78-
catch (TaskCanceledException)
79-
{
80-
//do nothing here
81-
return await Task.FromCanceled<string>(cancellationToken);
82-
}
8383
}
8484
}
85+
86+
return null;
8587
}
8688

87-
private static async Task<string> InternalReadAllTextAsync(string path, Encoding encoding, CancellationToken cancellationToken)
89+
private static async Task<string> InternalReadAllTextAsync(string path, Encoding encoding, CancellationToken cancellationToken, long maxFileSize = 0)
8890
{
8991
Debug.Assert(!string.IsNullOrEmpty(path));
9092
Debug.Assert(encoding != null);
@@ -94,6 +96,20 @@ private static async Task<string> InternalReadAllTextAsync(string path, Encoding
9496
try
9597
{
9698
cancellationToken.ThrowIfCancellationRequested();
99+
100+
101+
if (maxFileSize > 0)
102+
{
103+
long len = sr.BaseStream.Length; //NB! the length might change during the code execution, so need to save it into separate variable
104+
105+
maxFileSize = Math.Min(MaxCharArraySize * sizeof(char), maxFileSize);
106+
if (len > maxFileSize)
107+
{
108+
return null; //TODO: return file size so that error message can report it
109+
}
110+
}
111+
112+
97113
#if NETSTANDARD
98114
buffer = ArrayPool<char>.Shared.Rent(sr.CurrentEncoding.GetMaxCharCount(DefaultBufferSize));
99115
#else

Program.cs

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ internal static class Global
4040
public static bool UseIdlePriority = false;
4141
public static bool ShowErrorAlerts = true;
4242

43+
public static int RetryCountOnSrcFileOpenError = 10;
44+
4345
public static long MaxFileSizeMB = 2048;
4446

4547

@@ -152,6 +154,7 @@ private static void Main()
152154

153155

154156
Global.MaxFileSizeMB = fileConfig.GetLong("MaxFileSizeMB") ?? Global.MaxFileSizeMB;
157+
Global.RetryCountOnSrcFileOpenError = (int?)fileConfig.GetLong("RetryCountOnSrcFileOpenError") ?? Global.RetryCountOnSrcFileOpenError;
155158

156159

157160
Global.Bidirectional = fileConfig.GetTextUpper("Bidirectional") != "FALSE"; //default is true
@@ -257,7 +260,7 @@ private static async Task MainTask()
257260
watch.Start();
258261

259262

260-
var messageContext = new Context(
263+
var initialSyncMessageContext = new Context(
261264
eventObj: null,
262265
token: Global.CancellationToken.Token,
263266
isSyncPath: false, //unused here
@@ -267,16 +270,16 @@ private static async Task MainTask()
267270

268271
BackgroundTaskManager.Run(async () =>
269272
{
270-
await ConsoleWatch.AddMessage(ConsoleColor.White, "Doing initial synchronisation...", messageContext);
273+
await ConsoleWatch.AddMessage(ConsoleColor.White, "Doing initial synchronisation...", initialSyncMessageContext);
271274

272-
await ScanFolders(isInitialScan: true);
275+
await ScanFolders(initialSyncMessageContext: initialSyncMessageContext);
273276

274277
BackgroundTaskManager.Run(async () =>
275278
{
276279
await InitialSyncCountdownEvent.WaitAsync(Global.CancellationToken.Token);
277280

278281
//if (!Global.CancellationToken.IsCancellationRequested)
279-
await ConsoleWatch.AddMessage(ConsoleColor.White, "Done initial synchronisation...", messageContext);
282+
await ConsoleWatch.AddMessage(ConsoleColor.White, "Done initial synchronisation...", initialSyncMessageContext);
280283
});
281284

282285
}); //BackgroundTaskManager.Run(async () =>
@@ -306,27 +309,27 @@ private static async Task MainTask()
306309

307310
private static readonly AsyncCountdownEvent InitialSyncCountdownEvent = new AsyncCountdownEvent(1);
308311

309-
private static async Task ScanFolders(bool isInitialScan)
312+
private static async Task ScanFolders(Context initialSyncMessageContext)
310313
{
311314
//1. Do initial synchronisation from sync to async folder //TODO: config for enabling and ordering of this operation
312-
await ScanFolder(Global.SyncPath, "*.*", isInitialScan: isInitialScan); //NB! use *.* in order to sync resx files also
315+
await ScanFolder(Global.SyncPath, "*.*", initialSyncMessageContext: initialSyncMessageContext); //NB! use *.* in order to sync resx files also
313316

314317
if (Global.Bidirectional)
315318
{
316319
//2. Do initial synchronisation from async to sync folder //TODO: config for enabling and ordering of this operation
317-
await ScanFolder(Global.AsyncPath, "*.*", isInitialScan: isInitialScan); //NB! use *.* in order to sync resx files also
320+
await ScanFolder(Global.AsyncPath, "*.*", initialSyncMessageContext: initialSyncMessageContext); //NB! use *.* in order to sync resx files also
318321
}
319322

320-
if (isInitialScan)
323+
if (initialSyncMessageContext?.IsInitialScan == true)
321324
InitialSyncCountdownEvent.Signal();
322325
}
323326

324-
private static async Task ScanFolder(string path, string extension, bool isInitialScan)
327+
private static async Task ScanFolder(string path, string extension, Context initialSyncMessageContext)
325328
{
326-
var fileInfos = ProcessSubDirs(new DirectoryInfo(Extensions.GetLongPath(path)), extension);
329+
var fileInfos = ProcessSubDirs(new DirectoryInfo(Extensions.GetLongPath(path)), extension, initialSyncMessageContext: initialSyncMessageContext);
327330
await fileInfos.ForEachAsync(fileInfo =>
328331
{
329-
if (isInitialScan)
332+
if (initialSyncMessageContext?.IsInitialScan == true)
330333
InitialSyncCountdownEvent.AddCount();
331334

332335
BackgroundTaskManager.Run(async () =>
@@ -335,19 +338,25 @@ await ConsoleWatch.OnAddedAsync
335338
(
336339
new DummyFileSystemEvent(fileInfo),
337340
Global.CancellationToken.Token,
338-
isInitialScan
341+
initialSyncMessageContext?.IsInitialScan == true
339342
);
340343

341-
if (isInitialScan)
344+
if (initialSyncMessageContext?.IsInitialScan == true)
342345
InitialSyncCountdownEvent.Signal();
343346
});
344347
});
345348
}
346349

347-
private static IAsyncEnumerable<FileInfo> ProcessSubDirs(DirectoryInfo srcDirInfo, string searchPattern, int recursionLevel = 0)
350+
private static IAsyncEnumerable<FileInfo> ProcessSubDirs(DirectoryInfo srcDirInfo, string searchPattern, int recursionLevel = 0, Context initialSyncMessageContext = null)
348351
{
349352
return new AsyncEnumerable<FileInfo>(async yield => {
350353

354+
#if DEBUG && false
355+
if (initialSyncMessageContext?.IsInitialScan == true)
356+
await ConsoleWatch.AddMessage(ConsoleColor.Blue, "Scanning folder " + Extensions.GetLongPath(srcDirInfo.FullName), initialSyncMessageContext);
357+
#endif
358+
359+
351360
#if false //this built-in functio will throw IOException in case some subfolder is an invalid reparse point
352361
return new DirectoryInfo(sourceDir)
353362
.GetFiles(searchPattern, SearchOption.AllDirectories);
@@ -417,7 +426,7 @@ private static IAsyncEnumerable<FileInfo> ProcessSubDirs(DirectoryInfo srcDirInf
417426
}
418427

419428

420-
var subDirFileInfos = ProcessSubDirs(dirInfo, searchPattern, recursionLevel + 1);
429+
var subDirFileInfos = ProcessSubDirs(dirInfo, searchPattern, recursionLevel + 1, initialSyncMessageContext: initialSyncMessageContext);
421430
await subDirFileInfos.ForEachAsync(async subDirFileInfo =>
422431
{
423432
await yield.ReturnAsync(subDirFileInfo);
@@ -926,10 +935,13 @@ public static async Task FileUpdated(Context context)
926935
Tuple<byte[], long> fileDataTuple = null;
927936
try
928937
{
929-
fileDataTuple = await FileExtensions.ReadAllBytesAsync(Extensions.GetLongPath(context.Event.FullName), context.Token);
938+
fileDataTuple = await FileExtensions.ReadAllBytesAsync(Extensions.GetLongPath(context.Event.FullName), context.Token, retryCount: Global.RetryCountOnSrcFileOpenError);
930939
if (fileDataTuple.Item1 == null) //maximum length exceeded
931940
{
932-
await AddMessage(ConsoleColor.Red, $"Error synchronising updates from file {context.Event.FullName} : fileLength > maxFileSize : {fileDataTuple.Item2} > {maxFileSize}", context);
941+
if (fileDataTuple.Item2 >= 0)
942+
await AddMessage(ConsoleColor.Red, $"Error synchronising updates from file {context.Event.FullName} : fileLength > maxFileSize : {fileDataTuple.Item2} > {maxFileSize}", context);
943+
else
944+
await AddMessage(ConsoleColor.Red, $"Error synchronising updates from file {context.Event.FullName}", context);
933945

934946
return; //TODO: log error?
935947
}

SyncToAsyncConverter.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,13 @@ public static async Task SyncFileUpdated(Context context)
103103
string fileData;
104104
try
105105
{
106-
fileData = await FileExtensions.ReadAllTextAsync(Extensions.GetLongPath(context.Event.FullName), context.Token);
106+
long maxFileSize = Global.MaxFileSizeMB * (1024 * 1024);
107+
fileData = await FileExtensions.ReadAllTextAsync(Extensions.GetLongPath(context.Event.FullName), context.Token, maxFileSize: maxFileSize, retryCount: Global.RetryCountOnSrcFileOpenError);
108+
109+
if (fileData == null)
110+
{
111+
await ConsoleWatch.AddMessage(ConsoleColor.Red, $"Error synchronising updates from file {context.Event.FullName}", context);
112+
}
107113
}
108114
catch (FileNotFoundException) //file was removed by the time queue processing got to it
109115
{

appsettings.example.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"UseIdlePriority": false,
55
"ShowErrorAlerts": true,
66
"MaxFileSizeMB": 2048,
7+
"RetryCountOnSrcFileOpenError": 10,
78

89

910
"Bidirectional": true,

0 commit comments

Comments
 (0)