SQL Server Blazor Server App does not survive after several accesses to the Database - no s

yfwxisqw  于 2023-05-28  发布在  其他
关注(0)|答案(2)|浏览(102)

I am desperate for a solution - I have been trying to solve this for about two months now with no solution in sight.

Update 5/25/23: Per the Microsoft folks I removed the complicated Service Method and replaced it with a far simpler method. Ironically this resulted in the server crashing on the first hit every time... so not quite sure what that says. I also noticed that while I was implementing this I had forgotten to restore the Logins, and that failure caused the Server to go down. That does not seem reasonable. it should survive such failures.

I need to be very clear. I know how to trap exceptions. You can clearly see that in my code below. Please understand: No exceptions are ever thrown. Ever. Nor is any logging data posted to the Event Log or any other log no matter what I do. The Site just dies silently.

I have now produced a small project which reproduces the issue and I submitted it to Microsoft for review. Please do not suggest that I use Try/Catch blocks - I already do, and no exceptions are thrown. If they were, I would not be here, I would be pursuing whatever they suggested. Same for logging, I have implemented every log suggested, and none show any hints of why this is happening. That is my major frustration, a complete lack of any evidence from which I can develop a strategy.

The symptom is that after about 10 to 30 hits to the database, the Web Server App Pool (or something) dies and returns "The service is unavailable". I have tested the AddScoped and AddTransient options for my services, neither makes a major difference. I am using DI where appropriate, have resolved all those issues long ago.

Controller:

using BTOnlineBlazor.Data;
using BTOnlineBlazor.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace BTOnlineBlazor.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class GetAppListHandlerController : ControllerBase
    {           
        
        private readonly AccountUtilitiesService _appManagerService;

        public GetAppListHandlerController( [FromServices] AccountUtilitiesService appManagerService

        )
        {
            _appManagerService = appManagerService;
        }

        [HttpGet]
        public ActionResult GetAppList([FromQuery] string AccountID)
        {
            string? result = _appManagerService.GetAccount(AccountID)?.AccountName;
            return Ok(result);
        }
    }
}

Service Constructor:

private readonly ErrorReporterService errReport;
    private readonly IDbContextFactory<BtDbContext> _dbContextFactory;
    
    public const string cNoAccount = "NoAccount";
    public AccountUtilitiesService(ErrorReporterService errReport, IDbContextFactory<BtDbContext> dbContextFactory)
    {
        this.errReport = errReport;
        _dbContextFactory = dbContextFactory;
        
    }

Service Method:

public Account? GetAccount(int? AccountID)
    {
        try
        {
            //errReport.LogMessage("Getting Account {0}", AccountID);
            using BtDbContext context = _dbContextFactory.CreateDbContext();
            Account? account = context.Accounts.SingleOrDefault(c => c.AccountId.Equals(AccountID));
            return account;

        }
        catch (Exception ex)
        {
            errReport.LogErr("Account ID: {0}", ex, AccountID);
            return null;
        }
    }

The primary problem is that I get no error information from which to develop a solution. I have pored over the Event Log, added Logging, added all the various IIS logs and so on. None show anything relating to the crash beyond generic messages which say things like 'The ISAPI App Pool is in an unhealthy state" or similar vague and generally useless messages.

I have completed a Blazor Server App that uses Entity Framework but cannot release it because it keeps crashing the server. It lasts far longer when run on the same machine I develop on, and crashes much sooner when deployed to the Beta server.

I just ran the same single DB call page on the LocalHost IIS on Windows 10, and it did not die after more than 60 hits to the page. It dies after no more than 32 refreshes when deployed on the Windows 2022 IIS instance. I have seen it die on the Local server if enough hits to the DB are performed, it just takes far more to kill the Server.

I have followed every recommendation provided in my previous posts to no avail. I now inject a DBContextFactory which I use to create a DBContext for UoW patterns.

I have determined that I can create the DBContext as many times as I wish - meaning that if I just create the DBContext I can refresh the page as many times as I want and it does not seem to fail - I have hit over 50 refreshes in this scenario and the server remains up.

However, if I make even one call to get data, it will eventually crash.

To be clear here, what I did was test a page that allowed me to isolate the code execution to create the DBContext via the factory and then make a single call to the Database.

If I just create the DBContext without accessing the DB it will run indefinitely - or at least 50+ refreshes.

If I make just one call to get actual data, it will only survive about 32 refreshes. The more calls I make, the fewer refreshes it takes to kill the server.

How can I figure out what is causing this? I have tried every logging and debug output that people have told me to try - including the Event Logging and the Server Logs. None of they show anything that reveals a cause. The closest I can come is that the Websocket might be dying. Aside from that, there are no errors to speak of.

I do observe a tiny consumption of memory for each refresh even though I am following all the proper principles regarding memory and disposal of resources. Not sure why it consumes a small amount of memory on each refresh, there are no objects that are not being cleaned up to the best of my knowledge.

I have observed the crash in the VS IDE and no exceptions are thrown, no evidence of the crash is visible, it just stops working and returns the infamous "The service is unavailable"

I have been trying to find a resolution to this problem for about 2 months now to no avail. I find it hard to believe anyone could release a project based on Blazor and Entity Framework if it crashes like this, so what is different for me?

Here is the link to the last article I posted - I have done everything that I was told to do and it still crashes.

Previous Article on this subject

mspsb9vt

mspsb9vt1#

Here's a demo system based on your code. I've made up Account as you didn't provide it and I've created a demo in-memory database to test against.

Account

public record Account
{
    public string? Name { get; init; }
    public int AccountId { get; init; }
    public Guid UserId { get; init; }
    public string? Email { get; init; }
}

The DbContext

public sealed class InMemoryTestDbContext
    : DbContext
{
    public DbSet<Account> Accounts { get; set; } = default!;
 
    public InMemoryTestDbContext(DbContextOptions<InMemoryTestDbContext> options) : base(options) { }
}

The Test DataProvider

public sealed class TestDataProvider
{
    public static IEnumerable<Account> Accounts => _accounts;

    private static List<Account> _accounts = new()
    {
        new() { UserId=Guid.Parse("10000000-0000-0000-0000-000000000001"), Name="Visitor", AccountId=1, Email="me@you.com"},
    };

    public static void LoadDbContext<TDbContext>(IDbContextFactory<TDbContext> factory) where TDbContext : DbContext
    {
        using var dbContext = factory.CreateDbContext();

        var accounts = dbContext.Set<Account>();

        // Check if we already have a data set
        // If not add the data set
        if (accounts.Count() == 0)
        {
            dbContext.AddRange(Accounts);
            dbContext.SaveChanges();
        }
    }
}

AccountHandler

This is your code encapsulated within a handler that you register in DI. Note there's no try as you don't need one.

I've taken the liberty of refactoring your code and changing the EF interaction to async behaviour.

public class AccountHandler
{
    private readonly IDbContextFactory<InMemoryTestDbContext> _dbContextFactory;
    private Account? _account;

    public AccountHandler(IDbContextFactory<InMemoryTestDbContext> dbContextFactory)
        => _dbContextFactory = dbContextFactory;

    public async ValueTask<Account?> GetAccountAsync(string? accountID)
    {
        _account = null;

        if (accountID is null)
            return null;

        if (await TryGetGuidAccountAsync(accountID))
            return _account;

        if (await TryGetIntAccountAsync(accountID))
            return _account;

        if (await TryGetEmailAccountAsync(accountID))
            return _account;

        return null;
    }

    private async ValueTask<bool> TryGetGuidAccountAsync(string accountID)
    {
        if (Guid.TryParse(accountID, out Guid accountId))
        {
            using var context = _dbContextFactory.CreateDbContext();
            _account = await context.Accounts.FirstOrDefaultAsync(c => c.UserId.Equals(accountId));

            return _account is not null;
        }
        return false;
    }

    private async ValueTask<bool> TryGetIntAccountAsync(string accountID)
    {
        if (int.TryParse(accountID, out int id))
        {
            using var context = _dbContextFactory.CreateDbContext();
            _account = await context.Accounts.FirstOrDefaultAsync(a => a.AccountId.Equals(id));

            return _account is not null;
        }
        return false;
    }

    private async ValueTask<bool> TryGetEmailAccountAsync(string accountID)
    {
        // simplified logic to demo
        if (accountID.Contains("@"))
        {
            using var context = _dbContextFactory.CreateDbContext();
            _account = await context.Accounts.FirstOrDefaultAsync(a => accountID.Equals(a.Email));

            return _account is not null;
        }

        return false;
    }
}

The following XUnit test class tests the pipeline.

public class AccountHandlerTests
{
    private ServiceProvider GetServiceProvider()
    {
        // Creates a DI Service collection
        var services = new ServiceCollection();

        //Adds the required services
        services.AddDbContextFactory<InMemoryTestDbContext>(options
            => options.UseInMemoryDatabase($"TestDatabase-{Guid.NewGuid().ToString()}"));
        services.AddTransient<AccountHandler>();
        services.AddLogging(builder => builder.AddDebug());

        // Builds the provider
        var provider = services.BuildServiceProvider();

        // get the DbContext factory and add the test data
        var factory = provider.GetService<IDbContextFactory<InMemoryTestDbContext>>();
        if (factory is not null)
            TestDataProvider.LoadDbContext<InMemoryTestDbContext>(factory);

        return provider!;
    }

    [Fact]
    public async Task TestAccountHandlerGuid()
    {
        var serviceProvider = GetServiceProvider();
        var handler = serviceProvider.GetService<AccountHandler>()!;

        var account = await handler.GetAccountAsync("10000000-0000-0000-0000-000000000001");

        Assert.NotNull(account);
        Assert.Equal("Visitor", account.Name);
    }

    [Fact]
    public async Task TestAccountHandlerInt()
    {
        var serviceProvider = GetServiceProvider();
        var handler = serviceProvider.GetService<AccountHandler>()!;

        var account = await handler.GetAccountAsync("1");

        Assert.NotNull(account);
        Assert.Equal("Visitor", account.Name);
    }

    [Fact]
    public async Task TestAccountHandlerEmail()
    {
        var serviceProvider = GetServiceProvider();
        var handler = serviceProvider.GetService<AccountHandler>()!;

        var account = await handler.GetAccountAsync("me@you.com");

        Assert.NotNull(account);
        Assert.Equal("Visitor", account.Name);
    }

    [Fact]
    public async Task Test1000Times()
    {
        var serviceProvider = GetServiceProvider();

        var counter = 0;
        for (var i = 0; i < 1000; i++)
        {
            {
                var handler = serviceProvider.GetService<AccountHandler>()!;

                var account = await handler.GetAccountAsync("10000000-0000-0000-0000-000000000001");

                Assert.NotNull(account);
                Assert.Equal("Visitor", account.Name);

            }
            {
                var handler = serviceProvider.GetService<AccountHandler>()!;

                var account = await handler.GetAccountAsync("1");

                Assert.NotNull(account);
                Assert.Equal("Visitor", account.Name);
            }
            {
                var handler = serviceProvider.GetService<AccountHandler>()!;

                var account = await handler.GetAccountAsync("me@you.com");

                Assert.NotNull(account);
                Assert.Equal("Visitor", account.Name);
            }

            counter++;
        }

        Assert.Equal(1000, counter);
    }
}
umuewwlo

umuewwlo2#

You are learning a painful lesson is application design.

If your only way of testing the problem is to do page refreshes then you have serious design problems, and need to structure your application so you can both end-to-end and unit test it.

To do this you need to apply some sort of structured design principles where you break you code out into a set of design domains with clean interfaces between the domains.

To give you an idea this is what I do.

I base my solutions on "Clean Design" principles and have three domains:

  1. The Core Domain contains all my business logic and data classes.
  2. The Infrastructure Domain contains all the code that interfaces to data stores and third party systems.
  3. The Presentation Domain contains all the UI code.

I use projects within my VS solution with clearly defined dependancies. Presentation depends on Core, Infrastructure depends on Core.

I can then write XUnit tests that run whatever tests I want to test individual sections of my data pipeline or from a specific point backwards to the data store. I can create tests that will open and close a DbContext say a thousand times at various points, and therefore isolate which bit is the root cause of the problem.

I can also write BUnit tests to do the same with Blazor Components.

相关问题