0

I have a complex situation but I will try to short it out and let only know for important details. I am trying to implement a task-based job handling. here is the class for that:

internal class TaskBasedJob : IJob
{
    public WaitHandle WaitHandle { get; }
    public JobStatus Status { get; private set; }
    public TaskBasedJob(Func<Task<JobStatus>> action, TimeSpan interval, TimeSpan delay)
    {
         Status = JobStatus.NotExecuted;
        var semaphore = new SemaphoreSlim(0, 1);
        WaitHandle = semaphore.AvailableWaitHandle;

        _timer = new Timer(async x =>
        {
            // return to prevent duplicate executions
            // Semaphore starts locked so WaitHandle works properly
            if (semaphore.CurrentCount == 0 && Status != JobStatus.NotExecuted)
            {
                return;
                Status = JobStatus.Failure;
            }

            if(Status != JobStatus.NotExecuted)
                await semaphore.WaitAsync();

            try
            {
                await action();
            }
            finally
            {
                semaphore.Release();
            }

        }, null, delay, interval);
    }
}

Below is the scheduler class :

internal class Scheduler : IScheduler
{
    private readonly ILogger _logger;
    private readonly ConcurrentDictionary<string, IJob> _timers = new ConcurrentDictionary<string, IJob>();

    public Scheduler(ILogger logger)
    {
        _logger = logger;
    }

    public IJob ScheduleAsync(string jobName, Func<Task<JobStatus>> action, TimeSpan interval, TimeSpan delay = default(TimeSpan))
    {
        if (!_timers.ContainsKey(jobName))
        {
            lock (_timers)
            {
                if (!_timers.ContainsKey(jobName))
                    _timers.TryAdd(jobName, new TaskBasedJob(jobName, action, interval, delay, _logger));
            }
        }

        return _timers[jobName];
    }

    public IReadOnlyDictionary<string, IJob> GetJobs()
    {
        return _timers;
    }
}

Inside of this library I have a service like below: So the idea of this service is only to fetch some data at the dictionary called _accessInfos and its async method. You can see at the constructor I already add the job to fetch the data.

internal class AccessInfoStore : IAccessInfoStore
{
    private readonly ILogger _logger;
    private readonly Func<HttpClient> _httpClientFunc;
    private volatile Dictionary<string, IAccessInfo> _accessInfos;
    private readonly IScheduler _scheduler;
    private static string JobName = "AccessInfoProviderJob";

    public AccessInfoStore(IScheduler scheduler, ILogger logger, Func<HttpClient> httpClientFunc)
    {
        _accessInfos = new Dictionary<string, IAccessInfo>();
        _config = config;
        _logger = logger;
        _httpClientFunc = httpClientFunc;
        _scheduler = scheduler;
        scheduler.ScheduleAsync(JobName, FetchAccessInfos, TimeSpan.FromMinutes(1));
    }


    public IJob FetchJob => _scheduler.GetJobs()[JobName];

    private async Task<JobStatus> FetchAccessInfos() 
    {
        using (var client = _httpClientFunc())
        {
            accessIds = //calling a webservice

            _accessInfos = accessIds;

            return JobStatus.Success;
        }
    }

All of this code is inside another library that I have referenced into my ASP.NET Core 2.1 project. On the startup class I have a call like this:

//adding services
...
services.AddScoped<IScheduler, Scheduler>();
services.AddScoped<IAccessInfoStore, AccessInfoStore>();

var accessInfoStore = services.BuildServiceProvider().GetService<IAccessInfoStore>();

accessInfoStore.FetchJob.WaitHandle.WaitOne();

At the first time WaitOne() method does not work so the data are not loaded(_accessInfos is empty) but if I refresh the page again I can see the data loaded(_accessInfos is not empty but has data). So, as far as I know WaitOne() method is to block thread execution until my job is completed.

Does anybody know why WaitOne() method does not work properly or what I might be doing wrong ?

EDIT 1:

Scheduler only stores all IJob-s into a concurrent dictionary in order to get them later if needed mainly for showing them in a health page. Then every time we insert a new TaskBasedJob in dictionary the constructor will be executed and at the end we use a Timer to re-execute the job later after some interval, but in order to make this thread-safe I use SemaphoreSlim class and from there I expose WaitHandle. This is only for those rare cases I need to turn a method from async to sync. Because in general I would not use this because the job will execute in async manner for normal cases.

What I expect - The WaitOne() should stop execution of current thread and wait until my scheduled job is executed and then continue on executing current thread. In my case current thread is the one running Configure method at StartUp class.

Rey
  • 3,663
  • 3
  • 32
  • 55
  • I'm kinda confused by the code, to be honest; it seems to me that the natural thing here would be to just expose a `Task` that you can `await`, no? – Marc Gravell Aug 09 '18 at 15:30
  • hi @MarcGravell, I have to ask if I can expose that because I am not the only one using this library and maybe it might cause issues to the others. I have tried to simplify the question a little bit to be more clear. please let me know if still not clear so I can add some images tomorrow while debugging the application. – Rey Aug 09 '18 at 19:40
  • Have you had a look at [background tasks with hosted services](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1)? If it's not applicable in your case it may help point you in the right direction. – Brad Aug 09 '18 at 23:47
  • @MarcGravell That doesn't really help as far as I know the code considering it is done in the Startup.ConfigureServices method (which is sync). Either way, we also played around with actually waiting on the SemaphoreSlim itself, which has the same results. That is, when we are allowed to enter the SemaphoreSlim, which must happen after the job was run once (so by definition FetchAccessInfos must have executed), the _accessInfos is still empty, and the job status is NotExecuted. That's the real confusing part here. – FrankyBoy Aug 10 '18 at 08:02

1 Answers1

1

Colleague of Rajmond here. I figure out our issue. Basically, waiting works fine and so on. Our issue is simply that if you do IServiceCollection.BuildServiceProvider() you will get a different scope each time (and thus a different object is created even with Singleton instance). Simple way to try this out:

var serviceProvider1 = services.BuildServiceProvider();
var hashCode1 = serviceProvider1.GetService<IAccessInfoStore>().GetHashCode();
var hashCode2 = serviceProvider1.GetService<IAccessInfoStore>().GetHashCode();
var serviceProvider2 = services.BuildServiceProvider();
var hashCode3 = serviceProvider2.GetService<IAccessInfoStore>().GetHashCode();
var hashCode4 = serviceProvider2.GetService<IAccessInfoStore>().GetHashCode();

hashCode1 and hashCode2 are the same, same as hashCode3 and hashCode4 (because Singleton), but hashCode1/hashCode2 are not the same as hashCode3/hashCode4 (because different service provider).

The real fix will probably be some check in that IAccessInfoStore that will block internally until the job has finished the first time.

Cheers!

FrankyBoy
  • 1,865
  • 2
  • 18
  • 32