in DeviceBridge/Services/SubscriptionScheduler.cs [304:350]
private async Task AttemptDeviceConnection(string deviceId)
{
_logger.Info("Starting scheduled connection attempt for device {deviceId}.", deviceId);
// Synchronization is needed to avoid the race condition of a device connection being closed between the time we check
// if a connection should be open (the device has subscriptions) and the connection open call being actually issued.
var mutex = _dbAndConnectionStateSyncSemaphores.GetOrAdd(deviceId, new SemaphoreSlim(1, 1));
await mutex.WaitAsync();
try
{
if (!_hasDataSubscriptions.TryGetValue(deviceId, out _))
{
_logger.Info("Skipping scheduled connection attempt for device {deviceId} as it no longer has data subscriptions.", deviceId);
return;
}
await _connectionManager.AssertDeviceConnectionOpenAsync(deviceId, false /* permanent */);
_consecutiveConnectionFailures.TryRemove(deviceId, out int _);
_logger.Info("Successfully opened scheduled connection for device {deviceId}.", deviceId);
}
catch (Exception e)
{
int failedAttempts;
if (_consecutiveConnectionFailures.TryGetValue(deviceId, out failedAttempts))
{
failedAttempts++;
}
else
{
failedAttempts = 1;
}
_consecutiveConnectionFailures.AddOrUpdate(deviceId, failedAttempts, (key, oldValue) => failedAttempts);
// A failed connection attempt already includes the regular device client retries and a DPS registration attempt,
// so we back off after each failed attempt, up to 30 minutes.
var backoff = new Random().Next(0, Math.Min(ConnectionBackoffPerFailedAttemptSeconds * failedAttempts, MaxConnectionBackoffSeconds));
_logger.Error(e, "Failed to open scheduled connection for device {deviceId}. {failedAttempts} consecutive failed attempts so far. Connection scheduled for retry after {backoff} seconds.", deviceId, failedAttempts, backoff);
var notBefore = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + backoff;
_scheduledConnectionsNotBefore.AddOrUpdate(deviceId, notBefore, (key, oldValue) => notBefore);
}
finally
{
mutex.Release();
}
}