-1

Why is the concurrent dictionary not behaving like a concurrent dictionary?

[Route("aaa")]
[HttpPost]
public Outbound Post(Inbound inbound)
{
    Outbound outbound = new Outbound();

    TaskState taskState = new TaskState();
    taskState.state = 0;

    TaskState s;
    bool rc = taskDictionary.TryGetValue(inbound.taskId, out s);
    if (rc == false)
        taskDictionary.GetOrAdd(inbound.taskId, taskState);
    else
        return null;
    if (s != null)
        return null;

    rc = taskDictionary.TryGetValue(inbound.taskId, out s);
    if (rc)
    {
        string m0 = String.Format("POST dictionary count is " + taskDictionary.Count);
        _logger.Log(LogLevel.Error, m0);
    }
    else
        return null;

    _logger.Log(LogLevel.Error, "POST method is doing long running work.");

    long Billion = 1000000000;
    // work quantity 5 means about 26 seconds // 30 billion is 215 seconds
    for (long i = 0; i < inbound.WorkQuantity * Billion; i++)
        ;
    outbound.Message = "The server did some work.";
    outbound.BigObject = Modify(inbound.BigObject);

    _logger.Log(LogLevel.Error, "POST finished long running work and will soon remove from the dictionary.");

    rc = taskDictionary.Remove(inbound.taskId, out s);
    if (rc == false)
        return null;
    if (s == null)
        return null;

    _logger.Log(LogLevel.Error, "POST is returning the object.");

    return outbound;
}
[Route("bbb")]
[HttpPost]
public TaskState PostState(TaskId taskId)
{
    TaskState s;
    if (taskDictionary.TryGetValue(taskId, out s))
    {
        _logger.Log(LogLevel.Error, "POSTSTATE, state is " + s.AsString());
        return s;
    }
    else
    {
        _logger.Log(LogLevel.Error, "POSTSTATE, state not found, dictionary count is " + taskDictionary.Count);
        return null;
    }
}



TaskDictionary taskDictionary = new TaskDictionary();
class TaskDictionary : ConcurrentDictionary<TaskId, TaskState>
{
    internal bool IncrementState(TaskId taskId)
    {
        TaskState s;
        if (TryGetValue(taskId, out s))
        {
            s.Increment();
            return true;
        }
        else
            return false;
    }
}

The logging output is...

12.207 +00:00 [Error] MyController: POST dictionary count is 1
12.322 +00:00 [Error] MyController: POST method is doing long running work.
14.361 +00:00 [Error] MyController: POSTSTATE, state not found, dictionary count is 0
40.452 +00:00 [Error] MyController: POST finished long running work and will soon remove from the dictionary.
40.569 +00:00 [Error] MyController: POST is returning the object.

Specifically the problem is that request for the state information does not work because the dictionary appears empty.

H2ONaCl
  • 10,644
  • 14
  • 70
  • 114
  • So, explain to me how you generate the third line in the logging, because it's not part of the Post method that's doing the long running work. Through the "bbb" route during the execution of Post? – Dennis VW Aug 09 '20 at 01:29
  • You initiate the post.. It runs for a while. But during that time you post to PostState(taskId); and you expect that the dictionary count is still 1 when you do that. But that doesn't work because every request essentially, initializes your TaskDictionary again. This field you put there`TaskDictionary taskDictionary = new TaskDictionary();` is not shared between requests obviously. You'd need to create a service and DI it so you can share it in different requests – Dennis VW Aug 09 '20 at 01:33
  • Let me put it this way. Your logging is generated by *two* post requests. Correct? – Dennis VW Aug 09 '20 at 01:40
  • You're saying each request is a whole new instance of the controller (hence whole new TaskDictionary) ? – H2ONaCl Aug 09 '20 at 01:41
  • Yes. There are 2 posts. One URI is "mycontroller/aaa" and the other is "mycontroller/bbb". – H2ONaCl Aug 09 '20 at 01:41
  • Exactly. Controllers are 'Scoped' to the request. Meaning they live for the duration of the request and are spun up by each request. – Dennis VW Aug 09 '20 at 01:42

1 Answers1

1

So, based on the logging output you are doing two post requests to generate the output that you show us.

The way you initialize the Concurrent Dictionary, is the reason why it returns null on the second request. And that has to do with how the app receives those requests. Each request to your app is independent of another request.

Simply put;

A user POST's to you app. The requests goes through the middleware pipeline, and eventually it will end up at your controller. Now, here's the important part, the controller will be 'constructed' for this particular request. And it will live for the duration of the request. That's because the lifetime of a controller is scoped by default. So the next request, will construct a new controller and that means that the Dictionary is different from the first one.

So to overcome this scoped request problem, you create a service that contains the dictionary, register this as a singleton (meaning it will only be constructed once and then it's shared), and use dependency injection to use it in the controller.

Dennis VW
  • 2,977
  • 1
  • 15
  • 36
  • "Service" is a bit non-specific or broad. Perhaps you meant something like this approach. https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio – H2ONaCl Aug 10 '20 at 01:18
  • Who on earth would think I was talking about running background tasks in a hosted service in this context? But I appreciate the comment, and improved the answer by adding links to the *correct* documentation! :) – Dennis VW Aug 10 '20 at 11:21
  • Thanks. It's just that one of the requests is long running. I can't reliably go beyond 215 seconds. https://stackoverflow.com/questions/63145037/what-aspect-of-azure-app-service-determines-the-elapsed-time-limit – H2ONaCl Aug 10 '20 at 17:24