< Summary

Information
Class: OzonOrdersWeb.Extensions.DatabaseExtensions
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Extensions/DatabaseExtensions.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 122
Coverable lines: 122
Total lines: 171
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 20
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddDatabaseServices(...)0%420200%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Extensions/DatabaseExtensions.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using OzonRepositories.Context;
 3using OzonRepositories.Context.Identity;
 4using OzonRepositories.Utils;
 5using OzonOrdersWeb.Observability;
 6using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
 7using Services.DataServices.TriggerSelfHealing;
 8
 9namespace OzonOrdersWeb.Extensions;
 10
 11public static class DatabaseExtensions
 12{
 13    public static void AddDatabaseServices(this IServiceCollection services, IConfiguration configuration, bool isProd)
 14    {
 015        var slowDbInterceptor = new SlowDbCommandInterceptor(
 016            configuration.GetValue<int?>($"{ObservabilityOptions.SectionName}:SlowDependencyMilliseconds") ?? 1_000);
 017        string sqlConnection = (isProd
 018            ? configuration.GetConnectionString("MySqlConnectionProd")
 019            : configuration.GetConnectionString("MySqlConnectionLocal"))
 020            ?? throw new InvalidOperationException("MySQL connection string is not configured.");
 21
 022        string jcEtalonConnection = configuration.GetConnectionString("JcEtalonConnectionProd")
 023            ?? throw new InvalidOperationException("JcEtalon connection string is not configured.");
 24
 025        string bitrixConnection = (isProd
 026            ? configuration.GetConnectionString("BitrixConnectionProd")
 027            : configuration.GetConnectionString("BitrixConnectionLocal") ?? configuration.GetConnectionString("BitrixCon
 028            ?? throw new InvalidOperationException("Bitrix connection string is not configured.");
 29
 030        string ozonApiConnection = configuration.GetConnectionString("OzonApiConnectionProd")
 031            ?? throw new InvalidOperationException("OzonApi connection string is not configured.");
 32
 33        // MariaDB 11.4 на 192.168.1.111 (ветка local-deploy)
 034        var serverVersion = St2MariaDb.ServerVersion;
 35
 36        // Самолечение триггеров ST2 (MariaDB 1711 после KILL QUERY, см. TriggerBodyErrorHealer):
 37        // интерцептор перезагружает триггеры, ретрай-стратегия повторяет упавшую команду.
 038        var triggerHealer = new TriggerBodyErrorHealer(new MySqlTriggerReloader(sqlConnection), TimeSpan.FromSeconds(30)
 039        var triggerHealingInterceptor = new TriggerBodyHealingInterceptor(triggerHealer);
 040        services.AddSingleton(triggerHealer);
 041        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
 053        services.AddDbContextPool<ApplicationDbContext>(options =>
 054        {
 055            options.AddInterceptors(slowDbInterceptor, triggerHealingInterceptor);
 056            options.AddFirstWithoutOrderByDiagnostics();
 057            options.UseMySql(sqlConnection, serverVersion, mySqlOptions =>
 058            {
 059                mySqlOptions.CommandTimeout(30);
 060                // MaxBatchSize(1): не объединять INSERT в разные таблицы в один multi-statement пакет
 061                // с несколькими RETURNING — на проде это приводило к "syntax error near RETURNING" (не
 062                // воспроизводится изолированно, похоже на проблему сетевого пути), см. письма OrderCaster.
 063                mySqlOptions.MaxBatchSize(1);
 064                mySqlOptions.EnableRetryOnFailure(
 065                    maxRetryCount: 3,
 066                    maxRetryDelay: TimeSpan.FromSeconds(5),
 067                    errorNumbersToAdd: st2RetryErrors);
 068            });
 069        }, poolSize: 24);
 70
 71        // DbContextFactory для фоновых задач (логирование, Hangfire jobs) — пулированная фабрика.
 72        // AddPooledDbContextFactory регистрирует IDbContextFactory<T> с пулом контекстов.
 73        // Нельзя использовать AddDbContextFactory — он перезаписывает factory, созданную AddDbContextPool выше,
 74        // превращая её в непулированную (нет ограничений на число контекстов, только MaximumPoolSize коннектора).
 075        services.AddPooledDbContextFactory<ApplicationDbContext>(options =>
 076        {
 077            options.AddInterceptors(slowDbInterceptor, triggerHealingInterceptor);
 078            options.AddFirstWithoutOrderByDiagnostics();
 079            options.UseMySql(sqlConnection, serverVersion, mySqlOptions =>
 080            {
 081                mySqlOptions.CommandTimeout(60);
 082                mySqlOptions.MaxBatchSize(1);
 083                mySqlOptions.EnableRetryOnFailure(
 084                    maxRetryCount: 3,
 085                    maxRetryDelay: TimeSpan.FromSeconds(5),
 086                    errorNumbersToAdd: st2RetryErrors);
 087            });
 088        }, poolSize: 8);
 89
 090        services.AddDbContextPool<OzonIdentityOrderContext>(options =>
 091        {
 092            options.AddInterceptors(slowDbInterceptor, triggerHealingInterceptor);
 093            options.AddFirstWithoutOrderByDiagnostics();
 094            options.UseMySql(sqlConnection, serverVersion, mySqlOptions =>
 095            {
 096                mySqlOptions.CommandTimeout(30);
 097                mySqlOptions.MaxBatchSize(1);
 098                mySqlOptions.EnableRetryOnFailure(
 099                    maxRetryCount: 3,
 0100                    maxRetryDelay: TimeSpan.FromSeconds(5),
 0101                    errorNumbersToAdd: st2RetryErrors);
 0102            });
 0103        }, poolSize: 8);
 104
 0105        services.AddDbContextPool<BitrixContext>(options =>
 0106        {
 0107            options.AddInterceptors(slowDbInterceptor);
 0108            options.AddFirstWithoutOrderByDiagnostics();
 0109            options.UseMySql(bitrixConnection, serverVersion, mySqlOptions =>
 0110            {
 0111                mySqlOptions.CommandTimeout(30);
 0112                mySqlOptions.MaxBatchSize(1);
 0113                mySqlOptions.EnableRetryOnFailure(
 0114                    maxRetryCount: 3,
 0115                    maxRetryDelay: TimeSpan.FromSeconds(5),
 0116                    errorNumbersToAdd: null);
 0117            });
 0118        }, poolSize: 16);
 119
 0120        services.AddDbContextPool<OzonApiContext>(options =>
 0121        {
 0122            options.AddInterceptors(slowDbInterceptor);
 0123            options.AddFirstWithoutOrderByDiagnostics();
 0124            options.UseMySql(ozonApiConnection, serverVersion, mySqlOptions =>
 0125            {
 0126                mySqlOptions.CommandTimeout(60);
 0127                mySqlOptions.MaxBatchSize(1);
 0128                mySqlOptions.EnableRetryOnFailure(
 0129                    maxRetryCount: 3,
 0130                    maxRetryDelay: TimeSpan.FromSeconds(5),
 0131                    errorNumbersToAdd: null);
 0132            });
 0133        }, poolSize: 4);
 134
 0135        services.AddPooledDbContextFactory<OzonApiContext>(options =>
 0136        {
 0137            options.AddInterceptors(slowDbInterceptor);
 0138            options.AddFirstWithoutOrderByDiagnostics();
 0139            options.UseMySql(ozonApiConnection, serverVersion, mySqlOptions =>
 0140            {
 0141                mySqlOptions.CommandTimeout(60);
 0142                mySqlOptions.MaxBatchSize(1);
 0143                mySqlOptions.EnableRetryOnFailure(
 0144                    maxRetryCount: 3,
 0145                    maxRetryDelay: TimeSpan.FromSeconds(5),
 0146                    errorNumbersToAdd: null);
 0147            });
 0148        }, poolSize: 4);
 149
 150        // --- SQL Server ---
 0151        services.AddDbContextPool<JcEtalonContext>(options =>
 0152        {
 0153            options.AddInterceptors(slowDbInterceptor);
 0154            options.AddFirstWithoutOrderByDiagnostics();
 0155            options.UseSqlServer(jcEtalonConnection, sqlServerOptions =>
 0156            {
 0157                sqlServerOptions.CommandTimeout(30);
 0158                sqlServerOptions.EnableRetryOnFailure(
 0159                    maxRetryCount: 3,
 0160                    maxRetryDelay: TimeSpan.FromSeconds(30),
 0161                    errorNumbersToAdd: null);
 0162            });
 0163        });
 164
 165        // --- Прочее ---
 166        // DbSeederOptions читает AdminEmail/AdminPassword из конфига (секция "DbSeeder").
 167        // Переопределить через appsettings или переменную среды DbSeeder__AdminPassword.
 0168        services.Configure<DbSeederOptions>(configuration.GetSection(DbSeederOptions.SectionName));
 0169        services.AddTransient<DbInitializer>();
 0170    }
 171}