1

i am having a hard time to figure this one out, maybe someone can help me out here.

i have an Asp.net Core 2.0 web application, i am using Hangfire to process jobs in the background, usually its to sync products or orders between different platforms, but there is one job which always fail with the 'Insufficient memory to continue the execution of the program.' and also 'Object reference not set to an instance of an object' i log the trace and it takes me to a Parallel.ForEach where the error is being thrown, now when i do the same thing manually (meaning without hangfire) i get no exception and everything works just fine.

so let me share some code here.

so first we setup hangfire like this. inside the startup class, at the method Configure, we do this.

            app.UseHangfireDashboard("/tasks", new DashboardOptions() {
            Authorization = new[] { new HangFireAuthorizationFilter() }
        });

            var schedules = new ScheduleTasks(app.ApplicationServices);
            schedules.RegisterRecurentJobs();

now the ScheduleTasks looks like this.

    public class ScheduleTasks
{
    private readonly IApiService _apiService;
    private readonly ISettingService _settingService;

    public ScheduleTasks(IServiceProvider serviceProvider)
    {
        var scope = serviceProvider.CreateScope();
        _apiService = scope.ServiceProvider.GetService<IApiService>();
        _settingService = scope.ServiceProvider.GetService<ISettingService>();
    }

    public void RegisterRecurentJobs()
    {
        var settings = _settingService.LoadSetting<ApiSettings>();

        if(!settings.SilentMode)
        {
            RecurringJob.AddOrUpdate<IApiService>("Config", x => x.SyncConfig(), "* 1 * * *");
            RecurringJob.AddOrUpdate<IApiService>("Manufacturers", x => x.SyncManufacturers(), "* 2 * * *");
            RecurringJob.AddOrUpdate<IApiService>("Products", x => x.SyncProducts(30, false), "* 3 * * *");
        }

    }
}

and now here is my api service method that executed the SyncProducts.

        public async Task SyncProducts(int days, bool hasSalesOnly = true)
    {

        var s = new StringBuilder();
        try
        {
            var search = new SearchModel()
            {
                ModifiedDate = new DateRange()
                {
                    FromDate = DateTime.Today.AddDays(-days)
                },
                PageSize = 300000,
                HaveSalesOnly = hasSalesOnly
            };

            var ids = await _productService.GetProductFeed(search);
            var hasNext = ids?.Result?.AdditionalResultsId;
            var failedIds = new List<string>();
            var retry = true;

            while (ids.Result.Ids != null && ids.Result.Ids.Count > 0)
            {
                var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = 20 };

                s.Append("[");
                var start = true;

                Parallel.ForEach(ids.Result.Ids, parallelOptions, id =>
                {
                    var itemResult = _productService.GetProductByProductId(id).Result;
                    if (itemResult.Success)
                    {
                        lock (s)
                        {
                            if (start)
                            {
                                s.Append(itemResult.ResponseString);
                            }
                            else
                            {
                                var r = "," + itemResult.ResponseString;
                                s.Append(r);
                            }

                            start = false;
                        }
                    }
                    else
                    {
                        failedIds.Add(id);
                    }

                    itemResult = null;

                });

                s.Append("]");

                var newFileName = "result.json";
                var newFileDir = GetJsonFileDirectoryPath() + newFileName;
                var baseDir = Environment.CurrentDirectory + "/wwwroot";
                var dataPath = baseDir + newFileDir;
                File.WriteAllText(dataPath, s.ToString());
                s = null;

                ids.Result.Ids = null;

                var param = new SqlParameter
                {
                    ParameterName = "path",
                    SqlDbType = SqlDbType.VarChar,
                    Direction = ParameterDirection.Input,
                    Value = dataPath
                };

                var imagesToProcess = await _itemRepository.ExecuteStoredProcedureList<SyncItems>("[dbo].[SyncItems]", param);

                foreach (var image in imagesToProcess)
                {
                    if (image.ProductId == null)
                        continue;

                    //for now delete all images, and save the new ones.
                    var imagesToDelete = _pictureService.GetProductPicturesByProductId(image.ProductId.Value);

                    foreach (var i in imagesToDelete)
                    {
                        await _pictureService.DeleteProductPicture(i);
                    }

                    if (image.ImageUrls != null)
                    {
                        var imageList = image.ImageUrls.Split(',');

                        foreach (var i in imageList)
                        {
                            await _pictureService.InsertProductPictureFromPictureUrl(i, image.ProductId.Value);
                        }
                    }


                }

                if (!string.IsNullOrEmpty(hasNext))
                {
                    ids = await _productService.GetNextPage(ids.Result.AdditionalResultsId);
                    hasNext = ids.Result.AdditionalResultsId;
                }
                else
                {
                    ids.Result.Ids = null;
                }

                if (string.IsNullOrEmpty(hasNext) && ids.Result.Ids == null && retry ||
                    string.IsNullOrEmpty(hasNext) && ids.Result.Ids.Count == 0 && retry)
                {
                    ids = new BaseSearchResult();
                    ids.Result.Ids = failedIds;

                    failedIds = new List<string>();
                    retry = false;
                }
            }

            ids = null;

        }
        catch (Exception ex)
        {
            s = null;
            aEvent = new ApiEvent()
            {
                Level = LogLevel.Critical,
                EventDate = DateTime.Now,
                User = "Api",
                EventType = EventTypeEnum.Product,
                EntityTypeId = "",
                Event = ex.Message,
                Details = ex.StackTrace
            };

            await _eventService.PublishEvent(aEvent);
        }

    }

if i execute this method manually, either by writing a functional test to execute the method, or by having a button on my page which triggers this method to execute, everything works fine, but when Hangfire is trying to execute the exact same method, i get this error.

One or more errors occurred. (Insufficient memory to continue the execution of the program.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.) (Object reference not set to an instance of an object.)

and the trace is this.

at System.Threading.Tasks.TaskReplicator.Run[TState](ReplicatableUserAction1 action, ParallelOptions options, Boolean stopOnFirstFailure) at System.Threading.Tasks.Parallel.ForWorker[TLocal](Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action1 body, Action2 bodyWithState, Func4 bodyWithLocal, Func1 localInit, Action1 localFinally) --- End of stack trace from previous location where exception was thrown --- at System.Threading.Tasks.Parallel.ThrowSingleCancellationExceptionOrOtherException(ICollection exceptions, CancellationToken cancelToken, Exception otherException) at System.Threading.Tasks.Parallel.ForWorker[TLocal](Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action1 body, Action2 bodyWithState, Func4 bodyWithLocal, Func1 localInit, Action1 localFinally) at System.Threading.Tasks.Parallel.ForEachWorker[TSource,TLocal](IList1 list, ParallelOptions parallelOptions, Action1 body, Action2 bodyWithState, Action3 bodyWithStateAndIndex, Func4 bodyWithStateAndLocal, Func5 bodyWithEverything, Func1 localInit, Action1 localFinally) at System.Threading.Tasks.Parallel.ForEachWorker[TSource,TLocal](IEnumerable1 source, ParallelOptions parallelOptions, Action1 body, Action2 bodyWithState, Action3 bodyWithStateAndIndex, Func4 bodyWithStateAndLocal, Func5 bodyWithEverything, Func1 localInit, Action1 localFinally) at System.Threading.Tasks.Parallel.ForEach[TSource](IEnumerable1 source, ParallelOptions parallelOptions, Action`1 body) at *****.*****.Services.ApiService.SyncProducts(Int32 days, Boolean hasSalesOnly) in C:***************.****\Services\ApiService.cs:line 493

can anyone help me figure why it fails when hangfire executes the method?

thanks in advance for everyone looking into this.

Sol Stein
  • 656
  • 10
  • 29
  • Bloody murder, this is some *bad* code. I'm not sure what you exact issue is as it could be any number of problems. You're loading a metric crap ton of objects into memory. You're blocking on async. You've got hugely inefficient database queries, and a ton of them at that. You've got actual locks. You're doing naive and inefficient file access. The list goes on. – Chris Pratt Mar 08 '19 at 16:20
  • @ChrisPratt the reason why i load them into memory is because the api i am using has no support for flat files or anything the like, so i can only obtain 1 product at a time, so my best bet to get it done fast, is by obtaining first a list of id's that i need to update, and then put them into a parallel multi thread to obtain the info for every product in the list, in that way it gets done pretty fast. – Sol Stein Mar 08 '19 at 17:15
  • @ChrisPratt i am not doing any databse queries untill the point of processing the images, i use a stored procedure to process the whole chunk of data that i write into a json file, all the service that u see are api calls which i need to do based on the api documentation that i got from that 3rd party. – Sol Stein Mar 08 '19 at 17:18
  • @ChrisPratt the reason i do a lock is because string builder is not multi thread safe, and since i need access to the string builder in multi thread, i need to add a lock on it. – Sol Stein Mar 08 '19 at 17:19
  • Use `SemaphoreSlim` instead. – Chris Pratt Mar 08 '19 at 19:02
  • @ChrisPratt based on benchmarks that i see here https://stackoverflow.com/a/53407851/2457865 it seems that using lock is twice faster than SemaphoreSlim – Sol Stein Mar 08 '19 at 21:26

0 Answers0