Ejemplo n.º 1
0
        public async Task SendAsync(SmsMessage sms, CancellationToken cancellation = default)
        {
            var error = SmsValidation.Validate(sms);

            if (error != null)
            {
                throw new SmsInvalidException(error);
            }

            var serviceSid = !string.IsNullOrWhiteSpace(_options.ServiceSid) ? _options.ServiceSid : throw new InvalidOperationException("ServiceSid is missing");

            // Extract the values from the argument
            var to        = new PhoneNumber(sms.ToPhoneNumber);
            var message   = sms.Message;
            var messageId = sms.MessageId;
            var tenantId  = sms.TenantId;

            // Calculate the callbackUri (if required)
            Uri callbackUri = null;

            if (messageId != 0)
            {
                string hostname  = _options.CallbackHost?.WithoutTrailingSlash() ?? throw new InvalidOperationException("CallbackHost is missing");
                string stringUri = $"{hostname}/api/sms-callback?{MessageIdParamName}={messageId}";
                if (tenantId != 0)
                {
                    stringUri += $"&{TenantIdParamName}={tenantId}";
                }

                callbackUri = new Uri(stringUri);
            }

            // Exponential backoff
            const int maxAttempts  = 5;
            const int maxBackoff   = 25000; // 25 Seconds
            const int minBackoff   = 1000;  // 1 Second
            const int deltaBackoff = 1000;  // 1 Second

            int attemptsSoFar = 0;
            int backoff       = minBackoff;

            while (attemptsSoFar < maxAttempts && !cancellation.IsCancellationRequested)
            {
                attemptsSoFar++;

                try
                {
                    // Send using Twilio's Messaging Service
                    await MessageResource.CreateAsync(
                        body : message,
                        messagingServiceSid : serviceSid,
                        to : to,
                        statusCallback : callbackUri
                        );

                    break; // Success
                }
                catch (ApiException ex) when(ex.Status == (int)HttpStatusCode.TooManyRequests || ex.Status >= 500)
                {
                    // Twilio imposes a maximum number of concurrent calls, and returns 429 when that maximum is reached
                    // Here we implement exponential backoff to retry the call few more times before giving up as
                    // recommended here https://bit.ly/2CWYrjQ
                    if (attemptsSoFar < maxAttempts)
                    {
                        var randomOffset = _rand.Next(0, deltaBackoff);
                        await Task.Delay(backoff + randomOffset, cancellation);

                        // Double the backoff for next attempt
                        backoff = Math.Min(backoff * 2, maxBackoff);
                    }
                    else
                    {
                        _logger.LogError($"Twilio: 429 Too Many Requests even after {attemptsSoFar} attempts with exponential backoff.");

                        throw; // Give up
                    }
                }
            }
        }