I have following problem: My server (ASP.MVC WebAPI) is tracking, when client application ("Agent") is on-line. It's storing this inf on following table:
+-------+-------------------------------+-----+
|AgentId| Date |State|
+-------+-------------------------------+-----+
| 1 | 30 may 2016 г. 3:02:20 +03:00 |True |
| 1 | 30 may 2016 г. 0:25:26 +02:00 |True |
| 1 |29 may 2016 г. 23:05:59 +02:00 |False|
| 1 |29 may 2016 г. 23:05:01 +02:00 |True |
+-------+-------------------------------+-----+
"Agent" software is sending some data to the server with some frequecy, each HTTP request is threated as ping/keep alive, and tels server, that agent is Up and running.
Algorithm is following:
- Ping received, if previous ping time isn't stored in RAM cache:
- Write ping time into cache
- Add DB record, that "Agent" become on-line
- Ping received, if previous ping time is stored in RAM cache:
- update ping time in cache
- Server haven'r received anything from "Agent" for 10 minutes (AgentTimeout)
- remove this agent's record from cache
- add record to DB that agent wen't offline
Following implementation is using non blocking algorithm. I'd like you to review correctness of this algorithm.
namespace X
{
[SingletonScope]
public class AgentPingReceiver : IAgentPingReceiver
{
private ILogger Log = Serilog.Log.Logger.ForContext<AgentPingReceiver>();
private static readonly TimeSpan AgentTimeout = TimeSpan.FromMinutes(10);
private static readonly TimeSpan StateRefreshInterval = TimeSpan.FromMinutes(1);
private CancellationTokenSource _cancelToken;
ConcurrentDictionary<int,DateTimeOffset> _lastPingReceived;
public AgentPingReceiver()
{
_lastPingReceived = new ConcurrentDictionary<int, DateTimeOffset>();
_cancelToken = new CancellationTokenSource();
Task.Run((Func<Task>)BackgroundChecker,_cancelToken.Token);
}
private async Task BackgroundChecker()
{
do
{
if (_cancelToken.IsCancellationRequested)
break;
try
{
await Task.Delay(StateRefreshInterval, _cancelToken.Token);
}
catch (TaskCanceledException)
{
break;
}
if (_lastPingReceived.Any())
{
var lastValidDateTime = DateTimeOffset.Now - AgentTimeout;
var itemsToRemove = _lastPingReceived
.Where(p => p.Value <= lastValidDateTime)
.ToList();
if(itemsToRemove.Any())
using (IocMannager.BeginScope())
{
IAgentStateChangeHandler handler =
IocMannager.GetServiceContainer<IAgentStateChangeHandler>().Service;
foreach (var kv in itemsToRemove)
{
await handler.ChangeAgentState(kv.Key, false, kv.Value);
if (!((ICollection<KeyValuePair<int, DateTimeOffset>>) _lastPingReceived).Remove(kv))
{
DateTimeOffset newValue;
if (_lastPingReceived.TryGetValue(kv.Key, out newValue))
{
//Item was resurected, but PingReceived() didn't knew that we are deleting it
// so updating DB accordingly
//
//P.S. I thought to put handler.ChangeAgentState(kv.Key, false, kv.Value) if Remove() return true
// but then I realized, that if item was offline for 10 minutes, than it really was gone!
// Just need to keep DB in sync.
try
{
await handler.ChangeAgentState(kv.Key, true, newValue);
}
catch (Exception e)
{
// we must not let this method to fail
Log.Error(e, "Error in background checker");
}
}
}
}
}
}
} while (true);
}
public async Task PingReceived(int agentId, DateTimeOffset when)
{
bool isNew = true;
_lastPingReceived.AddOrUpdate(agentId, when, (_, __) =>
{
isNew = false;
return when;
}
);
if (isNew)
{
using (var serviceContainer = IocMannager.GetServiceContainer<IAgentStateChangeHandler>())
{
var handler = serviceContainer.Service;
await handler.ChangeAgentState(agentId, true, when);
}
}
}
public void Dispose()
{
if (_cancelToken != null)
{
_cancelToken.Cancel();
_cancelToken.Dispose();
}
}
}
}
P.S. I foresee, that
if (!((ICollection<KeyValuePair<int, DateTimeOffset>>) _lastPingReceived).Remove(kv))
...
await handler.ChangeAgentState(kv.Key, true, newValue);
code can cause duplicates in some rare cases, so inside method ChangeAgentState I'm chcking for KeyDuplicate exception, and ignoring it.