| | | 1 | | using Microsoft.Extensions.Configuration; |
| | | 2 | | using Microsoft.Extensions.Logging; |
| | | 3 | | using MySqlConnector; |
| | | 4 | | |
| | | 5 | | namespace OzonRepositories.Services; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Сервис для проверки доступности соединения с Bitrix базой данных. |
| | | 9 | | /// Кэширует результат проверки для оптимизации производительности. |
| | | 10 | | /// </summary> |
| | | 11 | | public interface IBitrixConnectionService |
| | | 12 | | { |
| | | 13 | | /// <summary> |
| | | 14 | | /// Проверяет доступность соединения с Bitrix (с кэшированием результата) |
| | | 15 | | /// </summary> |
| | | 16 | | Task<bool> IsConnectionAvailableAsync(); |
| | | 17 | | |
| | | 18 | | /// <summary> |
| | | 19 | | /// Принудительно обновляет кэш состояния соединения |
| | | 20 | | /// </summary> |
| | | 21 | | Task RefreshConnectionStatusAsync(); |
| | | 22 | | } |
| | | 23 | | |
| | | 24 | | public class BitrixConnectionService : IBitrixConnectionService |
| | | 25 | | { |
| | | 26 | | // Регрессия #2174, #2290: хостинг Bitrix изредка отвечает на новый вход «Access denied … to database» |
| | | 27 | | // (1044) при верных учётных данных — со второй/третьей попытки подключение проходит, рабочие запросы |
| | | 28 | | // BitrixContext при этом не падают. Повторные попытки сглаживают такие микросбои хостинга. |
| | 0 | 29 | | internal static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(1); |
| | | 30 | | private const int MaxAttempts = 3; |
| | | 31 | | |
| | | 32 | | private readonly string _connectionString; |
| | | 33 | | private readonly Func<CancellationToken, Task> _openConnection; |
| | | 34 | | private readonly TimeSpan _retryDelay; |
| | | 35 | | private readonly ILogger<BitrixConnectionService> _logger; |
| | | 36 | | private bool? _cachedStatus; |
| | 4 | 37 | | private DateTime _lastCheckTime = DateTime.MinValue; |
| | 4 | 38 | | private readonly TimeSpan _cacheTimeout = TimeSpan.FromMinutes(1); |
| | 4 | 39 | | private readonly SemaphoreSlim _checkSemaphore = new(1, 1); |
| | | 40 | | |
| | | 41 | | public BitrixConnectionService(IConfiguration configuration, ILogger<BitrixConnectionService> logger) |
| | 0 | 42 | | : this(configuration.GetConnectionString("BitrixConnectionProd") ?? string.Empty, null, RetryDelay, logger) |
| | | 43 | | { |
| | 0 | 44 | | } |
| | | 45 | | |
| | 4 | 46 | | internal BitrixConnectionService( |
| | 4 | 47 | | string connectionString, |
| | 4 | 48 | | Func<CancellationToken, Task>? openConnection, |
| | 4 | 49 | | TimeSpan retryDelay, |
| | 4 | 50 | | ILogger<BitrixConnectionService> logger) |
| | | 51 | | { |
| | 4 | 52 | | _connectionString = connectionString; |
| | 4 | 53 | | _openConnection = openConnection ?? OpenMySqlConnectionAsync; |
| | 4 | 54 | | _retryDelay = retryDelay; |
| | 4 | 55 | | _logger = logger; |
| | 4 | 56 | | } |
| | | 57 | | |
| | | 58 | | public async Task<bool> IsConnectionAvailableAsync() |
| | | 59 | | { |
| | 4 | 60 | | if (_cachedStatus.HasValue && DateTime.UtcNow - _lastCheckTime < _cacheTimeout) |
| | 0 | 61 | | return _cachedStatus.Value; |
| | | 62 | | |
| | 4 | 63 | | await _checkSemaphore.WaitAsync(); |
| | | 64 | | try |
| | | 65 | | { |
| | 4 | 66 | | if (_cachedStatus.HasValue && DateTime.UtcNow - _lastCheckTime < _cacheTimeout) |
| | 0 | 67 | | return _cachedStatus.Value; |
| | | 68 | | |
| | 4 | 69 | | _cachedStatus = await CheckConnectionInternalAsync(); |
| | 4 | 70 | | _lastCheckTime = DateTime.UtcNow; |
| | 4 | 71 | | return _cachedStatus.Value; |
| | | 72 | | } |
| | | 73 | | finally |
| | | 74 | | { |
| | 4 | 75 | | _checkSemaphore.Release(); |
| | | 76 | | } |
| | 4 | 77 | | } |
| | | 78 | | |
| | | 79 | | public async Task RefreshConnectionStatusAsync() |
| | | 80 | | { |
| | 0 | 81 | | await _checkSemaphore.WaitAsync(); |
| | | 82 | | try |
| | | 83 | | { |
| | 0 | 84 | | _cachedStatus = await CheckConnectionInternalAsync(); |
| | 0 | 85 | | _lastCheckTime = DateTime.UtcNow; |
| | 0 | 86 | | } |
| | | 87 | | finally |
| | | 88 | | { |
| | 0 | 89 | | _checkSemaphore.Release(); |
| | | 90 | | } |
| | 0 | 91 | | } |
| | | 92 | | |
| | | 93 | | private async Task<bool> CheckConnectionInternalAsync() |
| | | 94 | | { |
| | 4 | 95 | | if (string.IsNullOrWhiteSpace(_connectionString)) |
| | 1 | 96 | | return false; |
| | | 97 | | |
| | 8 | 98 | | for (var attempt = 1; ; attempt++) |
| | | 99 | | { |
| | | 100 | | try |
| | | 101 | | { |
| | 8 | 102 | | using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); |
| | 8 | 103 | | await _openConnection(cts.Token); |
| | 2 | 104 | | return true; |
| | | 105 | | } |
| | 6 | 106 | | catch (Exception ex) when (attempt < MaxAttempts) |
| | | 107 | | { |
| | 5 | 108 | | _logger.LogInformation("Bitrix connection check attempt {Attempt} failed, retrying: {Message}", attempt, |
| | 5 | 109 | | await Task.Delay(_retryDelay); |
| | | 110 | | } |
| | 1 | 111 | | catch (Exception ex) |
| | | 112 | | { |
| | 1 | 113 | | _logger.LogWarning(ex, "Bitrix connection check failed: {Message}", ex.Message); |
| | 1 | 114 | | return false; |
| | | 115 | | } |
| | | 116 | | } |
| | 4 | 117 | | } |
| | | 118 | | |
| | | 119 | | private async Task OpenMySqlConnectionAsync(CancellationToken cancellationToken) |
| | | 120 | | { |
| | 0 | 121 | | await using var connection = new MySqlConnection(_connectionString); |
| | 0 | 122 | | await connection.OpenAsync(cancellationToken); |
| | 0 | 123 | | } |
| | | 124 | | } |