Пример #1
0
        protected override async Task ExecuteAsync(CancellationToken cancellation)
        {
            while (!cancellation.IsCancellationRequested)
            {
                // Grab a hold of a concrete list of adopted tenantIds at the current moment
                var tenantIds = _instanceInfo.AdoptedTenantIds.ToList();
                if (tenantIds.Any())
                {
                    // Match every tenantID to the Task of polling
                    IEnumerable <Task> tasks = tenantIds.Select(async tenantId =>
                    {
                        try // To make sure the background service keeps running
                        {
                            // Begin serializable transaction
                            using var trx = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = IsolationLevel.Serializable }, TransactionScopeAsyncFlowOption.Enabled);

                            // Retrieve NEW or stale PENDING emails, after marking them as fresh PENDING
                            IEnumerable <EmailForSave> emailEntities = await _repo.Notifications_Emails__Poll(tenantId, _options.PendingNotificationExpiryInSeconds, PollingBatchSize, cancellation);
                            IEnumerable <Email> emails = emailEntities.Select(e => ExternalNotificationsService.FromEntity(e, tenantId));

                            // Queue the emails for dispatching
                            _queue.QueueBackgroundWorkItem(emails);

                            trx.Complete();

                            // Log a warning, since in theory this job should rarely find anything, if it finds stuff too often it means something is wrong
                            if (emails.Any())
                            {
                                _logger.LogWarning($"{nameof(EmailPollingJob)} found {emailEntities.Count()} emails in database for tenant {tenantId}.");
                            }
                        }
                        catch (Exception ex)
                        {
                            _logger.LogError(ex, $"Error in {nameof(EmailPollingJob)}. TenantId = {tenantId}");
                        }
                    });

                    // Wait until all adopted tenants have returned
                    await Task.WhenAll(tasks);
                }

                // Go to sleep until the next round
                await Task.Delay(_options.NotificationCheckFrequencyInSeconds * 1000, cancellation);
            }
        }
Пример #2
0
        /// <summary>
        /// Queues the notifications in the database and if the database table is clearn queues them immediately for processing.
        /// If the database contains stale notifications, then do not queue the new ones immediately. Instead wait for <see cref="SmsPollingJob"/>
        /// to pick them out in order and schedule them, this prevent notifications being dispatched grossly out of order
        /// </summary>
        public async Task Enqueue(
            int tenantId,
            List <Email> emails                       = null,
            List <SmsMessage> smsMessages             = null,
            List <PushNotification> pushNotifications = null,
            CancellationToken cancellation            = default)
        {
            // (1) Map notifications to Entities and validate them
            // Email
            emails ??= new List <Email>();
            var validEmails   = new List <Email>(emails.Count);
            var emailEntities = new List <EmailForSave>(emails.Count);

            foreach (var email in emails)
            {
                var emailEntity = ToEntity(email);
                emailEntities.Add(emailEntity);

                var error = EmailValidation.Validate(email);
                if (error != null)
                {
                    emailEntity.State        = EmailState.ValidationFailed;
                    emailEntity.ErrorMessage = error;
                }
                else
                {
                    validEmails.Add(email);
                }
            }

            // SMS
            smsMessages ??= new List <SmsMessage>();
            var validSmsMessages = new List <SmsMessage>(smsMessages.Count);
            var smsEntities      = new List <SmsMessageForSave>(smsMessages.Count);

            foreach (var sms in smsMessages)
            {
                var smsEntity = ToEntity(sms);
                smsEntities.Add(smsEntity);

                var error = SmsValidation.Validate(sms);
                if (error != null)
                {
                    smsEntity.State        = SmsState.ValidationFailed;
                    smsEntity.ErrorMessage = error;
                }
                else
                {
                    validSmsMessages.Add(sms);
                }
            }

            // Push
            pushNotifications ??= new List <PushNotification>();
            var validPushNotifications = new List <PushNotification>(pushNotifications.Count);
            var pushEntities           = new List <PushNotificationForSave>(pushNotifications.Count);

            // TODO

            // Start a serializable transaction
            using var trx = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = IsolationLevel.Serializable }, TransactionScopeAsyncFlowOption.Enabled);

            // If the table already contains notifications that are 90% expired, do not queue the new notifications
            int expiryInSeconds = _options.PendingNotificationExpiryInSeconds * 9 / 10;

            // (2) Call the stored procedure
            // Persist the notifications in the database, the returned booleans will tell us which notifications we can queue immediately
            var(queueEmails, queueSmsMessages, queuePushNotifications) = await _repo.Notifications_Enqueue(
                tenantId, expiryInSeconds, emailEntities, smsEntities, pushEntities, cancellation);

            // (3) Map the Ids back to the DTOs and queue valid notifications
            // Email
            if (queueEmails && validEmails.Any())
            {
                // Map the Ids
                for (int i = 0; i < emailEntities.Count; i++)
                {
                    var entity = emailEntities[i];
                    var dto    = emails[i];
                    dto.EmailId  = entity.Id;
                    dto.TenantId = tenantId;
                }

                // Queue (Emails are queued in bulk unlike SMS and Push)
                _emailQueue.QueueBackgroundWorkItem(validEmails);
            }

            // SMS
            if (queueSmsMessages && validSmsMessages.Any())
            {
                // Map the Ids
                for (int i = 0; i < smsEntities.Count; i++)
                {
                    var smsEntity = smsEntities[i];
                    var sms       = smsMessages[i];
                    sms.MessageId = smsEntity.Id;
                    sms.TenantId  = tenantId;
                }

                // Queue
                _smsQueue.QueueAllBackgroundWorkItems(validSmsMessages);
            }

            // Push
            if (queuePushNotifications && validPushNotifications.Any())
            {
                // Map the Ids
                for (int i = 0; i < smsEntities.Count; i++)
                {
                    var entity = pushEntities[i];
                    var dto    = pushNotifications[i];
                    dto.PushId   = entity.Id;
                    dto.TenantId = tenantId;
                }

                // Queue
                _pushQueue.QueueAllBackgroundWorkItems(validPushNotifications);
            }

            trx.Complete();
        }