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

Lazy queue creation, don't recreate on delete #38

Merged
merged 10 commits into from
Apr 27, 2016

Conversation

jamie94bc
Copy link

As dicussed in #25, #36, #37 and on Gitter.

Need to do AzureStorageQueue too - RedisQueue already permenently deletes and doesn't have network requests in the ctor, so that should be fine.

Jamie Clarke added 3 commits April 16, 2016 00:27
…eleteQueueAsync()

This enables us to move network requests out of the constructor (where they don't belong) and to use the async methods where applicable.
@jamie94bc
Copy link
Author

I think it would be a good idea if QueueBase handled calling EnsureQueueCreatedAsync(). Couple of ways to do this:

  1. Make the abstract methods virtual and update all implementations to call base (if appropriate).
  2. Rename the abstract methods to to {MethodName}Impl, and implement the behavior in the public method... eg.
public abstract Task<string> EnqueueAsync(T data);

becomes...

public Task<string> EnqueueAsync(T data) {
    await EnsureQueueCreatedAsync().AnyContext();
    await EnqueueAsyncImpl(data).AnyContext();
}

protected abstract Task<string> EnqueueAsyncImpl(T data);

@jamie94bc
Copy link
Author

Would it be worth accounting for the scenario where a queue might be deleted by another instance of the implementation? I didn't do that for the current implementation primarily because I don't want to be making an additional network request (in the case of Azure Storage / Service Bus) for almost every API call. Consider the below, or in a scale out scenario...

var a = new AzureServiceBusQueue("myqueue");
var b = new AzureServiceBusQueue("myqueue");

a.EnqueueAsync(new Thing());
var bEntry = b.DequeueAsync();

a.DeleteQueueAsync();
b.EnqueueAsync(new Thing()); // Throws as queue is deleted
a.EnqueueAsync(new Thing()); // Works as the class knows the queue is deleted and recreates

We could handle this by adding something like:

public abstract class QueueBase<T> {
    private bool _queueCreated;

    protected abstract Task EnsureQueueCreatedImplAsync(CancellationToken cancellationToken);
    protected Task EnsureQueueCreatedAsync(CancellationToken cancellationToken = default(CancellationToken)) {
        if (!_queueCreated) {
            await EnsureQueueCreatedImplAsync(cancellationToken).AnyContext();
            _queueCreated = true;
        }
    }

    protected async Task<TResult> WrapQueueCall<TResult>(Func<Task<TResult>> action, CancellationToken cancellationToken) where TResult : Task {
        await EnsureQueueCreatedAsync(cancellationToken);
        try {
            return await action();
        }
        catch (Exception ex) when (IsQueueDeletedException(ex)) {
            _queueCreated = false;
            await EnsureQueueCreatedAsync(cancellationToken);
            return await action();
        }
    }

    protected abstract bool IsQueueDeletedException(Exception ex);

    protected abstract Task<string> EnqueueImplAsync(T data);
    public async Task<string> EnqueueAsync(T data) {
        return await WrapQueueCall(() => EnqueueImplAsync(data));
        await EnsureQueueCreatedAsync().AnyContext();
        return await .AnyContext();
    }
}

public class AzureStorageQueue<T> : QueueBase<T> {
    protected override bool IsQueueDeletedException(Exception ex) => ex is StorageException && ex.Message.Contains("404");
}

@niemyjski
Copy link
Member

I think if you delete it you should have to create it again

@jamie94bc
Copy link
Author

Not sure I agree although not a common scenario.

If that is the case then we need a ClearQueueAsync() method (#25) and we also need to wrap the exception that is thrown (like Azure Storage SDK wraps WebExceptions with StorageExceptions).

@@ -133,40 +149,50 @@ public class AzureServiceBusQueue<T> : QueueBase<T> where T : class {
if (!queueEntry.IsAbandoned && !queueEntry.IsCompleted)
await queueEntry.AbandonAsync().AnyContext();
}
}, new OnMessageOptions {
AutoComplete = false
});
Copy link
Member

Choose a reason for hiding this comment

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

What does this control? Shouldn't the auto complete setting be set from the constructor?

Copy link
Author

Choose a reason for hiding this comment

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

This will tell Service Bus not to autocomplete... Autocompletion is controlled by queueEntry.CompleteAsync() and queueEntry.AbandonAsync() instead in order to trigger the events.

@jamie94bc jamie94bc self-assigned this Apr 22, 2016
@jamie94bc
Copy link
Author

Everyone happy to merge? 😄

@niemyjski
Copy link
Member

As soon as the build passes :)

@jamie94bc
Copy link
Author

I think @ejsmith was going to look into that Redis test 😉

_queueDescription.MaxDeliveryCount = newMaxDeliveryCount;
changes = true;
if (!await _namespaceManager.QueueExistsAsync(_queueName).AnyContext()) {
queueDescription = await _namespaceManager.CreateQueueAsync(new QueueDescription(_queueName) {
Copy link
Contributor

@moswald moswald Apr 26, 2016

Choose a reason for hiding this comment

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

You should catch the exception thrown when CreateQueueAsync fails because the queue already exists.

We've seen this in our current project using Foundatio: in a multi-process environment, QueueExistsAsync can return false for multiple processes, and then they each call CreateQueueAsync but only one will succeed. The rest will fail with either Microsoft.ServiceBus.Messaging.MessagingException or Microsoft.ServiceBus.Messaging.MessagingEntityAlreadyExistsException.

The correct solution is to catch those exceptions, and then fall back on calling GetQueueAsync. If that also fails, then you can bail. :)

Copy link
Author

Choose a reason for hiding this comment

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

Hadn't noticed that, I'll get that sorted and see if i can add a test to cover this also.

Copy link
Member

@niemyjski niemyjski Apr 26, 2016

Choose a reason for hiding this comment

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

We could add a distributed lock around this or bail out.

Copy link
Member

Choose a reason for hiding this comment

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

@moswald feel free to join our public slack channel: https://slack.exceptionless.com

Copy link
Author

@jamie94bc jamie94bc Apr 26, 2016

Choose a reason for hiding this comment

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

Yeah I did think that, but that makes queue coupled to a distributed locking provider. Rather not do that!

Jamie Clarke and others added 2 commits April 27, 2016 22:44
@niemyjski niemyjski merged commit c32ef09 into master Apr 27, 2016
@niemyjski niemyjski deleted the feature/queuelazycreation branch April 27, 2016 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants