0

I have .NET Core Web API solution. In each call, I need to perform some database operations.
The issue is at a time multiple db connections get opened & close. So to avoid it, I want to implement Queue of objects to be sent to database and then want a separate thread to perform db operation.
I've tried some code as below. But here, Consumer thread never executes assigned function. There is no separate thread for Producer, I am simply feeding queue with object.
What modifications I should do? Need some guidance as I'm new to Threading stuff.

  public static class BlockingQueue
{
    public static Queue<WebServiceLogModel> queue;
    static BlockingQueue()
    {
        queue = new Queue<WebServiceLogModel>();

    }

    public static object Dequeue()
    {
        lock (queue)
        {
            while (queue.Count == 0)
            {
                Monitor.Wait(queue);
            }
            return queue.Dequeue();
        }
    }
    public static void Enqueue(WebServiceLogModel webServiceLog)
    {
        lock (queue)
        {
            queue.Enqueue(webServiceLog);
            Monitor.Pulse(queue);
        }
    }

    public static void ConsumerThread(IConfiguration configuration)
    {
        WebServiceLogModel webServiceLog = (WebServiceLogModel)Dequeue();
        webServiceLog.SaveWebServiceLog(configuration);
    }

   public static void ProducerThread(WebServiceLogModel webServiceLog)
    {
        Enqueue(webServiceLog);
         Thread.Sleep(100);
    }
}

I've created and started thread in StartUp.cs:

    public Startup(IConfiguration configuration)
    {
        Thread t = new Thread(() => BlockingQueue.ConsumerThread(configuration));
        t.Start();
    }

In Controller, I've written code to feed the queue:

    [HttpGet]
    [Route("abc")]
    public IActionResult GetData()
    {
        BlockingQueue.ProducerThread(logModel);
        return StatusCode(HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound, ApplicationConstants.Message.NoBatchHistoryInfo);
    }
Priya
  • 1,375
  • 8
  • 21
  • 45
  • "*The issue is at a time multiple db connections get opened & close. So to avoid it*" why do you want to do this, do you have a concurrency problem? – TheGeneral Nov 21 '18 at 06:56
  • yes. concurrency issue I am facing. Any suggestions for that? – Priya Nov 21 '18 at 06:57
  • I will check TPL too then, if it is helpful then. – Priya Nov 21 '18 at 07:03
  • Two suggestions, firstly don't make the object you are using as a lock public. It's hard enough to make sure that you don't deadlock something without allowing other objects to join the party. Secondly, take a look at BlockingCollection which implements a producer consumer pattern for you. – Adam G Nov 21 '18 at 07:23
  • Blocking collection I've heard about. I will check if its helpful in my case. Thanks. – Priya Nov 21 '18 at 07:45

2 Answers2

1

First of all, try to avoid static classes and methods. Use pattern singleton in that case (and if you really need this). Second, try to avoid lock, Monitor - those concurrency primitives significantly lower your performance. In such situation, you can use BlockingCollection<> as 'Adam G' mentioned above, or you can develop your own solution.

public class Service : IDisposable
{
    private readonly BlockingCollection<WebServiceLogModel> _packets =
        new BlockingCollection<WebServiceLogModel>();
    private Task _task;
    private volatile bool _active;
    private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(1);

    public Service()
    {
        _active = true;
        _task = ExecTaskInternal();
    }

    public void Enqueue(WebServiceLogModel model)
    {
        _packets.Add(model);
    }

    public void Dispose()
    {
        _active = false;
    }

    private async Task ExecTaskInternal()
    {
        while (_active)
        {
            if (_packets.TryTake(out WebServiceLogModel model))
            {
                // TODO: whatever you need
            }
            else
            {
                await Task.Delay(WaitTimeout);
            }
        }
    }
}

public class MyController : Controller
{
    [HttpGet]
    [Route("abc")]
    public IActionResult GetData([FromServices] Service service)
    {
        // receive model form somewhere
        WebServiceLogModel model = FetchModel();
        // enqueue model
        service.Enqueue(model);
        // TODO: return what you need
    }
}

And in Startup:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<Service>();
        // TODO: other init staffs
    }
}

You even can add Start/Stop methods to the service instead of implementing IDisposable and start your service in the startup class in the method Configure(IApplicationBuilder app).

Igor Goyda
  • 1,949
  • 5
  • 10
  • Hi Igor. Thanks for providing solution. I've implemented with few modification as per my requirements. I have not worked with threads before. So was stuck. Btw, will this work flawlessly if multiple endpoints will try to add WebServiceLog concurrently? Thanks again for help! :) – Priya Nov 21 '18 at 13:02
  • 1
    According to the documentation: [link](https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.blockingcollection-1?view=netframework-4.7.2) this class was designed for concurrent access. – Igor Goyda Nov 21 '18 at 13:16
0

I think your consumer thread is executed just once if there is something in the queue and then immediately returns. If you want to have a thread doing work in background, which is started just once, it should never return and should catch all exceptions. Your thread from BlockingQueue.ConsumerThread is invoked once in Stratup and returns.

Also please be aware that doing such solution is not safe. ASP.NET doesn't guarantee background threads to be running if there are no requests coming in. Your application pool can recycle (and by default it recycles after 20 minutes of inactivity or every 27 hours), so there is a chance that your background code won't be executed for some queue items.

Also, while it doesn't solve all issues, I would suggest using https://www.hangfire.io/ to do background tasks in ASP.NET server. It has persistence layer, can retry jobs and has simple API's. In your request handler you can push new jobs to Hangfire and then have just 1 job processor thread.

dlxeon
  • 1,932
  • 1
  • 11
  • 14
  • What could be the efficient solution for such issues? I am facing concurrency issue. So want to store object in queue and then want a separate thread to perform db stuff. – Priya Nov 21 '18 at 07:06
  • Ideal and most reliable solution would be to have separate queue (RabbitMQ/ServiceBus etc) and separate service that is not running in ASP.Net (another machine / windows service etc) that will process that queue. Reason: survive app domain restarts, because if you have big queue of tasks in memory and app domain restarts, you lose it all. – dlxeon Nov 21 '18 at 07:08
  • Depending on your situation, good enough solution can be to use HangFire. It will handle for you persistence, thread management, locks etc. You can just push items to it and then it will call your job handler. – dlxeon Nov 21 '18 at 07:09
  • Unfortunately, I won't be able to use any third party solutions n required to solve this issue with help of pure .net concepts. :| – Priya Nov 21 '18 at 07:14
  • Actually, I'm a little bit unsure if concurrent database operations is root cause of issue, because databases are designed to handle lots of queries. But if you do believe that's the case, maybe you can tweak database connection pool settings to allow just 1 connection for whole application https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-connection-pooling , but then you should add retries and error handling implementation for every database access, because now it may fail due to timeout/lack of connection available. – dlxeon Nov 21 '18 at 07:19
  • Thanks. I will check if allowing only single connection can work in my case. But m afraid, it will create issues for other database calls too?!? – Priya Nov 21 '18 at 07:47