protected override async Task ExecuteAsync(CancellationToken cancellation) { while (!cancellation.IsCancellationRequested) { try // To make sure the background service keeps running { // When the queue is empty, this goes to sleep until an item is enqueued var(sms, scheduledAt) = await _queue.DequeueAsync(cancellation); // This SMS spent too long in the queue it is considered stale, SmsPollingJob will // (and might have already) pick it up and send it again, so ignore it if (IsStale(scheduledAt)) { _logger.LogWarning($"Stale SMS remained in the {nameof(SmsQueue)} for {(DateTimeOffset.Now - scheduledAt).TotalSeconds} seconds. TenantId = {sms.TenantId}, MessageId = {sms.MessageId}."); continue; } // Begin serializable transaction using var trx = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = IsolationLevel.Serializable }, TransactionScopeAsyncFlowOption.Enabled); // Update the state first (since this action can be rolled back) await _repo.Notifications_SmsMessages__UpdateState(sms.TenantId, sms.MessageId, SmsState.Dispatched, DateTimeOffset.Now); // actions that modify state should not use cancellationToken try { // Send the SMS after you update the state in the DB, since sending SMS // is non-transactional and therefore cannot be rolled back await _smsSender.SendAsync(sms); } catch (Exception ex) { _logger.LogWarning(ex, $"Failed to Dispatch SMS. TenantId = {sms.TenantId}, MessageId = {sms.MessageId}."); // If sending the SMS fails, update the state to DispatchFailed together with the error message await _repo.Notifications_SmsMessages__UpdateState(sms.TenantId, sms.MessageId, SmsState.DispatchFailed, DateTimeOffset.Now, ex.Message); } trx.Complete(); } catch (Exception ex) { _logger.LogError(ex, $"Error in {nameof(SmsJob)}."); } } }
public async Task HandleCallback(SmsEventNotification smsEvent, CancellationToken cancellation) { // Nothing to do if (smsEvent == null || smsEvent.TenantId == null || smsEvent.TenantId == 0) // Right now we do not handle null tenant Ids, those were probably sent from identity or admin servers { return; } // Map the event to the database representation var state = smsEvent.Event switch { SmsEvent.Sent => SmsState.Sent, SmsEvent.Failed => SmsState.SendingFailed, SmsEvent.Delivered => SmsState.Delivered, SmsEvent.Undelivered => SmsState.DeliveryFailed, _ => throw new InvalidOperationException($"[Bug] Unknown {nameof(SmsEvent)} = {smsEvent.Event}"), // Future proofing }; // Update the state in the database (should we make it serializable?) await _repo.Notifications_SmsMessages__UpdateState(smsEvent.TenantId.Value, smsEvent.MessageId, state, smsEvent.Timestamp, smsEvent.Error, cancellation); }