-1

I have a web application that makes industrial scheduling calculations, I'm trying to log events in Azure cosmos db table after every update that happens to the schedule without affecting the application performance (screen Loading time).

That means, I want to fire log method in the BACKGROUND and the end user will not feel it (no freeze or extra loading time) and without making the UI wait for this operation to be done.

I added the next C# lines of code just after finishing the whole calculations:

    private List<JPIEventEntity> batch = new List<JPIEventEntity>();
    private List<List<JPIEventEntity>> batchesList = new List<List<JPIEventEntity>>();

    Thread newThread = new Thread(() => myJPIEventHandler.checkForJPIEventsToSend(customer, author, model));
    newThread.Start();


  /*
  * check if there are any changes or updates in the calculations and log their events.
  */
    internal void checkForJPIEventsToSend(JPIBaseCustomer customer, JPIBaseUser author, SchedulingModel afterModel)
    {
        myCustomer = customer;
        myUser = author;

        // Looking for deleted Jobs
        foreach (Job beforeJob in myBeforeModel.Jobs)
        {
            if (!afterModel.Jobs.Any(x => x.Guid == beforeJob.Guid))
            {
                //Looking for deleted Tasks and Log the deletion
                foreach (Operation beforeOp in beforeJob.Operations)
                {
                    //if (!afterJob.Operations.Any(x => x.Guid == beforeOp.Guid))
                    logTaskEvent(EventType.Delete, beforeOp, "", "");
                }
                //Log Job Deletion
                logJobEvent(EventType.Delete, beforeJob, "", "");
            }
        }

        //Comparison 
        foreach (Job afterJob in afterModel.Jobs)
        {
            if (myBeforeModel.Jobs.Any(x => x.Guid == afterJob.Guid))
            {
                Job beforeJob = myBeforeModel.Jobs.First(x => x.Guid == afterJob.Guid);

                if (beforeJob.Name != afterJob.Name)
                    logJobEvent(EventType.NameChanged, afterJob, beforeJob.Name, afterJob.Name);

                if (beforeJob.ReleaseDate != afterJob.ReleaseDate)
                    logJobEvent(EventType.ReleaseDateChanged, afterJob, beforeJob.ReleaseDate, afterJob.ReleaseDate);
                if (beforeJob.DueDate != afterJob.DueDate)
                    logJobEvent(EventType.DueDateChanged, afterJob, beforeJob.DueDate, afterJob.DueDate);
                if (beforeJob.IsDueDateExceeded != afterJob.IsDueDateExceeded)
                    logJobEvent(EventType.DueDateExceededChanged, afterJob, beforeJob.IsDueDateExceeded.ToString(), afterJob.IsDueDateExceeded.ToString());

                if (beforeJob.ProcessingState != afterJob.ProcessingState)
                {
                    logJobEvent(EventType.StatusChanged, afterJob,
                        convertProcessingStateToStatus(beforeJob.ProcessingState.ToString()), convertProcessingStateToStatus(afterJob.ProcessingState.ToString()));
                }
               
                if (beforeJob.SequenceNumber != afterJob.SequenceNumber && afterJob.ProcessingState != JobProcessingState.Finished)
                    logJobEvent(EventType.SequenceNumberChanged, afterJob, beforeJob.SequenceNumber, afterJob.SequenceNumber);
                if (beforeJob.CustomQuantity != afterJob.CustomQuantity)
                    logJobEvent(EventType.QuantityChanged, afterJob, beforeJob.CustomQuantity, afterJob.CustomQuantity);

                DateTime? beforeStart = beforeJob.ProcessingStart != null ? beforeJob.ProcessingStart : beforeJob.PlannedStart;
                DateTime? afterStart = afterJob.ProcessingStart != null ? afterJob.ProcessingStart : afterJob.PlannedStart;
                if (beforeStart != afterStart)
                    logJobEvent(EventType.StartDateChanged, afterJob, beforeStart, afterStart);

                DateTime? beforeEnd = beforeJob.ProcessingEnd != null ? beforeJob.ProcessingEnd : beforeJob.PlannedEnd;
                DateTime? afterEnd = afterJob.ProcessingEnd != null ? afterJob.ProcessingEnd : afterJob.PlannedEnd;
                if (beforeEnd != afterEnd)
                    logJobEvent(EventType.EndDateChanged, afterJob, beforeEnd, afterEnd);

                TimeSpan? beforeBuffer = beforeJob.DueDate != null ? (beforeJob.DueDate - beforeEnd) : new TimeSpan(0L);
                TimeSpan? afterBuffer = afterJob.DueDate != null ? (afterJob.DueDate - afterEnd) : new TimeSpan(0L);
                if (beforeBuffer != afterBuffer)
                    logJobEvent(EventType.BufferChanged, afterJob, beforeBuffer, afterBuffer);


            
        }

        //Collect the Batches in one list of batches
        CollectBatches();
        //Log all the Batches
        LogBatches(batchesList);

    }

    /*
     * Collectes the events in one batch
     */
    private void logJobEvent(EventType evtType, Job afterJob, string oldValue, string newValue)
    {
        var eventGuid = Guid.NewGuid();
        JPIEventEntity evt = new JPIEventEntity();
        evt.Value = newValue;
        evt.PrevValue = oldValue;
        evt.ObjectGuid = afterJob.Guid.ToString();
        evt.PartitionKey = myCustomer.ID; //customer guid
        evt.RowKey = eventGuid.ToString();
        evt.EventType = evtType;
        evt.CustomerName = myCustomer.Name;
        evt.User = myUser.Email;
        evt.ParentName = null;
        evt.ObjectType = JOB;
        evt.ObjectName = afterJob.Name;
        evt.CreatedAt = DateTime.Now;
        batch.Add(evt);
    }

    /*
     * Collectes the Events lists in an enumerable of Batches (max capacity of a single batch insertion is 100).
     */
    private void CollectBatches()
    {
        //batchesList = new List<List<JPIEventEntity>>();
        if (batch.Count > 0)
        {
            int rest = batch.Count;
            var nextBatch = new List<JPIEventEntity>();
            if (batch.Count > MaxBatchSize) //MaxBatchSize = 100
            {
                foreach (var item in batch)
                {

                    nextBatch.Add(item);
                    rest = rest - 1; //rest = rest - (MaxBatchSize * hundreds); 
                    if (rest < MaxBatchSize && nextBatch.Count == (batch.Count % MaxBatchSize))
                    {
                        batchesList.Add(nextBatch);
                    }
                    else if (nextBatch.Count == MaxBatchSize)
                    {
                        batchesList.Add(nextBatch);
                        nextBatch = new List<JPIEventEntity>();
                    }
                }
            }
            else
            {
                batchesList.Add(batch);
            }
        }
    }


    private void LogBatches(List<List<JPIEventEntity>> batchesList)
    {
        if (batchesList.Count > 0)
        {
            JPIEventHandler.LogBatches(batchesList);
        }
    }



    /*
     * Insert Events into database
     */
    public static void LogBatches(List<List<JPIEventEntity>> batchesList)
    {
        foreach (var batch in batchesList)
        {
            var batchOperationObj = new TableBatchOperation();

            //Iterating through each batch entities  
            foreach (var Event in batch)
            {
                batchOperationObj.InsertOrReplace(Event);
            }

            var res = table.ExecuteBatch(batchOperationObj);
        }
    }

Inside the 'checkForJPIEventsToSend' method, I'm checking if there's any changes or updates in the calculations and insert events (hundreds or even thousands of lines) into the cosmos db table as batches.

After putting the method in a separate thread (as shown above) I still have an EXTRA LOADING duration of 2 to 4 seconds after every operation, which is something critical and bad for us.

Am I using the multi-threading correctly?

Thank you in advance.

  • 2
    Please consider posting more code. Simply showing us that you spawn a thread without showing exactly _what the method does_ is insufficient for a meaningful response. Thanks – MickyD Mar 24 '21 at 16:01
  • 4
    Web applications are usually blocked by I/O not CPU, so spinning up a new thread often makes your app slower, not faster. You need to use [async/await](https://stackoverflow.com/questions/17145028/). – Dour High Arch Mar 24 '21 at 16:04
  • 2
    As said above you should check Async/Await And Tasks. Since it looks like you're using a fire and forget aproche here, you could use a Task.Run(() => yourmethod()) without having to await it. https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run?view=net-5.0 – Platypus Mar 24 '21 at 16:13
  • @MickyD thank you for your comment, I added more code and more details. – Walid Ghodhbani Mar 25 '21 at 09:46

1 Answers1

2

As I understand your situation you have a front end application such as a desktop app or a website that creates requests. For each request you

  • Perform some calculations
  • Write some data to storage (Cosmos DB)

It is unclear whether you must display some result to the front end after these steps are complete. Your options depend on this question.

Scenario 1: The front end is waiting for the results of the calculations or database changes

The front end requires some result from the calculations or database changes, so the user is forced to wait for this to complete. However you want to avoid freezing your front end whilst you perform the long running tasks.

The solution most developers reach for here is to perform the work in a background thread. Your main thread waits for this background thread to complete and return a result, at which point the main/UI thread will update the front end with the result. This is because the main thread is often the only thread allowed to update the front end.

How you offload the work to a background thread depends on the type of work load you have. If the work is mostly I/O such as File I/O, Network I/O (writing to Azure CosmosDB) then you want to use the Async methods of the Cosmos API and async/await.

See https://stackoverflow.com/a/18033198/6662878

If the work you are doing is mostly CPU based, then threads will only speed up the processing if the problem can be broken into parts and run in parallel across multiple background threads. If the problem cannot be broken down and parallelised then running the work on a single background thread has a small cost associated with thread switching, but in turn this frees up the main/UI thread whilst the CPU based work is in progress in the background.

See https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern

You will need to think about how you handle exceptions that occur on background threads, and how the code you run in your background thread will respond to a request to stop processing.

See https://docs.microsoft.com/en-us/dotnet/standard/threading/canceling-threads-cooperatively

Caveat: if any thread is carrying out very CPU intensive work (such as compressing or encrypting large amounts of data, encoding audio or video etc) this can often cause the front end to freeze, stop responding, drop network requests etc. If you have some processor intensive work to complete you need to think about how the work is spread over CPU cores, or CPUs.

Scenario 2: The front end does not need to display any specific result for the request

In this scenario you have more flexibilty about how and when you perform your background work because you can simply respond to the front end request with an acknowledgement that the request is received and will be processed in the (near) future. For example a Web API may respond with a 201 ACCEPTED HTTP response code to signal this.

You now want to queue the requests and process them somewhere other than your main/UI thread. There are a number of options, background threads being one of them, though not the simplest. You may also consider using a framework like https://www.hangfire.io/.

Another popular approach is to create a completely separate service or microservice that is responsible for your picking up requests from a queue and performing the work.

See https://docs.microsoft.com/en-us/dotnet/architecture/microservices/architect-microservice-container-applications/asynchronous-message-based-communication

Multithreading should come with a big warning message. Sometimes it is unavoidable, but it is always difficult and troublesome to get right. The C# APIs have evolved over time and so there's a lot to learn and a lot of ground to cover. It is often seen as a quick option to convert an application to be multithreaded, though you should be wary of this. Although more complex architectures as discussed in the link above seem overly burdensome, they invariably force you to think through a number of issues that come up when you begin to split up your application into threads, or processes, or services.

Module11
  • 53
  • 6
  • Thank you for your detailed response, I'm interested in Scenario 2, I only need to log events into a cosmos db table in the background after something has been changed in a production scheduler. All of this need to be done without affecting the application speed and the UI MUST NOT wait until the process is done. – Walid Ghodhbani Mar 25 '21 at 09:49
  • @WalidGhodhbani the quickest option for you is calling [CreateItemAsync](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.cosmos.container.createitemasync?view=azure-dotnet) with ConfigureAwait(false) [Stephen Toub ConfigureAwait FAQ](https://devblogs.microsoft.com/dotnet/configureawait-faq/) – Module11 Apr 26 '21 at 11:06