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

Refactor AMQP publishing, expose delivery options #456

Merged
merged 96 commits into from Aug 22, 2017

Conversation

mattbennett
Copy link
Member

This consolidates AMQP message publishing code into a shared module, and exposes delivery options where appropriate for the built-in DependencyProviders that publish messages (RpcProxy, EventDispatcher, Publisher).

The DependencyProviders now allow certain message and delivery options to be specified at instantiation or publish time, for example:

class Serv:
     
    # publish transient messages, don't confirm receipt, never attempt to retry
    publish = Publisher(delivery_mode=1, use_confirms=False, retry=False)

    @entrypoint
    def meth(self):

        # publish a message with the default options
        self.publish("msg", routing_key="foo")

        # do retry, but only once
        self.publish("msg", routing_key="foo", retry=True, retry_policy={'max_attempts': 1})

It's possible to provide headers and declarations in the same way:

class Serv:

    publish = Publisher(
        # always add this header to published messages
        headers={'foo': 'foo'},
        # ensure these queues and exchanges exist before publishing
        declare=[some_queue, some_exchange]
    )

    @entrypoint
    def meth(self):
        self.publish(
            "msg",
            headers:{'foo': 'FOO', 'bar': 'BAR'},  # extend or override default headers
            declare: [another_queue]  # more entities to declare before publishing
        )

The TestConfigurability tests for the DependencyProviders show which options can be specified for each (e.g. you can't specify a different routing key or exchange for an EventDispatcher).

This feature needs documenting but there's not currently anywhere sensible to describe it. I would like (soon) to add a dedicated page to the docs for each of the AMQP extensions, but this PR is big enough as it is.

simplify event dispatcher by having it reuse the publisher's
implementation
time as well as event dispatchers, and test that precedence is correct;

adds two new pytest fixtures: amqp_uri and get_message_from_queue
(which still need tests)
headers that we want to propagate (e.g. call stack)
Copy link
Member

@davidszotten davidszotten left a comment

Choose a reason for hiding this comment

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

nice work!

massive pr so hard to do a thorough review. seems like a sensible refactor and we have pretty good test coverage in general

hard to get a sense of impact on backwards compat

added a few style comments/questions

@@ -0,0 +1,4 @@
# backwards compat imports
from .utils import verify_amqp_uri # noqa: F401
Copy link
Member

Choose a reason for hiding this comment

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

how come we need these lint exemptions?

Copy link
Member Author

Choose a reason for hiding this comment

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

They're "unused imports" otherwise. I'm only pulling them into this package for a backwards compatibility import of from nameko.amqp import verify_amqp_uri

"""

delivery_mode = PERSISTENT
"""
Copy link
Member

Choose a reason for hiding this comment

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

nice way to document this stuff

self.amqp_uri = amqp_uri

# publish confirms
if use_confirms is not None:
Copy link
Member

Choose a reason for hiding this comment

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

how come you aren't just making the defaults the default parameters here? documentation? subclassing to override defaults? (are we adding too many ways to change settings?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Several of them have truthy defaults, so it's hard to compare whether a new value has been provided. This is a little more verbose but consistent for every param. I also like that the default values sit next to the docs for each option.

A slightly less verbose option could be the following:

def __init__(self, amqp_uri, **kwargs):
    self.amqp_uri = amqp_uri

    self.use_confirms = kwargs.pop('use_confirms', self.use_confirms)
    ...

Slight disadvantage here is that the sphinx docs would only show **kwargs.

Re: adding too many ways to change settings. Maybe. This does allow subclassing to override defaults as well as at instantiation (and use-time). Subclassing used to be the only way to change these things previously though, and we encourage subclassing for other reasons.

from functools import partial
from logging import getLogger
import warnings
Copy link
Member

Choose a reason for hiding this comment

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

should we consider adding a linter to enforce import orders?

Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps, although it's going to be another annoying step that frequently gets forgotten and fails the build. Out of order imports are annoying though, and so are casual rearrangements inside other commits.

Could we automatically re-write them on a commit hook? Then the order will always be consistent and any re-arrangement would be made automatically and in its own commit.

Copy link
Member

Choose a reason for hiding this comment

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

i would be -1 on auto-rewriting. status quo is probably fine, though a checking is a fairly fast lint run with e.g isort. also has the bonus of optionally autosorting. something for another day

"The signature of `Publisher` has changed. The `queue` kwarg "
"is now deprecated. You can use the `declare` kwarg "
"to provide a list of Kombu queues to be declared. "
"See CHANGES, version 2.5.2 for more details. This warning "
Copy link
Member

Choose a reason for hiding this comment

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

wrong version?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, thanks. Shows how long this PR has been in the works!

retry_policy=retry_policy, mandatory=mandatory,
**kwargs
)
propagate_headers = self.get_message_headers(worker_ctx)
Copy link
Member

Choose a reason for hiding this comment

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

how come this var is called propagate_ and not extra_? (propagate sounds like a bool to me)

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I was trying to communicate that these are headers that propagate from one call to the next. I agree it sounds like a bool.

I guess it's good enough to call them extra_headers.

nameko/rpc.py Outdated
@property
def serializer(self):
""" Name of the serializer to use when publishing response payloads.
Publisher = Publisher
Copy link
Member

Choose a reason for hiding this comment

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

i think we tend to name vars like this publisher_cls

)
)

# TODO: standalone event dispatcher should accept context event_data
Copy link
Member

Choose a reason for hiding this comment

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

todo?

Copy link
Member Author

Choose a reason for hiding this comment

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

Wasn't sure whether to add this as part of this PR. It's much easier to do now, and it would bring it in line with the standalone RPC proxy.

MockCallArgs = namedtuple("MockCallArgs", ["positional", "keyword"])


def unpack_mock_call(call):
Copy link
Member

Choose a reason for hiding this comment

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

do you prefer this to call_args, call_kwargs = call?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it's slightly easier to interpret using the named attribute rather than remembering that it unpacks to args, kwargs. If you're only interested in one or the other you don't have to do args, _ = call or whatever.

I'm not wedded to it if you'd rather not introduce a new helper though.

Really I just want Mock's call to be a namedtuple out of the box.

Copy link
Member Author

Choose a reason for hiding this comment

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

@tonybajan is that an endorsement for call being a namedtuple or for the helper?

Copy link
Contributor

Choose a reason for hiding this comment

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

for call being a namedtuple – makes so much more sense

Copy link
Member

Choose a reason for hiding this comment

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

i had quick look in the source, and Call already has a bunch of interesting behaviours, e.g. it compares successfully to both a tuple (compared against args) and a dict (matched against kwargz) but nothing obvious to check membership in kwargs like you do

happy to keep this helper if you like it

@davidszotten
Copy link
Member

also, didn't realise you were still using cagoule! still working ok? given the size of that file, it might be worth re-writing history to remove all traces of it before landing this pr

@mattbennett
Copy link
Member Author

mattbennett commented Aug 21, 2017

@davidszotten I think I've addressed all the comments here. History also rewritten to remove the cagoule file.

I've implemented the TODO on top of this but left it out of this branch. PR of the diff here for easier review: mattbennett#8.

Copy link
Member

@davidszotten davidszotten left a comment

Choose a reason for hiding this comment

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

you now have a conflict in changes but otherwise +1

@mattbennett mattbennett merged commit f09519a into nameko:master Aug 22, 2017
@mattbennett mattbennett mentioned this pull request Aug 22, 2017
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.

None yet

3 participants