Search code examples
c#asp.net-core.net-standard-2.0

WaitHandle.WaitOne() method in SemaphoreSlim class does not work properly


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.


Solution

  • 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!