Remote touch service for fileserver shards
This commit is contained in:
@@ -26,4 +26,21 @@ public class DistributionController : ControllerBase
|
|||||||
|
|
||||||
return File(fs, "application/octet-stream");
|
return File(fs, "application/octet-stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("touch")]
|
||||||
|
[Authorize(Policy = "Internal")]
|
||||||
|
public IActionResult TouchFiles([FromBody] string[] files)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"TouchFiles:{MareUser}:{files.Length}");
|
||||||
|
|
||||||
|
if (files.Length == 0)
|
||||||
|
return Ok();
|
||||||
|
|
||||||
|
Task.Run(() => {
|
||||||
|
foreach (var file in files)
|
||||||
|
_cachedFileProvider.TouchColdHash(file);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public sealed class CachedFileProvider : IDisposable
|
|||||||
private readonly FileStatisticsService _fileStatisticsService;
|
private readonly FileStatisticsService _fileStatisticsService;
|
||||||
private readonly MareMetrics _metrics;
|
private readonly MareMetrics _metrics;
|
||||||
private readonly ServerTokenGenerator _generator;
|
private readonly ServerTokenGenerator _generator;
|
||||||
|
private readonly ITouchHashService _touchService;
|
||||||
private readonly Uri _remoteCacheSourceUri;
|
private readonly Uri _remoteCacheSourceUri;
|
||||||
private readonly bool _useColdStorage;
|
private readonly bool _useColdStorage;
|
||||||
private readonly string _hotStoragePath;
|
private readonly string _hotStoragePath;
|
||||||
@@ -28,7 +29,7 @@ public sealed class CachedFileProvider : IDisposable
|
|||||||
private bool _isDistributionServer;
|
private bool _isDistributionServer;
|
||||||
|
|
||||||
public CachedFileProvider(IConfigurationService<StaticFilesServerConfiguration> configuration, ILogger<CachedFileProvider> logger,
|
public CachedFileProvider(IConfigurationService<StaticFilesServerConfiguration> configuration, ILogger<CachedFileProvider> logger,
|
||||||
FileStatisticsService fileStatisticsService, MareMetrics metrics, ServerTokenGenerator generator)
|
FileStatisticsService fileStatisticsService, MareMetrics metrics, ServerTokenGenerator generator, ITouchHashService touchService)
|
||||||
{
|
{
|
||||||
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
@@ -36,6 +37,7 @@ public sealed class CachedFileProvider : IDisposable
|
|||||||
_fileStatisticsService = fileStatisticsService;
|
_fileStatisticsService = fileStatisticsService;
|
||||||
_metrics = metrics;
|
_metrics = metrics;
|
||||||
_generator = generator;
|
_generator = generator;
|
||||||
|
_touchService = touchService;
|
||||||
_remoteCacheSourceUri = configuration.GetValueOrDefault<Uri>(nameof(StaticFilesServerConfiguration.DistributionFileServerAddress), null);
|
_remoteCacheSourceUri = configuration.GetValueOrDefault<Uri>(nameof(StaticFilesServerConfiguration.DistributionFileServerAddress), null);
|
||||||
_isDistributionServer = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.IsDistributionNode), false);
|
_isDistributionServer = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.IsDistributionNode), false);
|
||||||
_useColdStorage = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false);
|
_useColdStorage = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false);
|
||||||
@@ -111,16 +113,15 @@ public sealed class CachedFileProvider : IDisposable
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(_coldStoragePath)) return false;
|
if (string.IsNullOrEmpty(_coldStoragePath)) return false;
|
||||||
|
|
||||||
var coldStorageFilePath = FilePathUtil.GetFileInfoForHash(_coldStoragePath, hash);
|
var coldStorageFilePath = FilePathUtil.GetFilePath(_coldStoragePath, hash);
|
||||||
if (coldStorageFilePath == null) return false;
|
if (coldStorageFilePath == null) return false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Copying {hash} from cold storage: {path}", hash, coldStorageFilePath);
|
_logger.LogDebug("Copying {hash} from cold storage: {path}", hash, coldStorageFilePath);
|
||||||
var tempFileName = destinationFilePath + ".dl";
|
var tempFileName = destinationFilePath + ".dl";
|
||||||
File.Copy(coldStorageFilePath.FullName, tempFileName, true);
|
File.Copy(coldStorageFilePath, tempFileName, true);
|
||||||
File.Move(tempFileName, destinationFilePath, true);
|
File.Move(tempFileName, destinationFilePath, true);
|
||||||
coldStorageFilePath.LastAccessTimeUtc = DateTime.UtcNow;
|
|
||||||
var destinationFile = new FileInfo(destinationFilePath);
|
var destinationFile = new FileInfo(destinationFilePath);
|
||||||
destinationFile.LastAccessTimeUtc = DateTime.UtcNow;
|
destinationFile.LastAccessTimeUtc = DateTime.UtcNow;
|
||||||
destinationFile.CreationTimeUtc = DateTime.UtcNow;
|
destinationFile.CreationTimeUtc = DateTime.UtcNow;
|
||||||
@@ -180,9 +181,10 @@ public sealed class CachedFileProvider : IDisposable
|
|||||||
{
|
{
|
||||||
var fi = FilePathUtil.GetFileInfoForHash(_hotStoragePath, hash);
|
var fi = FilePathUtil.GetFileInfoForHash(_hotStoragePath, hash);
|
||||||
if (fi == null) return null;
|
if (fi == null) return null;
|
||||||
|
|
||||||
fi.LastAccessTimeUtc = DateTime.UtcNow;
|
fi.LastAccessTimeUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
_touchService.TouchColdHash(hash);
|
||||||
|
|
||||||
_fileStatisticsService.LogFile(hash, fi.Length);
|
_fileStatisticsService.LogFile(hash, fi.Length);
|
||||||
|
|
||||||
return new FileStream(fi.FullName, FileMode.Open, FileAccess.Read, FileShare.Inheritable | FileShare.Read);
|
return new FileStream(fi.FullName, FileMode.Open, FileAccess.Read, FileShare.Inheritable | FileShare.Read);
|
||||||
@@ -215,6 +217,11 @@ public sealed class CachedFileProvider : IDisposable
|
|||||||
return GetLocalFileStream(hash);
|
return GetLocalFileStream(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TouchColdHash(string hash)
|
||||||
|
{
|
||||||
|
_touchService.TouchColdHash(hash);
|
||||||
|
}
|
||||||
|
|
||||||
public bool AnyFilesDownloading(List<string> hashes)
|
public bool AnyFilesDownloading(List<string> hashes)
|
||||||
{
|
{
|
||||||
return hashes.Exists(_currentTransfers.Keys.Contains);
|
return hashes.Exists(_currentTransfers.Keys.Contains);
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using MareSynchronosShared.Services;
|
||||||
|
using MareSynchronosStaticFilesServer.Utils;
|
||||||
|
|
||||||
|
namespace MareSynchronosStaticFilesServer.Services;
|
||||||
|
|
||||||
|
// Perform access time updates for cold cache files accessed via hot cache or shard servers
|
||||||
|
public class ColdTouchHashService : ITouchHashService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ColdTouchHashService> _logger;
|
||||||
|
private readonly IConfigurationService<StaticFilesServerConfiguration> _configuration;
|
||||||
|
|
||||||
|
private readonly bool _useColdStorage;
|
||||||
|
private readonly string _coldStoragePath;
|
||||||
|
|
||||||
|
// Debounce multiple updates towards the same file
|
||||||
|
private readonly Dictionary<string, DateTime> _lastUpdateTimesUtc = new(1009, StringComparer.Ordinal);
|
||||||
|
private int _cleanupCounter = 0;
|
||||||
|
private const double _debounceTimeSecs = 90.0;
|
||||||
|
|
||||||
|
public ColdTouchHashService(ILogger<ColdTouchHashService> logger, IConfigurationService<StaticFilesServerConfiguration> configuration)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
_useColdStorage = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false);
|
||||||
|
_coldStoragePath = configuration.GetValue<string>(nameof(StaticFilesServerConfiguration.ColdStorageDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TouchColdHash(string hash)
|
||||||
|
{
|
||||||
|
if (!_useColdStorage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Clean up debounce dictionary regularly
|
||||||
|
if (_cleanupCounter++ >= 1000)
|
||||||
|
{
|
||||||
|
foreach (var entry in _lastUpdateTimesUtc.Where(entry => (nowUtc - entry.Value).TotalSeconds >= _debounceTimeSecs).ToList())
|
||||||
|
_lastUpdateTimesUtc.Remove(entry.Key);
|
||||||
|
_cleanupCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore multiple updates within a 90 second window of the first
|
||||||
|
if (_lastUpdateTimesUtc.TryGetValue(hash, out var lastUpdateTimeUtc) && (nowUtc - lastUpdateTimeUtc).TotalSeconds < _debounceTimeSecs)
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Debounced touch for {hash}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInfo = FilePathUtil.GetFileInfoForHash(_coldStoragePath, hash);
|
||||||
|
if (fileInfo != null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Touching {fileInfo.Name}");
|
||||||
|
fileInfo.LastAccessTimeUtc = nowUtc;
|
||||||
|
_lastUpdateTimesUtc.TryAdd(hash, nowUtc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace MareSynchronosStaticFilesServer.Services;
|
||||||
|
|
||||||
|
public interface ITouchHashService : IHostedService
|
||||||
|
{
|
||||||
|
void TouchColdHash(string hash);
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using MareSynchronos.API.Routes;
|
||||||
|
using MareSynchronosShared.Services;
|
||||||
|
using MareSynchronosShared.Utils;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace MareSynchronosStaticFilesServer.Services;
|
||||||
|
|
||||||
|
// Notify distribution server of file hashes downloaded via shards, so they are not prematurely purged from its cold cache
|
||||||
|
public class ShardTouchMessageService : ITouchHashService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ShardTouchMessageService> _logger;
|
||||||
|
private readonly ServerTokenGenerator _tokenGenerator;
|
||||||
|
private readonly IConfigurationService<StaticFilesServerConfiguration> _configuration;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly Uri _remoteCacheSourceUri;
|
||||||
|
private readonly HashSet<string> _touchHashSet = new();
|
||||||
|
private readonly ColdTouchHashService _nestedService = null;
|
||||||
|
|
||||||
|
private CancellationTokenSource _touchmsgCts;
|
||||||
|
|
||||||
|
public ShardTouchMessageService(ILogger<ShardTouchMessageService> logger, ILogger<ColdTouchHashService> nestedLogger,
|
||||||
|
ServerTokenGenerator tokenGenerator, IConfigurationService<StaticFilesServerConfiguration> configuration)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_tokenGenerator = tokenGenerator;
|
||||||
|
_configuration = configuration;
|
||||||
|
_remoteCacheSourceUri = _configuration.GetValueOrDefault<Uri>(nameof(StaticFilesServerConfiguration.DistributionFileServerAddress), null);
|
||||||
|
_httpClient = new();
|
||||||
|
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronosServer", "1.0.0.0"));
|
||||||
|
|
||||||
|
if (configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false))
|
||||||
|
{
|
||||||
|
_nestedService = new ColdTouchHashService(nestedLogger, configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_remoteCacheSourceUri == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
_logger.LogInformation("Touch Message Service started");
|
||||||
|
|
||||||
|
_touchmsgCts = new();
|
||||||
|
|
||||||
|
_ = TouchMessageTask(_touchmsgCts.Token);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_remoteCacheSourceUri == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
_touchmsgCts.Cancel();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendTouches(IEnumerable<string> hashes)
|
||||||
|
{
|
||||||
|
var mainUrl = _remoteCacheSourceUri;
|
||||||
|
var path = new Uri(mainUrl, MareFiles.Distribution + "/touch");
|
||||||
|
using HttpRequestMessage msg = new()
|
||||||
|
{
|
||||||
|
RequestUri = path
|
||||||
|
};
|
||||||
|
msg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenGenerator.Token);
|
||||||
|
msg.Method = HttpMethod.Post;
|
||||||
|
msg.Content = JsonContent.Create(hashes);
|
||||||
|
if (_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DistributionFileServerForceHTTP2), false))
|
||||||
|
{
|
||||||
|
msg.Version = new Version(2, 0);
|
||||||
|
msg.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Sending remote touch to {path}", path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var result = await _httpClient.SendAsync(msg).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failure to send touches for {hashChunk}", hashes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TouchMessageTask(CancellationToken ct)
|
||||||
|
{
|
||||||
|
List<string> hashes;
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock (_touchHashSet)
|
||||||
|
{
|
||||||
|
hashes = _touchHashSet.ToList();
|
||||||
|
_touchHashSet.Clear();
|
||||||
|
}
|
||||||
|
if (hashes.Count > 0)
|
||||||
|
await SendTouches(hashes);
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Error during touch message task");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_touchHashSet)
|
||||||
|
{
|
||||||
|
hashes = _touchHashSet.ToList();
|
||||||
|
_touchHashSet.Clear();
|
||||||
|
}
|
||||||
|
if (hashes.Count > 0)
|
||||||
|
await SendTouches(hashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TouchColdHash(string hash)
|
||||||
|
{
|
||||||
|
if (_nestedService != null)
|
||||||
|
_nestedService.TouchColdHash(hash);
|
||||||
|
|
||||||
|
lock (_touchHashSet)
|
||||||
|
{
|
||||||
|
_touchHashSet.Add(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -175,6 +175,17 @@ public class Startup
|
|||||||
services.AddHostedService(p => (MareConfigurationServiceClient<StaticFilesServerConfiguration>)p.GetService<IConfigurationService<StaticFilesServerConfiguration>>());
|
services.AddHostedService(p => (MareConfigurationServiceClient<StaticFilesServerConfiguration>)p.GetService<IConfigurationService<StaticFilesServerConfiguration>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_isDistributionNode)
|
||||||
|
{
|
||||||
|
services.AddSingleton<ITouchHashService, ColdTouchHashService>();
|
||||||
|
services.AddHostedService(p => p.GetService<ITouchHashService>());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddSingleton<ITouchHashService, ShardTouchMessageService>();
|
||||||
|
services.AddHostedService(p => p.GetService<ITouchHashService>());
|
||||||
|
}
|
||||||
|
|
||||||
// controller setup
|
// controller setup
|
||||||
services.AddControllers().ConfigureApplicationPartManager(a =>
|
services.AddControllers().ConfigureApplicationPartManager(a =>
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user