Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-1294 Add support to FIFO #3437

Open
wants to merge 18 commits into
base: master
Choose a base branch
from

Conversation

lillo42
Copy link
Contributor

@lillo42 lillo42 commented Dec 22, 2024

Hohohoho 🎅

  • Add support to SNS/SQS FIFO
  • Improve support for LocalStack
  • Rename some classes from Sqs to Sns (breaking changes)
  • Add support to SQS publication
  • Add Sample for SQS publication & FIFO

How should I handle the tests? My current approach is to duplicate them. Should I continue on this path?
Duplicate the test

Copy link

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Health Quality Gates: FAILED

Change in average Code Health of affected files: -0.12 (9.65 -> 9.53)

  • Declining Code Health: 4 findings(s) 🚩

View detailed results in CodeScene

Copy link

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Health Quality Gates: FAILED

Change in average Code Health of affected files: -0.10 (9.46 -> 9.36)

  • Declining Code Health: 8 findings(s) 🚩

View detailed results in CodeScene

@iancooper
Copy link
Member

Hi @lillo42 - thanks for this. To lift out of inline above, the usual strategy would be:

Folders for each queue type:

  • Standard
  • Fifo

Put the existing tests in Standard and duplicate the tests in Fifo, but with the changed semantics.

I tend to prefer being explicit in tests, over worrying about duplication there.

@iancooper
Copy link
Member

PS @lillo42 if you have time. One weakness in Brighter is that you have to produce to SNS and can't target a queue. It would be good to allow the queue to be the target. We now do something similar for ASB. Probably pop that in a separate PR though?

Copy link

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Health Quality Gates: FAILED

Change in average Code Health of affected files: +0.05 (9.61 -> 9.66)

  • Declining Code Health: 6 findings(s) 🚩

View detailed results in CodeScene

Copy link
Member

@iancooper iancooper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great. You may also need to support async approaches, you'll note the increased footprint when you merge in master. It is a little crude, as we just tend to duplicate in tests, but I sort of prefer tests to be explicit

Copy link

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Health Quality Gates: FAILED

Change in average Code Health of affected files: +0.10 (9.50 -> 9.60)

  • Declining Code Health: 5 findings(s) 🚩

View detailed results in CodeScene

@lillo42
Copy link
Contributor Author

lillo42 commented Dec 30, 2024

PS @lillo42 if you have time. One weakness in Brighter is that you have to produce to SNS and can't target a queue. It would be good to allow the queue to be the target. We now do something similar for ASB. Probably pop that in a separate PR though?

Sure no problem, I can add it to this PR.

@iancooper
Copy link
Member

Thanks @lillo42. Apologies in advance for the merge issues caused by the Reactor(Blocking)/Proactor(Non-Blocking) enhancement

Copy link

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Health Quality Gates: FAILED

Change in average Code Health of affected files: +0.05 (9.60 -> 9.65)

  • Declining Code Health: 5 findings(s) 🚩

View detailed results in CodeScene

@iancooper
Copy link
Member

We still have a credentials issue on the AWS tests, that means they don't show up here. I need to fix that too. Let me see if I can fix that and merge to main, so that we can at least see what is passing

RoutingKey topic,
TopicFindBy topicFindBy,
SnsAttributes? attributes,
OnMissingChannel makeTopic = OnMissingChannel.Create,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make makeTopic the last parameter? That keeps it in sync with other producers

namespace System.Diagnostics.CodeAnalysis;

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public sealed class MemberNotNullWhenAttribute : Attribute
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of interest, we have an open conversation on using polyfills vs dropping netstandard20

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to prefer drop support for netstandard20, but depending on how many downloads we have today on .NET standard maybe we still have people using Brighter with .NET Framework 4

Maybe on V11 we can remove the support for .NET Standard completed, and put a warning on V10 saying that is the last version of supporting .NET Standard/Framework

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you may be right that it would be 0-warning if we dropped it with V10 and we should declare that will happen with V11.

Copy link

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Health Quality Gates: FAILED

Change in average Code Health of affected files: +0.13 (9.61 -> 9.74)

  • Declining Code Health: 9 findings(s) 🚩
  • Improving Code Health: 1 findings(s) ✅

View detailed results in CodeScene

Comment on lines +41 to +47
public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage)
{
var topic = HeaderResult<RoutingKey>.Empty();
var messageId = HeaderResult<string?>.Empty();

Message message;
try

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Getting worse: Complex Method
CreateMessage increases in cyclomatic complexity from 10 to 12, threshold = 9

Suppress

publishRequest.MessageAttributes = messageAttributes;

var response = await _client.PublishAsync(publishRequest);
if (response.HttpStatusCode == System.Net.HttpStatusCode.OK || response.HttpStatusCode == System.Net.HttpStatusCode.Created || response.HttpStatusCode == System.Net.HttpStatusCode.Accepted)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No longer an issue: Complex Conditional
PublishAsync no longer has a complex conditional

Copy link

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Health Quality Gates: FAILED

Change in average Code Health of affected files: +0.43 (9.42 -> 9.85)

  • Declining Code Health: 10 findings(s) 🚩
  • Improving Code Health: 5 findings(s) ✅

View detailed results in CodeScene

Comment on lines +239 to +241
private HeaderResult<RoutingKey> ReadTopic()
{
if (_messageAttributes.TryGetValue(HeaderNames.Topic, out var topicArn))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Bumpy Road Ahead
ReadTopic has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function

Suppress


public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Getting worse: Complex Method
CreateMessage increases in cyclomatic complexity from 10 to 13, threshold = 9

Suppress

Comment on lines +235 to +245
private static HeaderResult<RoutingKey> ReadTopic(Amazon.SQS.Model.Message sqsMessage)
{
if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Topic, out MessageAttributeValue? value))
{
//we have an arn, and we want the topic
var topic = value.StringValue;
if (Arn.TryParse(value.StringValue, out var arn))
{
topic = arn.Resource;
}
else

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Bumpy Road Ahead
ReadTopic has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function

Suppress

Comment on lines +158 to +192
public SqsSubscription(
Type dataType,
SubscriptionName? name = null,
ChannelName? channelName = null,
RoutingKey? routingKey = null,
int bufferSize = 1,
int noOfPerformers = 1,
TimeSpan? timeOut = null,
int requeueCount = -1,
TimeSpan? requeueDelay = null,
int unacceptableMessageLimit = 0,
MessagePumpType messagePumpType = MessagePumpType.Proactor,
IAmAChannelFactory? channelFactory = null,
int lockTimeout = 10,
int delaySeconds = 0,
int messageRetentionPeriod = 345600,
TopicFindBy findTopicBy = TopicFindBy.Name,
string? iAmPolicy = null,
RedrivePolicy? redrivePolicy = null,
SnsAttributes? snsAttributes = null,
Dictionary<string, string>? tags = null,
OnMissingChannel makeChannels = OnMissingChannel.Create,
bool rawMessageDelivery = true,
TimeSpan? emptyChannelDelay = null,
TimeSpan? channelFailureDelay = null,
SnsSqsType sqsType = SnsSqsType.Standard,
bool contentBasedDeduplication = true,
DeduplicationScope? deduplicationScope = null,
int? fifoThroughputLimit = null,
RoutingKeyType routingKeyType = RoutingKeyType.PubSub,
QueueFindBy queueFindBy = QueueFindBy.Name
)
: base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount,
requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay,
channelFailureDelay)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ Getting worse: Constructor Over-Injection
SqsSubscription increases from 24 to 30 arguments, threshold = 5

Comment on lines +253 to +290
public SqsSubscription(
SubscriptionName? name = null,
ChannelName? channelName = null,
RoutingKey? routingKey = null,
int bufferSize = 1,
int noOfPerformers = 1,
TimeSpan? timeOut = null,
int requeueCount = -1,
TimeSpan? requeueDelay = null,
int unacceptableMessageLimit = 0,
MessagePumpType messagePumpType = MessagePumpType.Proactor,
IAmAChannelFactory? channelFactory = null,
int lockTimeout = 10,
int delaySeconds = 0,
int messageRetentionPeriod = 345600,
TopicFindBy findTopicBy = TopicFindBy.Name,
string? iAmPolicy = null,
RedrivePolicy? redrivePolicy = null,
SnsAttributes? snsAttributes = null,
Dictionary<string, string>? tags = null,
OnMissingChannel makeChannels = OnMissingChannel.Create,
bool rawMessageDelivery = true,
TimeSpan? emptyChannelDelay = null,
TimeSpan? channelFailureDelay = null,
SnsSqsType sqsType = SnsSqsType.Standard,
bool contentBasedDeduplication = true,
DeduplicationScope? deduplicationScope = null,
int? fifoThroughputLimit = null,
RoutingKeyType routingKeyType = RoutingKeyType.PubSub,
QueueFindBy queueFindBy = QueueFindBy.Name
)
: base(typeof(T), name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount,
requeueDelay,
unacceptableMessageLimit, messagePumpType, channelFactory, lockTimeout, delaySeconds,
messageRetentionPeriod, findTopicBy,
iAmPolicy, redrivePolicy, snsAttributes, tags, makeChannels, rawMessageDelivery, emptyChannelDelay,
channelFailureDelay, sqsType, contentBasedDeduplication, deduplicationScope, fifoThroughputLimit,
routingKeyType)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ Getting worse: Constructor Over-Injection
SqsSubscription increases from 23 to 29 arguments, threshold = 5

Comment on lines +402 to +426
private async Task SubscribeToTopicAsync(
string topicArn,
string queueUrl,
SqsAttributes? sqsAttributes,
AmazonSQSClient sqsClient,
AmazonSimpleNotificationServiceClient snsClient)
{
var arn = await snsClient.SubscribeQueueAsync(topicArn, sqsClient, queueUrl);
if (string.IsNullOrEmpty(arn))
{
throw new InvalidOperationException(
$"Could not subscribe to topic: {topicArn} from queue: {queueUrl} in region {AwsConnection.Region}");
}

var response = await snsClient.SetSubscriptionAttributesAsync(
new SetSubscriptionAttributesRequest(arn,
"RawMessageDelivery",
sqsAttributes?.RawMessageDelivery.ToString())
);

if (response.HttpStatusCode != HttpStatusCode.OK)
{
throw new InvalidOperationException("Unable to set subscription attribute for raw message delivery");
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ New issue: Excess Number of Function Arguments
SubscribeToTopicAsync has 5 arguments, threshold = 4

Comment on lines -233 to -302
private async Task CreateQueueAsync(AmazonSQSClient sqsClient)
{
if (_subscription is null)
throw new InvalidOperationException("ChannelFactory: Subscription cannot be null");

s_logger.LogDebug("Queue does not exist, creating queue: {ChannelName} subscribed to {Topic} on {Region}", _subscription.ChannelName.Value, _subscription.RoutingKey.Value, AwsConnection.Region);
_queueUrl = null;
try
{
var attributes = new Dictionary<string, string>();
if (_subscription.RedrivePolicy != null && _dlqARN != null)
{
var policy = new { maxReceiveCount = _subscription.RedrivePolicy.MaxReceiveCount, deadLetterTargetArn = _dlqARN };
attributes.Add("RedrivePolicy", JsonSerializer.Serialize(policy, JsonSerialisationOptions.Options));
}

attributes.Add("DelaySeconds", _subscription.DelaySeconds.ToString());
attributes.Add("MessageRetentionPeriod", _subscription.MessageRetentionPeriod.ToString());
if (_subscription.IAMPolicy != null) attributes.Add("Policy", _subscription.IAMPolicy);
attributes.Add("ReceiveMessageWaitTimeSeconds", _subscription.TimeOut.Seconds.ToString());
attributes.Add("VisibilityTimeout", _subscription.LockTimeout.ToString());

var tags = new Dictionary<string, string> { { "Source", "Brighter" } };
if (_subscription.Tags != null)
{
foreach (var tag in _subscription.Tags)
{
tags.Add(tag.Key, tag.Value);
}
}

var request = new CreateQueueRequest(_subscription.ChannelName.Value)
{
Attributes = attributes,
Tags = tags
};
var response = await sqsClient.CreateQueueAsync(request);
_queueUrl = response.QueueUrl;

if (!string.IsNullOrEmpty(_queueUrl))
{
s_logger.LogDebug("Queue created: {URL}", _queueUrl);
using var snsClient = new AmazonSimpleNotificationServiceClient(AwsConnection.Credentials, AwsConnection.Region);
await CheckSubscriptionAsync(_subscription.MakeChannels, sqsClient, snsClient);
}
else
{
throw new InvalidOperationException($"Could not create queue: {_subscription.ChannelName.Value} subscribed to {ChannelTopicArn} on {AwsConnection.Region}");
}
}
catch (QueueDeletedRecentlyException ex)
{
var error = $"Could not create queue {_subscription.ChannelName.Value} because {ex.Message} waiting 60s to retry";
s_logger.LogError(ex, "Could not create queue {ChannelName} because {ErrorMessage} waiting 60s to retry", _subscription.ChannelName.Value, ex.Message);
Thread.Sleep(TimeSpan.FromSeconds(30));
throw new ChannelFailureException(error, ex);
}
catch (AmazonSQSException ex)
{
var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {AwsConnection.Region.DisplayName} because {ex.Message}";
s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", _queueUrl, _subscription.RoutingKey.Value, AwsConnection.Region.DisplayName, ex.Message);
throw new InvalidOperationException(error, ex);
}
catch (HttpErrorResponseException ex)
{
var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {AwsConnection.Region.DisplayName} because {ex.Message}";
s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", _queueUrl, _subscription.RoutingKey.Value, AwsConnection.Region.DisplayName, ex.Message);
throw new InvalidOperationException(error, ex);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No longer an issue: Complex Method
CreateQueueAsync is no longer above the threshold for cyclomatic complexity

Comment on lines -196 to -216
private async Task EnsureQueueAsync()
{
if (_subscription is null)
throw new InvalidOperationException("ChannelFactory: Subscription cannot be null");

if (_subscription.MakeChannels == OnMissingChannel.Assume)
return;

using var sqsClient = new AmazonSQSClient(AwsConnection.Credentials, AwsConnection.Region);
var queueName = _subscription.ChannelName.ToValidSQSQueueName();
var topicName = _subscription.RoutingKey.ToValidSNSTopicName();

(bool exists, _) = await QueueExistsAsync(sqsClient, queueName);
if (!exists)
{
if (_subscription.MakeChannels == OnMissingChannel.Create)
{
if (_subscription.RedrivePolicy != null)
{
await CreateDLQAsync(sqsClient);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No longer an issue: Bumpy Road Ahead
EnsureQueueAsync is no longer above the threshold for logical blocks with deeply nested code

Comment on lines -356 to -372
private async Task CheckSubscriptionAsync(OnMissingChannel makeSubscriptions, AmazonSQSClient sqsClient, AmazonSimpleNotificationServiceClient snsClient)
{
if (makeSubscriptions == OnMissingChannel.Assume)
return;

if (!await SubscriptionExistsAsync(sqsClient, snsClient))
{
if (makeSubscriptions == OnMissingChannel.Validate)
{
throw new BrokerUnreachableException($"Subscription validation error: could not find subscription for {_queueUrl}");
}
else if (makeSubscriptions == OnMissingChannel.Create)
{
await SubscribeToTopicAsync(sqsClient, snsClient);
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No longer an issue: Bumpy Road Ahead
CheckSubscriptionAsync is no longer above the threshold for logical blocks with deeply nested code

@@ -1,4 +1,5 @@
#region Licence

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No longer an issue: Overall Code Complexity
The mean cyclomatic complexity in this module is no longer above the threshold

Copy link
Member

@iancooper iancooper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot here, and it is a great addition. A couple of minor tweaks, which are more o do with style over anything else.

else if (_publication.Topic is not null)
{
routingKey = _publication.Topic;
}

if (routingKey is null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a RoutingKey.IsNullOrEmpty()


var arnElements = s!.Split(':');
var topic = arnElements[(int)ARNAmazonSNS.TopicName];
var topic = topicArn.GetValueInString() ?? string.Empty;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The earlier version reduces nesting by returning early, which is generally preferable

}
}

var messageAttributes = new Dictionary<string, MessageAttributeValue>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically we factor this into a publisher, to keep the producer clean of all the header manipulation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants