-
Notifications
You must be signed in to change notification settings - Fork 56
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
new cli publish command #43
Conversation
82ebec3
to
3766d83
Compare
Codecov Report
@@ Coverage Diff @@
## master #43 +/- ##
==========================================
- Coverage 95.42% 93.24% -2.19%
==========================================
Files 13 12 -1
Lines 1312 1095 -217
Branches 158 149 -9
==========================================
- Hits 1252 1021 -231
- Misses 45 65 +20
+ Partials 15 9 -6
Continue to review full report at Codecov.
|
5efcdca
to
0fb9ba8
Compare
How to rerun Travis build? |
A quick way is to amend your latest commit without changing anything - |
aa176bb
to
c0b1c4b
Compare
fedora_messaging/cli.py
Outdated
@cli.command() | ||
@click.option("--publish-exchange", help=_publish_exchange_help) | ||
@click.option("--body-key", help=_body_key_help) | ||
@click.option("--body-value", help=_body_value_help) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it would be nicer to accept the path to a file that contains JSON. If we did that, we'd need to come up with a format for a JSON-serialized message (message topic, headers, body, etc). That seems generally useful anyway for users who want to create message test fixtures.
What do you think, @abompard?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree, that'd be useful anyway.
c0b1c4b
to
d341326
Compare
I'm not sure about information provided to user in case of 'loads' failure. |
ef4ca61
to
6e585b3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great, thanks for working on this!
I left a number of comments, a few docs suggestions, but nothing major. I had a few questions/idle wonderings as well, don't feel like you've got to address all of them to get this merged, I just wrote down thoughts I had 😄
docs/fedora-messaging.rst
Outdated
|
||
``fedora-messaging consume [OPTIONS]`` | ||
|
||
Starts a consumer process with a user-provided callback function to execute | ||
when a message arrives. | ||
|
||
``fedora-messaging publish [OPTIONS]`` | ||
|
||
Loads serialized messges from user-provided file and send all of them to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Loads serialized messges from user-provided file and send all of them to | |
Loads serialized messages from a file and publishes them to the specified exchange |
docs/fedora-messaging.rst
Outdated
publish | ||
------- | ||
|
||
``--publish-exchange`` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a flag on the publish
sub-command, I think just having this flag be --exchange
is fine.
fedora_messaging/cli.py
Outdated
|
||
@cli.command() | ||
@click.option("--publish-exchange", help=_publish_exchange_help) | ||
@click.option("--file", help=_publish_file_help, required=True, type=click.File("r")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be better as a Click argument rather than an option, although I don't have a great reason for that. Do options let you use -
for stdin/stdout?
Another thing to consider is if we want to accept multiple files, or require multiple calls, one per file. I don't have a preference one way or the other and I'm not sure it's going to be a usecase people encounter, so it probably doesn't matter.
fedora_messaging/cli.py
Outdated
messages = loads(msgs_json_str) | ||
except exceptions.ValidationError as e: | ||
_log.error("Unable to validate message: %s", str(e)) | ||
sys.exit(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With each of these failure cases exiting as 1, it'll be difficult for scripts to handle specific error scenarios. If a file is invalid, no amount of retrying will help, but if a connection can't be established, waiting a bit might help. I recommend adding different exit codes for the different cases and documenting those codes.
Here I think you can just raise https://click.palletsprojects.com/en/7.x/api/#click.BadArgumentUsage or similar with the message you're logging and it'll do the right thing.
fedora_messaging/cli.py
Outdated
|
||
|
||
@cli.command() | ||
@click.option("--file", help=_record_file_help, required=True, type=click.File("w")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this might be better as an argument, especially if options don't allow for -
to mean stdin/stdout. That way, users can just provide -
to get messages printed to the console with this command.
fedora_messaging/cli.py
Outdated
signal.signal(signal.SIGINT, _signal_handler) | ||
|
||
def __call__(self, message): | ||
self.messages.append(message) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you wanted to be super fancy here, you could set up a click progress bar (https://click.palletsprojects.com/en/7.x/api/#click.progressbar) so users can see how things are progressing. That could also be a follow-up feature, I think.
fedora_messaging/cli.py
Outdated
if app_name: | ||
config.conf["client_properties"]["app"] = app_name | ||
|
||
_log.info("Recording to file %s started.", file) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, you probably want to use click.echo here (or the progress bar)
fedora_messaging/cli.py
Outdated
sys.exit(1) | ||
|
||
for msg in messages: | ||
_log.info("Sending message with topic %s", msg.topic) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than logging I think you want to use https://click.palletsprojects.com/en/7.x/api/#click.echo
fedora_messaging/cli.py
Outdated
try: | ||
api.publish(msg, publish_exchange) | ||
except exceptions.PublishReturned as e: | ||
_log.error("Unable to publish message: %s", str(e.reason)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than exiting 1 here, I think it'd be good to use a different value than the default "something exploded" error status so scripts can potentially handle error cases differently. It's not a great mapping, but maybe https://docs.python.org/3/library/errno.html#errno.EREMOTEIO?
fedora_messaging/cli.py
Outdated
sys.exit(1) | ||
except exceptions.ConnectionException as e: | ||
_log.error("Unable to connect to the queue: %s", str(e.reason)) | ||
sys.exit(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same deal here, maybe use https://docs.python.org/3/library/errno.html#errno.ECONNREFUSED?
f04c0a7
to
0c7f507
Compare
Thanks for all tips! |
0c7f507
to
3f67add
Compare
|
3c324c9
to
892ef22
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Is there any way to get invloved Recorder class in UTs?
I've not had all my coffee yet this morning so I'm not sure what you mean by this. What does UTs stand for?
- Since we are providing hardcoded class to api.consume, is it necessary to catch "ValueError"?
I left a comment inline, but I think we can just call the cli.consume function with the Recorder class so we don't need to duplicate all the error handling code.
) | ||
self.assertIn("Sending message with topic test_topic", result.output) | ||
mock_publish.assert_called_once() | ||
self.assertEqual(mock_publish.call_args_list[0][0][0].topic, "test_topic") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be clearer here to assign mock_publish.call_args_list[0][0][0]
to a variable like sent_msg
or something. You should also be able to create an expected_msg
object with the expected body, topic, etc. and just do self.assertEqual(expected_msg, sent_msg)
since the message class implements the equality check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the equality check between expected_msg and sent_msg will fail because of header["sent-at"] field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, although you could set the expected_msg._headers["sent-at"]
to be the same as the fixture message
fedora_messaging/cli.py
Outdated
click.echo("Unable to publish message: {}".format(str(e.reason))) | ||
sys.exit(errno.EREMOTEIO) | ||
except exceptions.ConnectionException as e: | ||
click.echo("Unable to connect to the queue: {}".format(str(e.reason))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd have this be "Unable to connect to the message broker: {}"
fedora_messaging/cli.py
Outdated
def record(exchange, queue_name, routing_key, app_name, limit, file): | ||
"""Record messages from an AMQP queue to provided file.""" | ||
|
||
class Recorder: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what I'd recommend here is pulling the class out of the function. It's not wrong, but I think it's a little unexpected (I usually only define classes and functions in a parent class/function scope it if is both tiny and relevant only to the internal workings of the parent). Defining this class at the module level should be fine.
After that, I think you can get rid of pretty much all the rest of the code in the record command and have it just call the "consume" cli command here. The only tricky bit is the consume call expects the callback to be a path. I think you could either hard-code the path or add a check to the consume code to see if it's already a class/function. The only downside to hard-coding I see is if the module name changes it'll break, but that will also break APIs so I don't think that's a huge concern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the question is how to pass limit and file to Record instance(instance of class outside record func). As I remember consumer api is responsible to make instance of class . Mabye classmethod?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right. Maybe cli.consume() should accept a class/callable then, since you can create an instance of the class with the limit and file and pass it on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the problem is in api.comsume func. We pass class type, and api.consume is responsible to make instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also a good point. Given those things maybe leaving it as-is is fine. Or, since testing it here is a bit troublesome, perhaps pull it out into a function called recorder_factory
or something which accepts the limit as an argument and returns the class with the limit baked in:
def recorder_factory(limit):
class Recorder:
def __init__(self):
self.limit = limit
return Recorder
Perhaps a future enhancement to api.consume would be to allow passing instances of classes. I'd say we shouldn't deal with that right now.
UT stands for unittests. Since Recorder class was inside recorder func, I have no idea how to test Recorder class(call it in UTs). |
892ef22
to
c5ba3fe
Compare
@sebwoj Did you see the latest comments? |
I totally +1 the idea of having one message per line, and if possible to have the loads/dumps top-level functions accept an object and not necessarily a list. I've needed that to setup fedora-messaging in Bodhi CI. |
Please take a look at #172. |
I'm in favor of keeping the work in a single PR. FYI, I've got a lot of work for this done in a local branch, but I was waiting to see what people thought of the design before pushing it. As for backwards compatibility, I don't have a problem breaking it and releasing 2.0. I don't think anyone was using the dumps/loads APIs yet (it's not documented in the API) so it should be a non-event for everyone involved. |
ab6ebcd
to
7fe5d1b
Compare
7fe5d1b
to
a311b4e
Compare
Okay, this has been rebased on top of the crochet work so it should be ready for review after that's merged. |
|
||
``fedora-messaging consume [OPTIONS]`` | ||
|
||
Starts a consumer process with a user-provided callback function to execute | ||
when a message arrives. | ||
|
||
``fedora-messaging publish [OPTIONS] FILE`` | ||
|
||
Loads serialized messages from a file and publishes them to the specified |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we get an example of what a serialized message
looks like?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a description down in the detailed section below. Please let me know if there's anything that isn't clear in the description.
Currently if I call publish on a file that has newlines in it (i.e. pretty formatted json) it barfs. If I get rid of the newlines it works.
|
I added some docs to the man page about the message format expectations. |
a311b4e
to
10d7ebe
Compare
------- | ||
|
||
The publish command expects the message or messages provided in ``FILE`` to be | ||
JSON objects with each message separated by a newline character. The JSON |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh wow.. the newline was required as a delimeter.. did not know that.
}, | ||
}, | ||
"required": ["topic", "headers", "id", "body", "queue"], | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is great! I think it would be even better if we had a real example below (i.e. we pass in a real world example message into the fedora-messaging publish -
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went back and look at my comment from yesterday.. we could use that example if we wanted (or tweak it however you'd like)
On Thu, Jun 27, 2019 at 07:38:17PM -0700, Dusty Mabe wrote:
> @@ -91,8 +102,95 @@ configuration file and no options on the command line.
``--exchange``
The name of the exchange to bind the queue to. Can contain ASCII letters,
- digits, hyphen, underscore, period, or colon. If one is not specified, the
- default is the ``amq.topic`` exchange.
+ digits, hyphen, underscore, period, or colon.
+
+ Setting this option is equivalent to setting the ``exchange`` setting
+ in *all* ``bindings`` entries in the configuration file.
+
+
+publish
+-------
+
+The publish command expects the message or messages provided in ``FILE`` to be
+JSON objects with each message separated by a newline character. The JSON
oh wow.. the newline was required as a delimeter.. did not know that.
Yeah, this isn't great, but the reason was that I wanted you to be able
to pipe messages in and they'd be published as they were piped.
Requiring everything on one line makes it easy to just loop over
incoming lines and parse them as JSON.
If you've got a better way to do it I'd love to hear it since I'm not
crazy about the current approach either.
|
maybe since the messages are json you can just treat one message as the start and close of curly braces |
On Fri, Jun 28, 2019 at 07:14:23AM -0700, Dusty Mabe wrote:
> If you've got a better way to do it I'd love to hear it since I'm not crazy about the current approach either.
maybe since the messages are json you can just treat one message as the start and close of curly braces `{}`. Start reading a message at `{` and finish at `}`, repeat.
Perhaps this is safe:
```
message = ''
while True:
message.append(f.readline())
try:
json.loads(message)
break
except ValueError:
continue
```
I can't think of a message where this would exit the loop before getting
the complete message, but I haven't thought about it very hard either...
I guess it would fail schema validation if that happened anyway. I'll
experiment a bit.
|
I did think of a serious flaw for that approach. A user puts in some malformed JSON, and now you never exit that loop and you don't know if the user messed up or if it's just going to be right with the next line. The one message per line approach isn't pretty, but at least it lets you know when you're doing it wrong. |
Yep, that looks good, anything else you want to add / change @jeremycline ? |
These functions now dump and load strings where each line is a JSON object. Previously it created a JSON array of JSON objects, but this approach works better for piping messages into "fedora-messaging publish" from another process that is writing messages as they arrive, one line per message. Signed-off-by: Jeremy Cline <[email protected]>
Add the ability to publish messages using the CLI from a file source or from stdin. Signed-off-by: Sebastian Wojciechowski <[email protected]> [Rebase and rework for the new dump format] Signed-off-by: Jeremy Cline <[email protected]>
Add a new CLI command to record messages from a message broker and write them to a file (or stdout). This was initially intended to be used to collect test fixtures, but can also be chained together with other commands using pipes. Messages are written as they arrive, one message per line, so it's easy to, say, run a command when a message arrives: $ fedora-messaging record - | while read -r line || -n $line; do echo "message received"; done or create a quick (and completely unreliable) bridge between brokers: $ fedora-messaging --conf=broker1.toml record - | \ fedora-messaging --conf=broker2.toml publish Signed-off-by: Sebastian Wojciechowski <[email protected]> [Rebase and refactor] Signed-off-by: Jeremy Cline <[email protected]>
17d0940
to
e125de0
Compare
I think I'm happy with it (and we can improve it as we go, obviously). I've dismissed my old review which was blocking the merge, rebased, and I'll go ahead and re-approve this myself so the bot merges it when the tests are done. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✨
Signed-off-by: Sebastian Wojciechowski [email protected]
In test_good_conf, is it the good idea to check:
mock_publish.assert_called_with(
api.Message(topic=None, body={None:None}), None
)
instead of creating anther patch ->
@mock.patch("fedora_messaging.cli.api.Message")
and as return_value set mock_message, and han check:
mock_publish.assert_called_with(
mock_mnessage, None
)