public async Task SubscribeAsync(Func <PunchyMessage, Task> onMessageAsync, string groupId, CancellationToken cancellationToken, params string[] topics) { if (onMessageAsync == null) { throw new ArgumentNullException(nameof(onMessageAsync)); } if (groupId == null) { throw new ArgumentNullException(nameof(groupId)); } if (!TopicNameHelpers.IsValid(groupId)) { throw new ArgumentException( $"The groupId must match regex pattern: '{TopicNameHelpers.TopicOrGroupIdValidationRegexPattern}'", nameof(groupId)); } var messageHandler = new MessageHandler(onMessageAsync, pubSub, logger, configuration, groupId); var retryerTopics = new List <string>(); foreach (var topic in topics) { if (!TopicNameHelpers.IsValid(topic)) { throw new ArgumentException( $"The topic name must match regex pattern: '{TopicNameHelpers.TopicOrGroupIdValidationRegexPattern}'. " + $"Validation failed for '{topic}'.", nameof(topics)); } if (!TopicNameHelpers.IsBadMessageTopic(topic)) { for (int i = 0; i < configuration.LevelDelaysInSeconds.Length; i++) { retryerTopics.Add(TopicNameHelpers.BuildBadMessageTopicName(topic, i, groupId)); } } } var subscription = pubSub.Subscribe(messageHandler, groupId, topics.Union(retryerTopics).ToArray()); messageHandler.Subscription = subscription; await subscription.StartAsync(cancellationToken); }
private IMessage GetSourceMessage(IMessage message, List <Attempt> attempts) { var badMessageCurrentLevel = TopicNameHelpers.ExtractBadMessageLevel(message.Topic); while (message != null && badMessageCurrentLevel >= 0) { var badMessageContents = JsonConvert.DeserializeObject <BadMessageContents>( Encoding.UTF8.GetString(message.MessageBytes)); var attemp = new Attempt() { BadMessageContents = badMessageContents, MessageAddress = message.MessageAddress, Topic = message.Topic, Timestamp = message.Timestamp }; attempts.Insert(0, attemp); // retrying var messageTimestamp = message.Timestamp; var messageAgeMilliseconds = (int)Math.Round((DateTimeOffset.Now - messageTimestamp).TotalMilliseconds); var delayMilliseconds = configuration.LevelDelaysInSeconds[badMessageCurrentLevel] * 1000; if (messageAgeMilliseconds >= delayMilliseconds) { // the message is being replaced here with the original message message = pubSub.ReadSingle(badMessageContents.SourceMessageAddress); badMessageCurrentLevel = TopicNameHelpers.ExtractBadMessageLevel(message.Topic); } else { var reloadAt = DateTimeOffset.Now.AddMilliseconds(delayMilliseconds - messageAgeMilliseconds); logger.LogDebug("Retrying message but delay time did not ellapse yet, so ignoring it for now. Will reload PubSub at {ReloadAt}", reloadAt); Subscription?.ReloadAt(reloadAt); message = null; // retry time not ellapsed } } return(message); }
public async Task SubscribeToDeadLetterAsync(Func <PunchyMessage, Task> onMessageAsync, string groupId, CancellationToken cancellationToken, params string[] topics) { topics = topics.Select(topic => TopicNameHelpers.BuildDeadLetterTopicName(topic, groupId)).ToArray(); await SubscribeAsync(onMessageAsync, groupId, cancellationToken, topics); }
public async Task OnMessageAsync(IMessage message) { if (message == null) { throw new ArgumentNullException(nameof(message)); } var badMessageNextLevel = TopicNameHelpers.ExtractBadMessageNextLevel(message.Topic); string badMessageNextLevelTopicName; if (badMessageNextLevel == badMessageMaxLevels) { badMessageNextLevelTopicName = TopicNameHelpers.BuildDeadLetterTopicName(message.Topic, groupId); } else { badMessageNextLevelTopicName = TopicNameHelpers.BuildBadMessageTopicName(message.Topic, badMessageNextLevel, groupId); } var attempts = new List <Attempt>(); var badMessageCandidate = message; message = GetSourceMessage(badMessageCandidate, attempts); // If message is null, retry time not ellapsed so will ignore this message for now. // It will be read again latter if (message != null) { try { await onMessageAsync.Invoke(new PunchyMessage() { MessageAddress = message.MessageAddress, MessageBytes = message.MessageBytes, Timestamp = message.Timestamp, Topic = message.Topic, Attempts = attempts }); } catch (Exception exception) { if (badMessageNextLevel < badMessageMaxLevels) { logger.LogWarning(exception, "Action failed for message, moving to bad message level '{BadMessageNextLevel}'. " + "Will try again in {RetrySeconds}. Topic: {Topic}; Original message address: {@MessageAddress}", badMessageNextLevel, configuration.LevelDelaysInSeconds[badMessageNextLevel], message.Topic, message.MessageAddress); } else // is final level { logger.LogError(exception, "Action failed for message, will NOT try again. Topic: {Topic}; Original message address: {@MessageAddress}", message.Topic, message.MessageAddress); } var badMessageContents = new BadMessageContents() { Exception = exception, SourceMessageAddress = badMessageCandidate.MessageAddress }; await pubSub.PublishAsync(badMessageNextLevelTopicName, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(badMessageContents))); } await badMessageCandidate.CommitAsync(); } }