/// <summary>
        /// The <see cref="ProcessMessageStreamAsync(Stream)"/> method is overridden to
        /// provide specific behavior for this base class.
        /// </summary>
        /// <remarks>
        /// This method cannot be overridden.
        /// </remarks>
        /// <param name="stream">The stream with the request payload.</param>
        /// <returns>The task object representing the asynchronous operation.</returns>
        public override sealed async Task <Stream> ProcessMessageStreamAsync(Stream stream)
        {
            // read stream into memory
            LogInfo("reading stream body");
            string snsEventBody;

            using (var reader = new StreamReader(stream)) {
                snsEventBody = reader.ReadToEnd();
            }
            var stopwatch = Stopwatch.StartNew();
            var metrics   = new List <LambdaMetric>();

            // process received sns record (there is only ever one)
            try {
                // sns event deserialization
                LogInfo("deserializing SNS event");
                try {
                    var snsEvent = LambdaSerializer.Deserialize <SNSEvent>(snsEventBody);
                    _currentRecord = snsEvent.Records.First().Sns;

                    // message deserialization
                    LogInfo("deserializing message");
                    var message = Deserialize(CurrentRecord.Message);

                    // process message
                    LogInfo("processing message");
                    await ProcessMessageAsync(message);

                    // record successful processing metrics
                    stopwatch.Stop();
                    var now = DateTimeOffset.UtcNow;
                    metrics.Add(("MessageSuccess.Count", 1, LambdaMetricUnit.Count));
                    metrics.Add(("MessageSuccess.Latency", stopwatch.Elapsed.TotalMilliseconds, LambdaMetricUnit.Milliseconds));
                    metrics.Add(("MessageSuccess.Lifespan", (now - CurrentRecord.GetLifespanTimestamp()).TotalSeconds, LambdaMetricUnit.Seconds));
                    return("Ok".ToStream());
                } catch (Exception e) {
                    LogError(e);
                    try {
                        // attempt to send failed message to the dead-letter queue
                        await RecordFailedMessageAsync(LambdaLogLevel.ERROR, FailedMessageOrigin.SQS, LambdaSerializer.Serialize(snsEventBody), e);

                        // record failed processing metrics
                        metrics.Add(("MessageDead.Count", 1, LambdaMetricUnit.Count));
                    } catch {
                        // NOTE (2020-04-22, bjorg): since the message could not be sent to the dead-letter queue,
                        //  the next best action is to let Lambda retry it; unfortunately, there is no way
                        //  of knowing how many attempts have occurred already.

                        // unable to forward message to dead-letter queue; report failure to lambda so it can retry
                        metrics.Add(("MessageFailed.Count", 1, LambdaMetricUnit.Count));
                        throw;
                    }
                    return($"ERROR: {e.Message}".ToStream());
                }
            } finally {
                _currentRecord = null;
                LogMetric(metrics);
            }
        }
        public void LambdaExpressionShouldSerialize(LambdaExpression lambda)
        {
            var target = lambdaSerializer.Serialize(lambda, new SerializationState());

            Assert.Equal((ExpressionType)target.Type, lambda.NodeType);
        }
Exemple #3
0
        private async Task WriteResponse(
            CloudFormationResourceRequest <TProperties> rawRequest,
            CloudFormationResourceResponse <TAttributes> rawResponse
            )
        {
            Exception exception = null;
            var       backoff   = TimeSpan.FromMilliseconds(100);

            // write response to pre-signed S3 URL
            for (var i = 0; i < MAX_SEND_ATTEMPTS; ++i)
            {
                try {
                    if (rawRequest.ResponseURL == null)
                    {
                        throw new InvalidOperationException("ResponseURL is missing");
                    }
                    var httpResponse = await HttpClient.SendAsync(new HttpRequestMessage {
                        RequestUri = new Uri(rawRequest.ResponseURL),
                        Method     = HttpMethod.Put,
                        Content    = new ByteArrayContent(Encoding.UTF8.GetBytes(LambdaSerializer.Serialize(rawResponse)))
                    });

                    if (httpResponse.StatusCode != HttpStatusCode.OK)
                    {
                        throw new LambdaCustomResourceException(
                                  "PUT operation to pre-signed S3 URL failed with status code: {0} [{1} {2}] = {3}",
                                  httpResponse.StatusCode,
                                  rawRequest.RequestType,
                                  rawRequest.ResourceType ?? "<MISSING>",
                                  await httpResponse.Content.ReadAsStringAsync()
                                  );
                    }
                    return;
                } catch (InvalidOperationException e) {
                    exception = e;
                    break;
                } catch (Exception e) {
                    exception = e;
                    LogErrorAsWarning(e, "writing response to pre-signed S3 URL failed");
                    await Task.Delay(backoff);

                    backoff = TimeSpan.FromSeconds(backoff.TotalSeconds * 2);
                }
            }
            if (exception == null)
            {
                exception = new ShouldNeverHappenException($"ALambdaCustomResourceFunction.WriteResponse failed w/o an explicit");
            }

            // max attempts have been reached; fail permanently and record the failed request for playback
            LogError(exception);
            await RecordFailedMessageAsync(LambdaLogLevel.ERROR, FailedMessageOrigin.CloudFormation, LambdaSerializer.Serialize(rawRequest), exception);
        }
        /// <summary>
        /// The <see cref="ProcessMessageStreamAsync(Stream)"/> method is overridden to
        /// provide specific behavior for this base class.
        /// </summary>
        /// <remarks>
        /// This method cannot be overridden.
        /// </remarks>
        /// <param name="stream">The stream with the request payload.</param>
        /// <returns>The task object representing the asynchronous operation.</returns>
        public override sealed async Task <Stream> ProcessMessageStreamAsync(Stream stream)
        {
            // deserialize stream to sqs event
            LogInfo("deserializing stream to SQS event");
            var sqsEvent = LambdaSerializer.Deserialize <SQSEvent>(stream);

            if (!sqsEvent.Records.Any())
            {
                return($"empty batch".ToStream());
            }

            // process all received sqs records
            var eventSourceArn     = sqsEvent.Records.First().EventSourceArn;
            var successfulMessages = new List <SQSEvent.SQSMessage>();

            foreach (var record in sqsEvent.Records)
            {
                _currentRecord = record;
                var metrics = new List <LambdaMetric>();
                try {
                    var stopwatch = Stopwatch.StartNew();

                    // attempt to deserialize the sqs record
                    LogInfo("deserializing message");
                    var message = Deserialize(record.Body);

                    // attempt to process the sqs message
                    LogInfo("processing message");
                    await ProcessMessageAsync(message);

                    successfulMessages.Add(record);

                    // record successful processing metrics
                    stopwatch.Stop();
                    var now = DateTimeOffset.UtcNow;
                    metrics.Add(("MessageSuccess.Count", 1, LambdaMetricUnit.Count));
                    metrics.Add(("MessageSuccess.Latency", stopwatch.Elapsed.TotalMilliseconds, LambdaMetricUnit.Milliseconds));
                    metrics.Add(("MessageSuccess.Lifespan", (now - record.GetLifespanTimestamp()).TotalSeconds, LambdaMetricUnit.Seconds));
                } catch (Exception e) {
                    // NOTE (2020-04-21, bjorg): delete message if error is not retriable (i.e. logic error) or
                    //  the message has reached it's maximum number of retries.
                    var deleteMessage = !(e is LambdaRetriableException) ||
                                        (record.GetApproximateReceiveCount() >= await Provider.GetMaxRetriesForQueueAsync(record.EventSourceArn));

                    // the intent is to delete the message
                    if (deleteMessage)
                    {
                        // NOTE (2020-04-22, bjorg): always log an error since the intent is to send
                        //  this message to the dead-letter queue.
                        LogError(e);
                        try {
                            // attempt to send failed message to the dead-letter queue
                            await RecordFailedMessageAsync(LambdaLogLevel.ERROR, FailedMessageOrigin.SQS, LambdaSerializer.Serialize(record), e);

                            // record forwarded message as successful so it gets deleted from the queue
                            successfulMessages.Add(record);

                            // record failed processing metrics
                            metrics.Add(("MessageDead.Count", 1, LambdaMetricUnit.Count));
                        } catch {
                            // record attempted processing metrics
                            metrics.Add(("MessageFailed.Count", 1, LambdaMetricUnit.Count));
                        }
                    }
                    else
                    {
                        // record attempted processing metrics
                        metrics.Add(("MessageFailed.Count", 1, LambdaMetricUnit.Count));

                        // log error as a warning as we expect to see this message again
                        LogErrorAsWarning(e);
                    }
                } finally {
                    _currentRecord = null;
                    LogMetric(metrics);
                }
            }

            // check if any failures occurred
            if ((sqsEvent.Records.Count != successfulMessages.Count) && (successfulMessages.Count > 0))
            {
                // delete all messages that were successfully processed to avoid them being tried again
                await Provider.DeleteMessagesFromQueueAsync(
                    eventSourceArn,
                    successfulMessages.Select(message =>
                                              (MessageId: message.MessageId, ReceiptHandle: message.ReceiptHandle)
                                              )
                    );

                // fail invocation to prevent messages from being deleted
                throw new LambdaAbortException($"processing failed: {sqsEvent.Records.Count - successfulMessages.Count} errors ({successfulMessages.Count} messages succeeded)");
            }
            return($"processed {successfulMessages.Count} messages".ToStream());
        }