diff --git a/MareSynchronosServer/.editorconfig b/MareSynchronosServer/.editorconfig index 6cf5454..d35a212 100644 --- a/MareSynchronosServer/.editorconfig +++ b/MareSynchronosServer/.editorconfig @@ -2,3 +2,166 @@ # MA0048: File name must match type name dotnet_diagnostic.MA0048.severity = suggestion +csharp_indent_labels = one_less_than_current +csharp_space_around_binary_operators = before_and_after +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:suggestion +csharp_style_namespace_declarations = file_scoped:silent +csharp_style_prefer_method_group_conversion = true:suggestion +csharp_style_prefer_top_level_statements = false:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_qualification_for_field = false:silent +[*.cs] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = warning +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.private_or_internal_field_should_be_fieldstyle.severity = warning +dotnet_naming_rule.private_or_internal_field_should_be_fieldstyle.symbols = private_or_internal_field +dotnet_naming_rule.private_or_internal_field_should_be_fieldstyle.style = fieldstyle + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected +dotnet_naming_symbols.private_or_internal_field.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.fieldstyle.required_prefix = _ +dotnet_naming_style.fieldstyle.required_suffix = +dotnet_naming_style.fieldstyle.word_separator = +dotnet_naming_style.fieldstyle.capitalization = camel_case + +[*.vb] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case diff --git a/MareSynchronosServer/MareSynchronosShared/Services/IConfigurationService.cs b/MareSynchronosServer/MareSynchronosShared/Services/IConfigurationService.cs index 85c21e6..36c8787 100644 --- a/MareSynchronosServer/MareSynchronosShared/Services/IConfigurationService.cs +++ b/MareSynchronosServer/MareSynchronosShared/Services/IConfigurationService.cs @@ -5,6 +5,9 @@ namespace MareSynchronosShared.Services; public interface IConfigurationService where T : class, IMareConfiguration { bool IsMain { get; } + + event EventHandler ConfigChangedEvent; + T1 GetValue(string key); T1 GetValueOrDefault(string key, T1 defaultValue); string ToString(); diff --git a/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceClient.cs b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceClient.cs index 6eaff9f..248b8f5 100644 --- a/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceClient.cs +++ b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceClient.cs @@ -22,6 +22,8 @@ public class MareConfigurationServiceClient : IHostedService, IConfigurationS private readonly CancellationTokenSource _updateTaskCts = new(); private bool _initialized = false; private readonly HttpClient _httpClient; + public event EventHandler ConfigChangedEvent; + private IDisposable _onChanged; private Uri GetRoute(string key, string value) { @@ -39,6 +41,7 @@ public class MareConfigurationServiceClient : IHostedService, IConfigurationS public MareConfigurationServiceClient(ILogger> logger, IOptionsMonitor config, ServerTokenGenerator serverTokenGenerator) { + _onChanged = _config.OnChange((c) => { ConfigChangedEvent?.Invoke(this, EventArgs.Empty); }); _config = config; _logger = logger; _serverTokenGenerator = serverTokenGenerator; @@ -184,6 +187,7 @@ public class MareConfigurationServiceClient : IHostedService, IConfigurationS { _updateTaskCts.Cancel(); _httpClient.Dispose(); + _onChanged?.Dispose(); return Task.CompletedTask; } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs index 657f5a4..d8588f8 100644 --- a/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs +++ b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs @@ -6,13 +6,18 @@ using System.Text; namespace MareSynchronosShared.Services; -public class MareConfigurationServiceServer : IConfigurationService where T : class, IMareConfiguration +public sealed class MareConfigurationServiceServer : IDisposable, IConfigurationService where T : class, IMareConfiguration { private readonly IOptionsMonitor _config; + private bool _disposed; + public bool IsMain => true; + public event EventHandler ConfigChangedEvent; + private IDisposable _onChanged; public MareConfigurationServiceServer(IOptionsMonitor config) { + _onChanged = _config.OnChange((c) => { ConfigChangedEvent?.Invoke(this, EventArgs.Empty); }); _config = config; } @@ -48,4 +53,15 @@ public class MareConfigurationServiceServer : IConfigurationService where } return sb.ToString(); } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _onChanged.Dispose(); + _disposed = true; + } } diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/CdnShardConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/CdnShardConfiguration.cs deleted file mode 100644 index 32ab367..0000000 --- a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/CdnShardConfiguration.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MareSynchronosShared.Utils.Configuration; - -public class CdnShardConfiguration -{ - public List Continents { get; set; } - public string FileMatch { get; set; } - public Uri CdnFullUrl { get; set; } - - public override string ToString() - { - return CdnFullUrl.ToString() + "[" + string.Join(',', Continents) + "] == " + FileMatch; - } -} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ShardConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ShardConfiguration.cs new file mode 100644 index 0000000..e85d5af --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ShardConfiguration.cs @@ -0,0 +1,8 @@ +namespace MareSynchronosShared.Utils.Configuration; + +public class ShardConfiguration +{ + public List Continents { get; set; } + public string FileMatch { get; set; } + public Dictionary RegionUris { get; set; } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/StaticFilesServerConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/StaticFilesServerConfiguration.cs index f199dd1..850e942 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/StaticFilesServerConfiguration.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/StaticFilesServerConfiguration.cs @@ -1,5 +1,4 @@ -using MareSynchronosShared.Utils; -using System.Text; +using System.Text; namespace MareSynchronosShared.Utils.Configuration; @@ -25,8 +24,7 @@ public class StaticFilesServerConfiguration : MareConfigurationBase public double SpeedTestHoursRateLimit { get; set; } = 0.5; [RemoteConfiguration] public Uri CdnFullUrl { get; set; } = null; - [RemoteConfiguration] - public List CdnShardConfiguration { get; set; } = new(); + public ShardConfiguration? ShardConfiguration { get; set; } = null; public override string ToString() { StringBuilder sb = new(); @@ -42,7 +40,6 @@ public class StaticFilesServerConfiguration : MareConfigurationBase sb.AppendLine($"{nameof(CacheDirectory)} => {CacheDirectory}"); sb.AppendLine($"{nameof(DownloadQueueSize)} => {DownloadQueueSize}"); sb.AppendLine($"{nameof(DownloadQueueReleaseSeconds)} => {DownloadQueueReleaseSeconds}"); - sb.AppendLine($"{nameof(CdnShardConfiguration)} => {string.Join(", ", CdnShardConfiguration)}"); return sb.ToString(); } } diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/MainController.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/MainController.cs index f0ceb7b..7e85780 100644 --- a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/MainController.cs +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/MainController.cs @@ -1,4 +1,5 @@ using MareSynchronos.API.Routes; +using MareSynchronosShared.Utils.Configuration; using MareSynchronosStaticFilesServer.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -6,20 +7,60 @@ using Microsoft.AspNetCore.Mvc; namespace MareSynchronosStaticFilesServer.Controllers; [Route(MareFiles.Main)] +[Authorize(Policy = "Internal")] public class MainController : ControllerBase { private readonly IClientReadyMessageService _messageService; + private readonly MainServerShardRegistrationService _shardRegistrationService; - public MainController(ILogger logger, IClientReadyMessageService mareHub) : base(logger) + public MainController(ILogger logger, IClientReadyMessageService mareHub, + MainServerShardRegistrationService shardRegistrationService) : base(logger) { _messageService = mareHub; + _shardRegistrationService = shardRegistrationService; } [HttpGet(MareFiles.Main_SendReady)] - [Authorize(Policy = "Internal")] public async Task SendReadyToClients(string uid, Guid requestId) { await _messageService.SendDownloadReady(uid, requestId).ConfigureAwait(false); return Ok(); } + + [HttpPost("shardRegister")] + public IActionResult RegisterShard([FromBody] ShardConfiguration shardConfiguration) + { + try + { + _shardRegistrationService.RegisterShard(MareUser, shardConfiguration); + return Ok(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Shard could not be registered {shard}", MareUser); + return BadRequest(); + } + } + + [HttpPost("shardUnregister")] + public IActionResult UnregisterShard() + { + _shardRegistrationService.UnregisterShard(MareUser); + return Ok(); + } + + [HttpPost("shardHeartbeat")] + public IActionResult ShardHeartbeat() + { + try + { + _shardRegistrationService.ShardHeartbeat(MareUser); + return Ok(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Shard not registered: {shard}", MareUser); + return BadRequest(); + } + } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs index ba24df2..54507b6 100644 --- a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs @@ -31,11 +31,13 @@ public class ServerFilesController : ControllerBase private readonly IHubContext _hubContext; private readonly IDbContextFactory _mareDbContext; private readonly MareMetrics _metricsClient; + private readonly MainServerShardRegistrationService _shardRegistrationService; public ServerFilesController(ILogger logger, CachedFileProvider cachedFileProvider, IConfigurationService configuration, IHubContext hubContext, - IDbContextFactory mareDbContext, MareMetrics metricsClient) : base(logger) + IDbContextFactory mareDbContext, MareMetrics metricsClient, + MainServerShardRegistrationService shardRegistrationService) : base(logger) { _basePath = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false) ? configuration.GetValue(nameof(StaticFilesServerConfiguration.ColdStorageDirectory)) @@ -45,6 +47,7 @@ public class ServerFilesController : ControllerBase _hubContext = hubContext; _mareDbContext = mareDbContext; _metricsClient = metricsClient; + _shardRegistrationService = shardRegistrationService; } [HttpPost(MareFiles.ServerFiles_DeleteAll)] @@ -85,7 +88,7 @@ public class ServerFilesController : ControllerBase .Select(k => new { k.Hash, k.Size, k.RawSize }) .ToListAsync().ConfigureAwait(false); - var allFileShards = new List(_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CdnShardConfiguration), new List())); + var allFileShards = _shardRegistrationService.GetConfigurationsByContinent(Continent); foreach (var file in cacheFile) { @@ -94,25 +97,12 @@ public class ServerFilesController : ControllerBase if (forbiddenFile == null) { - List selectedShards = new(); var matchingShards = allFileShards.Where(f => new Regex(f.FileMatch).IsMatch(file.Hash)).ToList(); - if (string.Equals(Continent, "*", StringComparison.Ordinal)) - { - selectedShards = matchingShards; - } - else - { - selectedShards = matchingShards.Where(c => c.Continents.Contains(Continent, StringComparer.OrdinalIgnoreCase)).ToList(); - if (!selectedShards.Any()) selectedShards = matchingShards; - } + var shard = matchingShards.SelectMany(g => g.RegionUris) + .OrderBy(g => Guid.NewGuid()).FirstOrDefault(); - var shard = selectedShards - .OrderBy(s => !s.Continents.Any() ? 0 : 1) - .ThenBy(s => s.Continents.Contains("*", StringComparer.Ordinal) ? 0 : 1) - .ThenBy(g => Guid.NewGuid()).FirstOrDefault(); - - baseUrl = shard?.CdnFullUrl ?? _configuration.GetValue(nameof(StaticFilesServerConfiguration.CdnFullUrl)); + baseUrl = shard.Value ?? _configuration.GetValue(nameof(StaticFilesServerConfiguration.CdnFullUrl)); } response.Add(new DownloadFileDto @@ -133,15 +123,8 @@ public class ServerFilesController : ControllerBase [HttpGet(MareFiles.ServerFiles_DownloadServers)] public async Task GetDownloadServers() { - var allFileShards = new List(_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CdnShardConfiguration), new List())) - .DistinctBy(f => f.CdnFullUrl).ToList(); - if (!allFileShards.Any()) - { - return Ok(JsonSerializer.Serialize(new List { _configuration.GetValue(nameof(StaticFilesServerConfiguration.CdnFullUrl)).ToString() })); - } - var selectedShards = allFileShards.Where(c => c.Continents.Contains(Continent, StringComparer.OrdinalIgnoreCase)).ToList(); - if (!selectedShards.Any()) selectedShards = allFileShards.Where(c => c.Continents.Contains("*", StringComparer.Ordinal)).ToList(); - return Ok(JsonSerializer.Serialize(selectedShards.Select(t => t.CdnFullUrl.ToString()))); + var allFileShards = _shardRegistrationService.GetConfigurationsByContinent(Continent); + return Ok(JsonSerializer.Serialize(allFileShards.SelectMany(t => t.RegionUris.Select(v => v.Value.ToString())))); } [HttpPost(MareFiles.ServerFiles_FilesSend)] diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/MainServerShardRegistrationService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/MainServerShardRegistrationService.cs new file mode 100644 index 0000000..cdd7134 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/MainServerShardRegistrationService.cs @@ -0,0 +1,96 @@ +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using System.Collections.Concurrent; +using System.Collections.Frozen; + +namespace MareSynchronosStaticFilesServer.Services; + +public class MainServerShardRegistrationService : IHostedService +{ + private readonly ILogger _logger; + private readonly IConfigurationService _configurationService; + private readonly ConcurrentDictionary _shardConfigs = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary _shardHeartbeats = new(StringComparer.Ordinal); + private readonly CancellationTokenSource _periodicCheckCts = new(); + + public MainServerShardRegistrationService(ILogger logger, + IConfigurationService configurationService) + { + _logger = logger; + _configurationService = configurationService; + } + + public void RegisterShard(string shardName, ShardConfiguration shardConfiguration) + { + if (shardConfiguration == null || shardConfiguration == default) + throw new InvalidOperationException("Empty configuration provided"); + + if (_shardConfigs.ContainsKey(shardName)) + _logger.LogInformation("Re-Registering Shard {name}", shardName); + else + _logger.LogInformation("Registering Shard {name}", shardName); + + _shardHeartbeats[shardName] = DateTime.UtcNow; + _shardConfigs[shardName] = shardConfiguration; + } + + public void UnregisterShard(string shardName) + { + _logger.LogInformation("Unregistering Shard {name}", shardName); + + _shardHeartbeats.TryRemove(shardName, out _); + _shardConfigs.TryRemove(shardName, out _); + } + + public List GetConfigurationsByContinent(string continent) + { + var shardConfigs = _shardConfigs.Values.Where(v => v.Continents.Contains(continent, StringComparer.OrdinalIgnoreCase)).ToList(); + if (shardConfigs.Any()) return shardConfigs; + shardConfigs = _shardConfigs.Values.Where(v => v.Continents.Contains("*", StringComparer.OrdinalIgnoreCase)).ToList(); + if (shardConfigs.Any()) return shardConfigs; + return [new ShardConfiguration() { + Continents = ["*"], + FileMatch = ".*", + RegionUris = new(StringComparer.Ordinal) { + { "Central", _configurationService.GetValue(nameof(StaticFilesServerConfiguration.CdnFullUrl)) } + } }]; + } + + public void ShardHeartbeat(string shardName) + { + if (!_shardConfigs.ContainsKey(shardName)) + throw new InvalidOperationException("Shard not registered"); + + _logger.LogInformation("Heartbeat from {name}", shardName); + _shardHeartbeats[shardName] = DateTime.UtcNow; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = Task.Run(() => PeriodicHeartbeatCleanup(_periodicCheckCts.Token), cancellationToken).ConfigureAwait(false); + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _periodicCheckCts.CancelAsync().ConfigureAwait(false); + _periodicCheckCts.Dispose(); + } + + private async Task PeriodicHeartbeatCleanup(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + foreach (var kvp in _shardHeartbeats.ToFrozenDictionary()) + { + if (DateTime.UtcNow.Subtract(kvp.Value) > TimeSpan.FromMinutes(1)) + { + _shardHeartbeats.TryRemove(kvp.Key, out _); + _shardConfigs.TryRemove(kvp.Key, out _); + } + } + + await Task.Delay(5000, ct).ConfigureAwait(false); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardRegistrationService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardRegistrationService.cs new file mode 100644 index 0000000..1c028a1 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardRegistrationService.cs @@ -0,0 +1,107 @@ +using MareSynchronos.API.Routes; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils; +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosStaticFilesServer.Services; + +public class ShardRegistrationService : IHostedService +{ + private readonly ILogger _logger; + private readonly IConfigurationService _configurationService; + private readonly HttpClient _httpClient = new(); + private readonly CancellationTokenSource _heartBeatCts = new(); + private bool _isRegistered = false; + + public ShardRegistrationService(ILogger logger, + IConfigurationService configurationService, + ServerTokenGenerator serverTokenGenerator) + { + _logger = logger; + _configurationService = configurationService; + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", serverTokenGenerator.Token); + } + + private void OnConfigChanged(object sender, EventArgs e) + { + _isRegistered = false; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting"); + _configurationService.ConfigChangedEvent += OnConfigChanged; + _ = Task.Run(() => HeartbeatLoop(_heartBeatCts.Token)); + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stopping"); + + _configurationService.ConfigChangedEvent -= OnConfigChanged; + _heartBeatCts.Cancel(); + _heartBeatCts.Dispose(); + // call unregister + await UnregisterShard().ConfigureAwait(false); + _httpClient.Dispose(); + } + + private async Task HeartbeatLoop(CancellationToken ct) + { + while (!_heartBeatCts.IsCancellationRequested) + { + try + { + await ProcessHeartbeat(ct).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Issue during Heartbeat"); + _isRegistered = false; + } + + await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false); + } + } + + private async Task ProcessHeartbeat(CancellationToken ct) + { + if (!_isRegistered) + { + await TryRegisterShard(ct).ConfigureAwait(false); + } + + await ShardHeartbeat(ct).ConfigureAwait(false); + } + + private async Task ShardHeartbeat(CancellationToken ct) + { + Uri mainServer = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.MainFileServerAddress)); + _logger.LogInformation("Running heartbeat against Main {server}", mainServer); + + using var heartBeat = await _httpClient.PostAsync(new Uri(mainServer, MareFiles.Main + "/shardHeartbeat"), null, ct).ConfigureAwait(false); + heartBeat.EnsureSuccessStatusCode(); + } + + private async Task TryRegisterShard(CancellationToken ct) + { + Uri mainServer = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.MainFileServerAddress)); + _logger.LogInformation("Registering Shard with Main {server}", mainServer); + var config = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.ShardConfiguration)); + _logger.LogInformation("Config Value {varName}: {value}", nameof(ShardConfiguration.Continents), string.Join(", ", config.Continents)); + _logger.LogInformation("Config Value {varName}: {value}", nameof(ShardConfiguration.FileMatch), config.FileMatch); + _logger.LogInformation("Config Value {varName}: {value}", nameof(ShardConfiguration.RegionUris), string.Join("; ", config.RegionUris.Select(k => k.Key + ":" + k.Value))); + + using var register = await _httpClient.PostAsJsonAsync(new Uri(mainServer, MareFiles.Main + "/shardRegister"), config, ct).ConfigureAwait(false); + register.EnsureSuccessStatusCode(); + _isRegistered = true; + } + + private async Task UnregisterShard() + { + Uri mainServer = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.MainFileServerAddress)); + _logger.LogInformation("Unregistering Shard with Main {server}", mainServer); + using var heartBeat = await _httpClient.PostAsync(new Uri(mainServer, MareFiles.Main + "/shardUnregister"), null).ConfigureAwait(false); + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs index 2d08098..fa4e191 100644 --- a/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs @@ -98,6 +98,8 @@ public class Startup services.AddSingleton(); services.AddHostedService(); services.AddSingleton, MareConfigurationServiceServer>(); + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); services.AddDbContextPool(options => { options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => @@ -180,6 +182,8 @@ public class Startup } else { + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); services.AddSingleton(); services.AddHostedService(); services.AddSingleton, MareConfigurationServiceClient>();