Skip to content

Commit

Permalink
Replace with async lock (#294)
Browse files Browse the repository at this point in the history
* chore: use AsyncLock instead of regular lock since methods are async

* Fix merge

---------

Co-authored-by: Eric J. Smith <eric@ericjsmith.com>
Co-authored-by: Eric J. Smith <eric@codesmithtools.com>
  • Loading branch information
3 people authored Sep 24, 2023
1 parent abab988 commit 0cfb6f0
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 54 deletions.
19 changes: 10 additions & 9 deletions src/Foundatio/Storage/FolderFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Foundatio.AsyncEx;
using Foundatio.Extensions;
using Foundatio.Serializer;
using Foundatio.Utility;
Expand All @@ -12,7 +13,7 @@

namespace Foundatio.Storage {
public class FolderFileStorage : IFileStorage {
private readonly object _lockObject = new();
private readonly AsyncLock _lock = new();
private readonly ISerializer _serializer;
protected readonly ILogger _logger;

Expand Down Expand Up @@ -150,7 +151,7 @@ private Stream CreateFileStream(string filePath) {
return File.Create(filePath);
}

public Task<bool> RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) {
public async Task<bool> RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
if (String.IsNullOrEmpty(newPath))
Expand All @@ -161,7 +162,7 @@ public Task<bool> RenameFileAsync(string path, string newPath, CancellationToken
_logger.LogInformation("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath);

try {
lock (_lockObject) {
using (await _lock.LockAsync().AnyContext()) {
string directory = Path.GetDirectoryName(normalizedNewPath);
if (directory != null) {
_logger.LogInformation("Creating {Directory} directory", directory);
Expand All @@ -182,13 +183,13 @@ public Task<bool> RenameFileAsync(string path, string newPath, CancellationToken
}
} catch (Exception ex) {
_logger.LogError(ex, "Error renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath);
return Task.FromResult(false);
return false;
}

return Task.FromResult(true);
return true;
}

public Task<bool> CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) {
public async Task<bool> CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
if (String.IsNullOrEmpty(targetPath))
Expand All @@ -199,7 +200,7 @@ public Task<bool> CopyFileAsync(string path, string targetPath, CancellationToke
_logger.LogInformation("Copying {Path} to {TargetPath}", normalizedPath, normalizedTargetPath);

try {
lock (_lockObject) {
using (await _lock.LockAsync().AnyContext()) {
string directory = Path.GetDirectoryName(normalizedTargetPath);
if (directory != null) {
_logger.LogInformation("Creating {Directory} directory", directory);
Expand All @@ -210,10 +211,10 @@ public Task<bool> CopyFileAsync(string path, string targetPath, CancellationToke
}
} catch (Exception ex) {
_logger.LogError(ex, "Error copying {Path} to {TargetPath}: {Message}", normalizedPath, normalizedTargetPath, ex.Message);
return Task.FromResult(false);
return false;
}

return Task.FromResult(true);
return true;
}

public Task<bool> DeleteFileAsync(string path, CancellationToken cancellationToken = default) {
Expand Down
95 changes: 50 additions & 45 deletions src/Foundatio/Storage/InMemoryFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Foundatio.Utility;
using Foundatio.AsyncEx;
using Foundatio.Extensions;
using Foundatio.Serializer;
using Foundatio.Utility;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Foundatio.Storage {
public class InMemoryFileStorage : IFileStorage {
private readonly Dictionary<string, Tuple<FileSpec, byte[]>> _storage = new(StringComparer.OrdinalIgnoreCase);
private readonly object _lock = new();
private readonly AsyncLock _lock = new();
private readonly ISerializer _serializer;
protected readonly ILogger _logger;

public InMemoryFileStorage() : this(o => o) {}
public InMemoryFileStorage() : this(o => o) { }

public InMemoryFileStorage(InMemoryFileStorageOptions options) {
if (options == null)
Expand All @@ -30,7 +31,7 @@ public InMemoryFileStorage(InMemoryFileStorageOptions options) {
_logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance;
}

public InMemoryFileStorage(Builder<InMemoryFileStorageOptionsBuilder, InMemoryFileStorageOptions> config)
public InMemoryFileStorage(Builder<InMemoryFileStorageOptionsBuilder, InMemoryFileStorageOptions> config)
: this(config(new InMemoryFileStorageOptionsBuilder()).Build()) { }

public long MaxFileSize { get; set; }
Expand All @@ -40,20 +41,20 @@ public InMemoryFileStorage(Builder<InMemoryFileStorageOptionsBuilder, InMemoryFi
public Task<Stream> GetFileStreamAsync(string path, CancellationToken cancellationToken = default) =>
GetFileStreamAsync(path, StreamMode.Read, cancellationToken);

public Task<Stream> GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default) {
public async Task<Stream> GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));

string normalizedPath = path.NormalizePath();
_logger.LogTrace("Getting file stream for {Path}", normalizedPath);

lock (_lock) {
using (await _lock.LockAsync().AnyContext()) {
if (!_storage.ContainsKey(normalizedPath)) {
_logger.LogError("Unable to get file stream for {Path}: File Not Found", normalizedPath);
return Task.FromResult<Stream>(null);
return null;
}

return Task.FromResult<Stream>(new MemoryStream(_storage[normalizedPath].Item2));
return new MemoryStream(_storage[normalizedPath].Item2);
}
}

Expand All @@ -71,13 +72,16 @@ public async Task<FileSpec> GetFileInfoAsync(string path) {
return null;
}

public Task<bool> ExistsAsync(string path) {
public async Task<bool> ExistsAsync(string path) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));

string normalizedPath = path.NormalizePath();
_logger.LogTrace("Checking if {Path} exists", normalizedPath);
return Task.FromResult(_storage.ContainsKey(normalizedPath));

using (await _lock.LockAsync().AnyContext()) {
return _storage.ContainsKey(normalizedPath);
}
}

private static byte[] ReadBytes(Stream input) {
Expand All @@ -86,20 +90,20 @@ private static byte[] ReadBytes(Stream input) {
return ms.ToArray();
}

public Task<bool> SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) {
public async Task<bool> SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
if (stream == null)
throw new ArgumentNullException(nameof(stream));

string normalizedPath = path.NormalizePath();
_logger.LogTrace("Saving {Path}", normalizedPath);

var contents = ReadBytes(stream);
if (contents.Length > MaxFileSize)
throw new ArgumentException($"File size {contents.Length.ToFileSizeDisplay()} exceeds the maximum size of {MaxFileSize.ToFileSizeDisplay()}.");

lock (_lock) {
using (await _lock.LockAsync().AnyContext()) {
_storage[normalizedPath] = Tuple.Create(new FileSpec {
Created = SystemClock.UtcNow,
Modified = SystemClock.UtcNow,
Expand All @@ -111,10 +115,10 @@ public Task<bool> SaveFileAsync(string path, Stream stream, CancellationToken ca
_storage.Remove(_storage.OrderByDescending(kvp => kvp.Value.Item1.Created).First().Key);
}

return Task.FromResult(true);
return true;
}

public Task<bool> RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) {
public async Task<bool> RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
if (String.IsNullOrEmpty(newPath))
Expand All @@ -123,11 +127,11 @@ public Task<bool> RenameFileAsync(string path, string newPath, CancellationToken
string normalizedPath = path.NormalizePath();
string normalizedNewPath = newPath.NormalizePath();
_logger.LogInformation("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath);
lock (_lock) {

using (await _lock.LockAsync().AnyContext()) {
if (!_storage.ContainsKey(normalizedPath)) {
_logger.LogDebug("Error renaming {Path} to {NewPath}: File not found", normalizedPath, normalizedNewPath);
return Task.FromResult(false);
return false;
}

_storage[normalizedNewPath] = _storage[normalizedPath];
Expand All @@ -136,10 +140,10 @@ public Task<bool> RenameFileAsync(string path, string newPath, CancellationToken
_storage.Remove(normalizedPath);
}

return Task.FromResult(true);
return true;
}

public Task<bool> CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) {
public async Task<bool> CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
if (String.IsNullOrEmpty(targetPath))
Expand All @@ -148,72 +152,73 @@ public Task<bool> CopyFileAsync(string path, string targetPath, CancellationToke
string normalizedPath = path.NormalizePath();
string normalizedTargetPath = targetPath.NormalizePath();
_logger.LogInformation("Copying {Path} to {TargetPath}", normalizedPath, normalizedTargetPath);
lock (_lock) {

using (await _lock.LockAsync().AnyContext()) {
if (!_storage.ContainsKey(normalizedPath)) {
_logger.LogDebug("Error copying {Path} to {TargetPath}: File not found", normalizedPath, normalizedTargetPath);
return Task.FromResult(false);
return false;
}

_storage[normalizedTargetPath] = _storage[normalizedPath];
_storage[normalizedTargetPath].Item1.Path = normalizedTargetPath;
_storage[normalizedTargetPath].Item1.Modified = SystemClock.UtcNow;
}

return Task.FromResult(true);
return true;
}

public Task<bool> DeleteFileAsync(string path, CancellationToken cancellationToken = default) {
public async Task<bool> DeleteFileAsync(string path, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));

string normalizedPath = path.NormalizePath();
_logger.LogTrace("Deleting {Path}", normalizedPath);
lock (_lock) {

using (await _lock.LockAsync().AnyContext()) {
if (!_storage.ContainsKey(normalizedPath)) {
_logger.LogError("Unable to delete {Path}: File not found", normalizedPath);
return Task.FromResult(false);
return false;
}

_storage.Remove(normalizedPath);
}

return Task.FromResult(true);
return true;
}

public Task<int> DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) {
public async Task<int> DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) {
if (String.IsNullOrEmpty(searchPattern) || searchPattern == "*") {
lock(_lock)
using (await _lock.LockAsync().AnyContext()) {
_storage.Clear();
}

return Task.FromResult(0);
return 0;
}

searchPattern = searchPattern.NormalizePath();
int count = 0;

if (searchPattern[searchPattern.Length - 1] == Path.DirectorySeparatorChar)
if (searchPattern[searchPattern.Length - 1] == Path.DirectorySeparatorChar)
searchPattern = $"{searchPattern}*";
else if (!searchPattern.EndsWith(Path.DirectorySeparatorChar + "*") && !Path.HasExtension(searchPattern))
searchPattern = Path.Combine(searchPattern, "*");

var regex = new Regex($"^{Regex.Escape(searchPattern).Replace("\\*", ".*?")}$");
lock (_lock) {

using (await _lock.LockAsync().AnyContext()) {
var keys = _storage.Keys.Where(k => regex.IsMatch(k)).Select(k => _storage[k].Item1).ToList();

_logger.LogInformation("Deleting {FileCount} files matching {SearchPattern} (Regex={SearchPatternRegex})", keys.Count, searchPattern, regex);
foreach (var key in keys) {
_logger.LogTrace("Deleting {Path}", key.Path);
_storage.Remove(key.Path);
count++;
}

_logger.LogTrace("Finished deleting {FileCount} files matching {SearchPattern}", count, searchPattern);
}

return Task.FromResult(count);
return count;
}

public async Task<PagedFileListResult> GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) {
Expand All @@ -225,21 +230,21 @@ public async Task<PagedFileListResult> GetPagedFileListAsync(int pageSize = 100,

searchPattern = searchPattern.NormalizePath();

var result = new PagedFileListResult(s => Task.FromResult(GetFiles(searchPattern, 1, pageSize)));
var result = new PagedFileListResult(async s => await GetFilesAsync(searchPattern, 1, pageSize, cancellationToken));
await result.NextPageAsync().AnyContext();
return result;
}

private NextPageResult GetFiles(string searchPattern, int page, int pageSize) {
private async Task<NextPageResult> GetFilesAsync(string searchPattern, int page, int pageSize, CancellationToken cancellationToken = default) {
var list = new List<FileSpec>();
int pagingLimit = pageSize;
int skip = (page - 1) * pagingLimit;
if (pagingLimit < Int32.MaxValue)
pagingLimit++;

var regex = new Regex($"^{Regex.Escape(searchPattern).Replace("\\*", ".*?")}$");

lock (_lock) {
using (await _lock.LockAsync().AnyContext()) {
_logger.LogTrace(s => s.Property("Limit", pagingLimit).Property("Skip", skip), "Getting file list matching {SearchPattern}...", regex);
list.AddRange(_storage.Keys.Where(k => regex.IsMatch(k)).Select(k => _storage[k].Item1.DeepClone()).Skip(skip).Take(pagingLimit).ToList());
}
Expand All @@ -251,10 +256,10 @@ private NextPageResult GetFiles(string searchPattern, int page, int pageSize) {
}

return new NextPageResult {
Success = true,
HasMore = hasMore,
Success = true,
HasMore = hasMore,
Files = list,
NextPageFunc = hasMore ? _ => Task.FromResult(GetFiles(searchPattern, page + 1, pageSize)) : null
NextPageFunc = hasMore ? async _ => await GetFilesAsync(searchPattern, page + 1, pageSize, cancellationToken) : null
};
}

Expand Down

0 comments on commit 0cfb6f0

Please sign in to comment.