0

I have an issue with an endpoint blocking calls from other endpoints in my app. When we call this endpoint, this basically blocks all other api calls from executing, and they need to wait until this is finished.

       public async Task<ActionResult> GrantAccesstoUsers()
      {
         // other operations
         var grantResult = await 
         this._workSpaceProvider.GrantUserAccessAsync(this.CurrentUser.Id).ConfigureAwait(false);

         return this.Ok(result);
     }

The GrantUserAccessAsync method calls set of tasks that will run on a parallel.

        public async Task<List<WorkspaceDetail>> GrantUserAccessAsync(string currentUser)
        {
            var responselist = new List<WorkspaceDetail>();

            try
            {
                // calling these prematurely to be reused once threads are created
// none expensive calls
                var properlyNamedWorkSpaces = await this._helper.GetProperlyNamedWorkspacesAsync(true).ConfigureAwait(false);
                var dbGroups = await this._reportCatalogProvider.GetWorkspaceFromCatalog().ConfigureAwait(false);
                var catalogInfo = await this._clientServiceHelper.GetDatabaseConfigurationAsync("our-service").ConfigureAwait(false);

                if (properlyNamedWorkSpaces != null && properlyNamedWorkSpaces.Count > 0)
                {
// these methods returns tasks for parallel processing
                    var grantUserContributorAccessTaskList = await this.GrantUserContributorAccessTaskList(properlyNamedWorkSpaces, currentUser, dbGroups, catalogInfo).ConfigureAwait(false);
                    var grantUserAdminAccessTaskList = await this.GrantUserAdminAccessTaskList(properlyNamedWorkSpaces, currentUser, dbGroups, catalogInfo).ConfigureAwait(false);
                    var removeInvalidUserAndSPNTaskList = await this.RemoveAccessRightsToWorkspaceTaskList(properlyNamedWorkSpaces, dbGroups, currentUser, catalogInfo).ConfigureAwait(false);

                    var tasklist = new List<Task<WorkspaceDetail>>();
                    tasklist.AddRange(grantUserContributorAccessTaskList);
                    tasklist.AddRange(grantUserAdminAccessTaskList);
                    tasklist.AddRange(removeInvalidUserAndSPNTaskList);

                    // Start running Parallel Task
                    Parallel.ForEach(tasklist, task =>
                    {
                        Task.Delay(this._config.CurrentValue.PacingDelay);
                        task.Start();
                    });

                    // Get All Client Worspace Processing Results
                    var clientWorkspaceProcessingResult = await Task.WhenAll(tasklist).ConfigureAwait(false);

                    // Populate result
                    responselist.AddRange(clientWorkspaceProcessingResult.ToList());
                }
            }
            catch (Exception)
            {
                throw;
            }

            return responselist;
        }

These methods are basically identical in structure and they look like this:

private async Task<List<Task<WorkspaceDetail>>> GrantUserContributorAccessTaskList(List<Group> workspaces, string currentUser, List<WorkspaceManagement> dbGroups, DatabaseConfig catalogInfo)
        {
            var tasklist = new List<Task<WorkspaceDetail>>();

            foreach (var workspace in workspaces)
            {
                tasklist.Add(new Task<WorkspaceDetail>(() =>
                this.GrantContributorAccessToUsers(workspace, currentUser, dbGroups, catalogInfo).Result));
                // i added a delay here because we encountered an issue before in production and this seems to solve the problem. this is set to 4ms.
                Task.Delay(this._config.CurrentValue.DelayInMiliseconds);
            }

            return tasklist;
        }

The other methods called here looks like this:

private async Task<WorkspaceDetail> GrantContributorAccessToUsers(Group workspace, string currentUser, List<Data.ReportCatalogDB.WorkspaceManagement> dbGroups, DatabaseConfig catalogInfo)
        {
            // This prevents other thread or task to start and prevents exceeding the number of threads allowed
            await this._batchProcessor.WaitAsync().ConfigureAwait(false);

            var result = new WorkspaceDetail();

            try
            {
                
                var contributorAccessresult = await this.helper.GrantContributorAccessToUsersAsync(workspace, this._powerBIConfig.CurrentValue.SPNUsers).ConfigureAwait(false);

                if (contributorAccessresult != null 
                    && contributorAccessresult.Count > 0)
                {
                    // do something
                }
                else
                {
                    // do something
                }

// this is done to reuse the call that is being executed in the helper above. it's an expensive call from an external endpoint so we opted to reuse what was used in the initial call, instead of calling it again for this process
                var syncWorkspaceAccessToDb = await this.SyncWorkspaceAccessAsync(currentUser, workspace.Id, contributorAccessresult, dbGroups, catalogInfo).ConfigureAwait(false);

                foreach (var dbResponse in syncWorkspaceAccessToDb) {

                    result.ResponseMessage += dbResponse.ResponseMessage;
                }
            }
            catch (Exception ex)
            {
                this._loghelper.LogEvent(this._logger, logEvent, OperationType.GrantContributorAccessToWorkspaceManager, LogEventStatus.FAIL);

            }
            finally
            {
                this._batchProcessor.Release();
            }

            return result;
        }

The last method called writes the record in a database table:

private async Task<List<WorkspaceDetail>> SyncWorkspaceAccessAsync(string currentUser,
            Guid workspaceId,
            List<GroupUser> groupUsers,
            List<WorkspaceManagement> dbGroups,
            DatabaseConfig catalogInfo) {

            var result = new List<WorkspaceDetail>();

            var tasklist = new List<Task<WorkspaceDetail>>();

            // get active workspace details from the db
            var workspace = dbGroups.Where(x => x.PowerBIGroupId == workspaceId).FirstOrDefault();

            try
            {
                // to auto dispose the provider, we are creating this for each instance because 
                // having only one instance creates an error when the other task starts running
                using (var contextProvider = this._contextFactory.GetReportCatalogProvider(
                        catalogInfo.Server,
                        catalogInfo.Database,
                        catalogInfo.Username,
                        catalogInfo.Password,
                        this._dbPolicy))
                {
                    if (workspace != null)
                    {
                        // get current group users in the db from the workspace object
                        var currentDbGroupUsers = workspace.WorkspaceAccess.Where(w => w.Id == workspace.Id
                                                                                && w.IsDeleted == false).ToList();
                        #region identify to process

                        #region users to add
                        // identify users to add
                        var usersToAdd = groupUsers.Where(g => !currentDbGroupUsers.Any(w => w.Id == workspace.Id ))
                                                                    .Select(g => new WorkspaceAccess
                                                                    {

                                                                        // class properties

                                                                    }).ToList();
                        #endregion

                        var addTasks = await this.AddWorkspaceAccessToDbTask(catalogProvider, usersToAdd, workspace.PowerBIGroupId, workspace.WorkspaceName).ConfigureAwait(false);
                        
                        tasklist.AddRange(addTasks);

                        // this is a potential fix that i did, hoping adding another parallel thread can solve the problem
                        Parallel.ForEach(tasklist, new ParallelOptions { MaxDegreeOfParallelism = this._config.CurrentValue.MaxDegreeOfParallelism }, task =>
                        {
                            Task.Delay(this._config.CurrentValue.PacingDelay);
                            task.Start();
                        });

                        var processResult = await Task.WhenAll(tasklist).ConfigureAwait(false);

                        // Populate result
                        result.AddRange(processResult.ToList());

                    } 
                }

            }
            catch (Exception ex)
            {
                // handle error
            }

            return result;
        }

I tried some potential solutions already, like the methods here are written with Task.FromResult before instead of async so I changed that. Reference is from this thread:

Using Task.FromResult v/s await in C#

Also, I thought it was a similar issue that we faced before when we are creating multiple db context connections needed when running multiple parallel tasks by adding a small delay on tasks but that didn't solve the problem.

Task.Delay(this._config.CurrentValue.DelayInMiliseconds);

Any help would be much appreciated.

Dustine Tolete
  • 461
  • 1
  • 7
  • 19

1 Answers1

1

I assume your this._batchProcessor is an instance of SemaphoreSlim. If your other endpoints somehow call

await this._batchProcessor.WaitAsyc()

that means they can't go further until semaphor will be released.

Another thing I'd like to mention: please avoid using Parallel.ForEach with async/await. TPL is not designed to work with async/await, here is good answer why you should avoid using them together: Nesting await in Parallel.ForEach

Igor Goyda
  • 1,949
  • 5
  • 10
  • My other endpoints does not call the _batchProcessor, but in this one in particular, the batch processor handles 3 different operations in the GrantUserAccessAsync method. I already have something like this in place that also uses SemaphoreSlim and Parallel.ForEach, but it does not block any other calls when it is running. Should I create new batch processor for each of the operation? I am looking into your link to replace Parallel.ForEach in my code. – Dustine Tolete Mar 02 '21 at 09:18
  • It's quite hard to understand why other endpoints being blocked. Usually it happens then different requests fight for the same resource (it might be a semaphore, lock or smth else). You can do the following investigation: temporarily comment out an invocation of `GrantUserAccessAsync` in your `GrantAccesstoUsers`, and send several requests to other endpoints. If there will be no issue, so the reason is definitely connected to `GrantUserAccessAsync` method. – Igor Goyda Mar 02 '21 at 11:14
  • I forgot to update this, but I refactored the code by wrapping the Parallel.ForEach call into an async Task based on the link you attached. Thank you! Marking this as the answer. – Dustine Tolete Mar 17 '21 at 10:17