Search code examples
c#timerexceptionasync-awaitautoresetevent

Is it safe to call timer callback method like this?


Please correct me if I have some errors in this logic (not some elegancy things like getting rid of constructor initialization and using Init method instead for Poll). I have not had experience with timer callbacks so far. The code is pretty self-explanatory, I hope. What confuses me a bit is some mix of async things (like connection client creation) and further code - though, I just reused IClient class, it's not mine):

    public async Task<WaitForLoanActivationDto> WaitForLoanActivation(string userName, string accountGuid, int timeout)
    {
        const int dueTime = 0;
        const int pollPeriod = 500;
        Poll<WaitForLoanActivationDto> state = new Poll<WaitForLoanActivationDto>
        {
            Client = await _rpcConnectionPool.GetClientAsync(userName),
            AutoResetEvent = new AutoResetEvent(false),
            StartTime = DateTime.Now,
            Timeout = timeout,
            Parameters = new Variant[] { accountGuid },
            Result = new WaitForLoanActivationDto { Active = false }
        };

        Timer timer = new Timer(new TimerCallback(WaitForLoanActivationCallback), state, dueTime, pollPeriod);
        state.AutoResetEvent.WaitOne();
        timer.Dispose(state.AutoResetEvent);

        if (state.ThreadException != null)
        {
            throw state.ThreadException;
        }

        return state.Result;
    }

    private void WaitForLoanActivationCallback(object state)
    {
        Poll<WaitForLoanActivationDto> pollState = (Poll<WaitForLoanActivationDto>)state;

        if (pollState.StartTime.AddMilliseconds(pollState.Timeout) >= DateTime.Now)
        {
            try
            {
                using (RPCReply reply = ResultHelper.Check(pollState.Client.ExecuteRemoteCommand(WaitForLoanActivationRpcName, pollState.Parameters)))
                {
                    pollState.Result.Active = reply[2].IDList["Active"].AsBoolean().Value;
                    VariantList statusList = reply[2].IDList["statuses"].List;
                    if (statusList.Count > 0)
                    {
                        var statuses = CustomerInformationConverter.GetStatusesList(statusList);
                        pollState.Result.Statuses = statuses.ToArray();
                    }
                    if (pollState.Result.Active)
                    {
                        pollState.AutoResetEvent.Set();
                    }
                }
            }
            catch (Exception ex)
            {
                pollState.Result = null;
                pollState.ThreadException = ex;
                pollState.AutoResetEvent.Set();
            }
        }
        else
        {
            pollState.AutoResetEvent.Set();
        }
    }

Solution

  • Thank you guys. @ckuri, based on your idea I came up with the code below. I have not used Task.Delay though, because as I understood it creates delay even if the task is successfully complete - after it. The objective of my case is to run RPC method each pollPeriod milliseconds during timeout milliseconds. It the method returns Active == false - keep polling, otherwise - return the result. RPC execution time might take more than pollPeriod milliseconds, so if it's already running - no sense to spawn another task.

        public async Task<WaitForLoanActivationDto> WaitForLoanActivation(string userName, string accountGuid, int timeout)
        {
            var cancellationTokenSource = new CancellationTokenSource();
            try
            {
                const int pollPeriod = 500;
                IClient client = await _rpcConnectionPool.GetClientAsync(userName);
                DateTime startTime = DateTime.Now;
                WaitForLoanActivationDto waitForLoanActivationDto = null;
    
                while (startTime.AddMilliseconds(timeout) >= DateTime.Now)
                {
                    waitForLoanActivationDto = await Task.Run(() => WaitForLoanActivationCallback(client, accountGuid), cancellationTokenSource.Token);
                    if (waitForLoanActivationDto.Active)
                    {
                        break;
                    }
                    else
                    {
                        await Task.Delay(pollPeriod, cancellationTokenSource.Token);
                    }
                }
    
                return waitForLoanActivationDto;
            }
            catch (AggregateException ex)
            {
                cancellationTokenSource.Cancel();
                throw ex.InnerException;
            }
        }
    
        private WaitForLoanActivationDto WaitForLoanActivationCallback(IClient client, string accountGuid)
        {
            using (RPCReply reply = ResultHelper.Check(client.ExecuteRemoteCommand(WaitForLoanActivationRpcName, accountGuid)))
            {
                var waitForLoanActivationDto = new WaitForLoanActivationDto
                {
                    Active = reply[2].IDList["Active"].AsBoolean().Value
                };
                VariantList statusList = reply[2].IDList["statuses"].List;
                if (statusList.Count > 0)
                {
                    var statuses = CustomerInformationConverter.GetStatusesList(statusList);
                    waitForLoanActivationDto.Statuses = statuses.ToArray();
                }
                return waitForLoanActivationDto;
            }
        }