| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using OzonRepositories.Context; |
| | | 3 | | using OzonRepositories.Context.Identity; |
| | | 4 | | using OzonRepositories.Utils; |
| | | 5 | | using OzonOrdersWeb.Observability; |
| | | 6 | | using Pomelo.EntityFrameworkCore.MySql.Infrastructure; |
| | | 7 | | using Services.DataServices.TriggerSelfHealing; |
| | | 8 | | |
| | | 9 | | namespace OzonOrdersWeb.Extensions; |
| | | 10 | | |
| | | 11 | | public static class DatabaseExtensions |
| | | 12 | | { |
| | | 13 | | public static void AddDatabaseServices(this IServiceCollection services, IConfiguration configuration, bool isProd) |
| | | 14 | | { |
| | 0 | 15 | | var slowDbInterceptor = new SlowDbCommandInterceptor( |
| | 0 | 16 | | configuration.GetValue<int?>($"{ObservabilityOptions.SectionName}:SlowDependencyMilliseconds") ?? 1_000); |
| | 0 | 17 | | string sqlConnection = (isProd |
| | 0 | 18 | | ? configuration.GetConnectionString("MySqlConnectionProd") |
| | 0 | 19 | | : configuration.GetConnectionString("MySqlConnectionLocal")) |
| | 0 | 20 | | ?? throw new InvalidOperationException("MySQL connection string is not configured."); |
| | | 21 | | |
| | 0 | 22 | | string jcEtalonConnection = configuration.GetConnectionString("JcEtalonConnectionProd") |
| | 0 | 23 | | ?? throw new InvalidOperationException("JcEtalon connection string is not configured."); |
| | | 24 | | |
| | 0 | 25 | | string bitrixConnection = (isProd |
| | 0 | 26 | | ? configuration.GetConnectionString("BitrixConnectionProd") |
| | 0 | 27 | | : configuration.GetConnectionString("BitrixConnectionLocal") ?? configuration.GetConnectionString("BitrixCon |
| | 0 | 28 | | ?? throw new InvalidOperationException("Bitrix connection string is not configured."); |
| | | 29 | | |
| | 0 | 30 | | string ozonApiConnection = configuration.GetConnectionString("OzonApiConnectionProd") |
| | 0 | 31 | | ?? throw new InvalidOperationException("OzonApi connection string is not configured."); |
| | | 32 | | |
| | | 33 | | // MariaDB 11.4 на 192.168.1.111 (ветка local-deploy) |
| | 0 | 34 | | var serverVersion = St2MariaDb.ServerVersion; |
| | | 35 | | |
| | | 36 | | // Самолечение триггеров ST2 (MariaDB 1711 после KILL QUERY, см. TriggerBodyErrorHealer): |
| | | 37 | | // интерцептор перезагружает триггеры, ретрай-стратегия повторяет упавшую команду. |
| | 0 | 38 | | var triggerHealer = new TriggerBodyErrorHealer(new MySqlTriggerReloader(sqlConnection), TimeSpan.FromSeconds(30) |
| | 0 | 39 | | var triggerHealingInterceptor = new TriggerBodyHealingInterceptor(triggerHealer); |
| | 0 | 40 | | services.AddSingleton(triggerHealer); |
| | 0 | 41 | | int[] st2RetryErrors = [TriggerBodyErrorHealer.ErrorInUnknownTriggerBody]; |
| | | 42 | | |
| | | 43 | | // --- MySQL: ApplicationDbContext. |
| | | 44 | | // DataScope-ы применяются явно в контроллерах через IDataScopeService, поэтому |
| | | 45 | | // никаких глобальных EF-фильтров и специального ModelCacheKeyFactory здесь не нужно. |
| | | 46 | | // |
| | | 47 | | // ApplicationDbContext (DI) + ApplicationDbContext (Factory) + OzonIdentityOrderContext |
| | | 48 | | // используют одну строку подключения MySqlConnectionProd и делят один пул коннектора. |
| | | 49 | | // MaximumPoolSize=50 в строке подключения — запас на пиковую конкурентность всех трёх контекстов. |
| | | 50 | | // Hangfire использует отдельную строку подключения с собственным пулом (MaximumPoolSize=12). |
| | | 51 | | // MySQL default max_connections=151 — оставляем запас для мониторинга и прямых подключений. |
| | | 52 | | |
| | 0 | 53 | | services.AddDbContextPool<ApplicationDbContext>(options => |
| | 0 | 54 | | { |
| | 0 | 55 | | options.AddInterceptors(slowDbInterceptor, triggerHealingInterceptor); |
| | 0 | 56 | | options.AddFirstWithoutOrderByDiagnostics(); |
| | 0 | 57 | | options.UseMySql(sqlConnection, serverVersion, mySqlOptions => |
| | 0 | 58 | | { |
| | 0 | 59 | | mySqlOptions.CommandTimeout(30); |
| | 0 | 60 | | // MaxBatchSize(1): не объединять INSERT в разные таблицы в один multi-statement пакет |
| | 0 | 61 | | // с несколькими RETURNING — на проде это приводило к "syntax error near RETURNING" (не |
| | 0 | 62 | | // воспроизводится изолированно, похоже на проблему сетевого пути), см. письма OrderCaster. |
| | 0 | 63 | | mySqlOptions.MaxBatchSize(1); |
| | 0 | 64 | | mySqlOptions.EnableRetryOnFailure( |
| | 0 | 65 | | maxRetryCount: 3, |
| | 0 | 66 | | maxRetryDelay: TimeSpan.FromSeconds(5), |
| | 0 | 67 | | errorNumbersToAdd: st2RetryErrors); |
| | 0 | 68 | | }); |
| | 0 | 69 | | }, poolSize: 24); |
| | | 70 | | |
| | | 71 | | // DbContextFactory для фоновых задач (логирование, Hangfire jobs) — пулированная фабрика. |
| | | 72 | | // AddPooledDbContextFactory регистрирует IDbContextFactory<T> с пулом контекстов. |
| | | 73 | | // Нельзя использовать AddDbContextFactory — он перезаписывает factory, созданную AddDbContextPool выше, |
| | | 74 | | // превращая её в непулированную (нет ограничений на число контекстов, только MaximumPoolSize коннектора). |
| | 0 | 75 | | services.AddPooledDbContextFactory<ApplicationDbContext>(options => |
| | 0 | 76 | | { |
| | 0 | 77 | | options.AddInterceptors(slowDbInterceptor, triggerHealingInterceptor); |
| | 0 | 78 | | options.AddFirstWithoutOrderByDiagnostics(); |
| | 0 | 79 | | options.UseMySql(sqlConnection, serverVersion, mySqlOptions => |
| | 0 | 80 | | { |
| | 0 | 81 | | mySqlOptions.CommandTimeout(60); |
| | 0 | 82 | | mySqlOptions.MaxBatchSize(1); |
| | 0 | 83 | | mySqlOptions.EnableRetryOnFailure( |
| | 0 | 84 | | maxRetryCount: 3, |
| | 0 | 85 | | maxRetryDelay: TimeSpan.FromSeconds(5), |
| | 0 | 86 | | errorNumbersToAdd: st2RetryErrors); |
| | 0 | 87 | | }); |
| | 0 | 88 | | }, poolSize: 8); |
| | | 89 | | |
| | 0 | 90 | | services.AddDbContextPool<OzonIdentityOrderContext>(options => |
| | 0 | 91 | | { |
| | 0 | 92 | | options.AddInterceptors(slowDbInterceptor, triggerHealingInterceptor); |
| | 0 | 93 | | options.AddFirstWithoutOrderByDiagnostics(); |
| | 0 | 94 | | options.UseMySql(sqlConnection, serverVersion, mySqlOptions => |
| | 0 | 95 | | { |
| | 0 | 96 | | mySqlOptions.CommandTimeout(30); |
| | 0 | 97 | | mySqlOptions.MaxBatchSize(1); |
| | 0 | 98 | | mySqlOptions.EnableRetryOnFailure( |
| | 0 | 99 | | maxRetryCount: 3, |
| | 0 | 100 | | maxRetryDelay: TimeSpan.FromSeconds(5), |
| | 0 | 101 | | errorNumbersToAdd: st2RetryErrors); |
| | 0 | 102 | | }); |
| | 0 | 103 | | }, poolSize: 8); |
| | | 104 | | |
| | 0 | 105 | | services.AddDbContextPool<BitrixContext>(options => |
| | 0 | 106 | | { |
| | 0 | 107 | | options.AddInterceptors(slowDbInterceptor); |
| | 0 | 108 | | options.AddFirstWithoutOrderByDiagnostics(); |
| | 0 | 109 | | options.UseMySql(bitrixConnection, serverVersion, mySqlOptions => |
| | 0 | 110 | | { |
| | 0 | 111 | | mySqlOptions.CommandTimeout(30); |
| | 0 | 112 | | mySqlOptions.MaxBatchSize(1); |
| | 0 | 113 | | mySqlOptions.EnableRetryOnFailure( |
| | 0 | 114 | | maxRetryCount: 3, |
| | 0 | 115 | | maxRetryDelay: TimeSpan.FromSeconds(5), |
| | 0 | 116 | | errorNumbersToAdd: null); |
| | 0 | 117 | | }); |
| | 0 | 118 | | }, poolSize: 16); |
| | | 119 | | |
| | 0 | 120 | | services.AddDbContextPool<OzonApiContext>(options => |
| | 0 | 121 | | { |
| | 0 | 122 | | options.AddInterceptors(slowDbInterceptor); |
| | 0 | 123 | | options.AddFirstWithoutOrderByDiagnostics(); |
| | 0 | 124 | | options.UseMySql(ozonApiConnection, serverVersion, mySqlOptions => |
| | 0 | 125 | | { |
| | 0 | 126 | | mySqlOptions.CommandTimeout(60); |
| | 0 | 127 | | mySqlOptions.MaxBatchSize(1); |
| | 0 | 128 | | mySqlOptions.EnableRetryOnFailure( |
| | 0 | 129 | | maxRetryCount: 3, |
| | 0 | 130 | | maxRetryDelay: TimeSpan.FromSeconds(5), |
| | 0 | 131 | | errorNumbersToAdd: null); |
| | 0 | 132 | | }); |
| | 0 | 133 | | }, poolSize: 4); |
| | | 134 | | |
| | 0 | 135 | | services.AddPooledDbContextFactory<OzonApiContext>(options => |
| | 0 | 136 | | { |
| | 0 | 137 | | options.AddInterceptors(slowDbInterceptor); |
| | 0 | 138 | | options.AddFirstWithoutOrderByDiagnostics(); |
| | 0 | 139 | | options.UseMySql(ozonApiConnection, serverVersion, mySqlOptions => |
| | 0 | 140 | | { |
| | 0 | 141 | | mySqlOptions.CommandTimeout(60); |
| | 0 | 142 | | mySqlOptions.MaxBatchSize(1); |
| | 0 | 143 | | mySqlOptions.EnableRetryOnFailure( |
| | 0 | 144 | | maxRetryCount: 3, |
| | 0 | 145 | | maxRetryDelay: TimeSpan.FromSeconds(5), |
| | 0 | 146 | | errorNumbersToAdd: null); |
| | 0 | 147 | | }); |
| | 0 | 148 | | }, poolSize: 4); |
| | | 149 | | |
| | | 150 | | // --- SQL Server --- |
| | 0 | 151 | | services.AddDbContextPool<JcEtalonContext>(options => |
| | 0 | 152 | | { |
| | 0 | 153 | | options.AddInterceptors(slowDbInterceptor); |
| | 0 | 154 | | options.AddFirstWithoutOrderByDiagnostics(); |
| | 0 | 155 | | options.UseSqlServer(jcEtalonConnection, sqlServerOptions => |
| | 0 | 156 | | { |
| | 0 | 157 | | sqlServerOptions.CommandTimeout(30); |
| | 0 | 158 | | sqlServerOptions.EnableRetryOnFailure( |
| | 0 | 159 | | maxRetryCount: 3, |
| | 0 | 160 | | maxRetryDelay: TimeSpan.FromSeconds(30), |
| | 0 | 161 | | errorNumbersToAdd: null); |
| | 0 | 162 | | }); |
| | 0 | 163 | | }); |
| | | 164 | | |
| | | 165 | | // --- Прочее --- |
| | | 166 | | // DbSeederOptions читает AdminEmail/AdminPassword из конфига (секция "DbSeeder"). |
| | | 167 | | // Переопределить через appsettings или переменную среды DbSeeder__AdminPassword. |
| | 0 | 168 | | services.Configure<DbSeederOptions>(configuration.GetSection(DbSeederOptions.SectionName)); |
| | 0 | 169 | | services.AddTransient<DbInitializer>(); |
| | 0 | 170 | | } |
| | | 171 | | } |