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

new cli publish command #43

Merged
merged 3 commits into from
Aug 2, 2019

Conversation

sebwoj
Copy link
Contributor

@sebwoj sebwoj commented Aug 23, 2018

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
)

@sebwoj sebwoj requested a review from a team as a code owner August 23, 2018 09:58
@sebwoj sebwoj force-pushed the new_cli_publish_command branch from 82ebec3 to 3766d83 Compare August 23, 2018 10:19
@codecov-io
Copy link

codecov-io commented Aug 23, 2018

Codecov Report

Merging #43 into master will decrease coverage by 2.18%.
The diff coverage is 42.85%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
fedora_messaging/cli.py 67.1% <42.85%> (-25.66%) ⬇️
fedora_messaging/twisted/protocol.py 92.55% <0%> (-0.66%) ⬇️
fedora_messaging/api.py 100% <0%> (ø) ⬆️
fedora_messaging/config.py 100% <0%> (ø) ⬆️
fedora_messaging/testing.py 100% <0%> (ø) ⬆️
fedora_messaging/schema_utils.py
fedora_messaging/twisted/consumer.py
fedora_messaging/_session.py 96.47% <0%> (ø)
fedora_messaging/twisted/service.py 100% <0%> (+2.4%) ⬆️
... and 3 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ef04f74...e125de0. Read the comment docs.

@sebwoj sebwoj force-pushed the new_cli_publish_command branch 7 times, most recently from 5efcdca to 0fb9ba8 Compare August 28, 2018 18:31
@sebwoj
Copy link
Contributor Author

sebwoj commented Aug 28, 2018

How to rerun Travis build?

@jeremycline
Copy link
Member

A quick way is to amend your latest commit without changing anything - git commit --amend --no-edit - and force pushing to your branch: git push -f

@sebwoj sebwoj force-pushed the new_cli_publish_command branch 2 times, most recently from aa176bb to c0b1c4b Compare September 1, 2018 10:28
@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)
Copy link
Member

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?

Copy link
Member

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.

@sebwoj sebwoj force-pushed the new_cli_publish_command branch from c0b1c4b to d341326 Compare November 21, 2018 22:37
@sebwoj
Copy link
Contributor Author

sebwoj commented Nov 21, 2018

I'm not sure about information provided to user in case of 'loads' failure.
@jeremycline could you take a look on them?
Command 'record' is on the way.

@sebwoj sebwoj force-pushed the new_cli_publish_command branch 2 times, most recently from ef4ca61 to 6e585b3 Compare November 24, 2018 11:06
Copy link
Member

@jeremycline jeremycline 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, 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 😄


``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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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

publish
-------

``--publish-exchange``
Copy link
Member

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.


@cli.command()
@click.option("--publish-exchange", help=_publish_exchange_help)
@click.option("--file", help=_publish_file_help, required=True, type=click.File("r"))
Copy link
Member

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.

messages = loads(msgs_json_str)
except exceptions.ValidationError as e:
_log.error("Unable to validate message: %s", str(e))
sys.exit(1)
Copy link
Member

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.



@cli.command()
@click.option("--file", help=_record_file_help, required=True, type=click.File("w"))
Copy link
Member

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.

signal.signal(signal.SIGINT, _signal_handler)

def __call__(self, message):
self.messages.append(message)
Copy link
Member

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.

if app_name:
config.conf["client_properties"]["app"] = app_name

_log.info("Recording to file %s started.", file)
Copy link
Member

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)

sys.exit(1)

for msg in messages:
_log.info("Sending message with topic %s", msg.topic)
Copy link
Member

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

try:
api.publish(msg, publish_exchange)
except exceptions.PublishReturned as e:
_log.error("Unable to publish message: %s", str(e.reason))
Copy link
Member

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?

sys.exit(1)
except exceptions.ConnectionException as e:
_log.error("Unable to connect to the queue: %s", str(e.reason))
sys.exit(1)
Copy link
Member

Choose a reason for hiding this comment

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

@sebwoj sebwoj force-pushed the new_cli_publish_command branch 2 times, most recently from f04c0a7 to 0c7f507 Compare November 28, 2018 23:32
@sebwoj
Copy link
Contributor Author

sebwoj commented Nov 28, 2018

Thanks for all tips!
Check out my new version of cli.

@sebwoj sebwoj force-pushed the new_cli_publish_command branch from 0c7f507 to 3f67add Compare November 29, 2018 21:31
@sebwoj
Copy link
Contributor Author

sebwoj commented Nov 29, 2018

  1. Is there any way to get invloved Recorder class in UTs?
  2. Since we are providing hardcoded class to api.consume, is it necessary to catch "ValueError"?

@sebwoj sebwoj force-pushed the new_cli_publish_command branch 2 times, most recently from 3c324c9 to 892ef22 Compare November 29, 2018 22:05
Copy link
Member

@jeremycline jeremycline left a comment

Choose a reason for hiding this comment

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

  1. 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?

  1. 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")
Copy link
Member

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.

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 think that the equality check between expected_msg and sent_msg will fail because of header["sent-at"] field.

Copy link
Member

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

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)))
Copy link
Member

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: {}"

def record(exchange, queue_name, routing_key, app_name, limit, file):
"""Record messages from an AMQP queue to provided file."""

class Recorder:
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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.

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 think that the problem is in api.comsume func. We pass class type, and api.consume is responsible to make instance.

Copy link
Member

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.

@sebwoj
Copy link
Contributor Author

sebwoj commented Dec 5, 2018

UT stands for unittests. Since Recorder class was inside recorder func, I have no idea how to test Recorder class(call it in UTs).

@sebwoj sebwoj force-pushed the new_cli_publish_command branch from 892ef22 to c5ba3fe Compare December 14, 2018 15:09
@pypingou
Copy link
Member

pypingou commented May 9, 2019

@sebwoj Did you see the latest comments?

@abompard
Copy link
Member

abompard commented May 9, 2019

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.

@sebwoj
Copy link
Contributor Author

sebwoj commented May 12, 2019

Please take a look at #172.

@jeremycline
Copy link
Member

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.

@sebwoj sebwoj force-pushed the new_cli_publish_command branch from ab6ebcd to 7fe5d1b Compare May 15, 2019 07:47
@jeremycline jeremycline force-pushed the new_cli_publish_command branch from 7fe5d1b to a311b4e Compare June 26, 2019 19:01
@jeremycline
Copy link
Member

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

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?

Copy link
Member

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.

@dustymabe
Copy link

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.

[vagrant@vanilla-f30 fedora-coreos-koji-tagger]$ cat test
{
    "body": {
        "encouragement": "You're doing great!"
    },
    "headers": {
        "fedora_messaging_schema": "base.message",
        "fedora_messaging_severity": 20,
        "sent-at": "null"
    },
    "id": "null",
    "queue": "new",
    "topic": "nice.message"
}
[vagrant@vanilla-f30 fedora-coreos-koji-tagger]$ cat test | fedora-messaging publish -
Usage: fedora-messaging publish [OPTIONS] FILE

Error: Unable to validate message: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
[vagrant@vanilla-f30 fedora-coreos-koji-tagger]$ 
[vagrant@vanilla-f30 fedora-coreos-koji-tagger]$ cat test | tr '\n' ' ' | fedora-messaging publish - 
Publishing message with topic nice.message

@jeremycline
Copy link
Member

I added some docs to the man page about the message format expectations.

@jeremycline jeremycline added the backwards-incompatible Indicates an issue or pull request is backwards-incompatible label Jun 27, 2019
@jeremycline jeremycline force-pushed the new_cli_publish_command branch from a311b4e to 10d7ebe Compare June 27, 2019 21:11
-------

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

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"],
}

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 -

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)

@jeremycline
Copy link
Member

jeremycline commented Jun 28, 2019 via email

@dustymabe
Copy link

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.

@jeremycline
Copy link
Member

jeremycline commented Jun 28, 2019 via email

@jeremycline
Copy link
Member

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.

@abompard
Copy link
Member

abompard commented Aug 2, 2019

Yep, that looks good, anything else you want to add / change @jeremycline ?

abompard
abompard previously approved these changes Aug 2, 2019
jeremycline and others added 3 commits August 2, 2019 12:34
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]>
@jeremycline
Copy link
Member

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.

Copy link
Member

@jeremycline jeremycline left a comment

Choose a reason for hiding this comment

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

@mergify mergify bot merged commit a8c9952 into fedora-infra:master Aug 2, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backwards-incompatible Indicates an issue or pull request is backwards-incompatible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants