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

Proposal for restore_label_and_restart functionality #87

Closed
wants to merge 5 commits into from

Conversation

stephencookdev
Copy link

@stephencookdev stephencookdev commented Sep 5, 2019

Currently there is no API to allow restoring a label, once deleted.

This is PR adds in an API to allow restoring a label, if it's been deleted, restarting the label to the initial state machine state, and taking the metadata specified

@coveralls
Copy link

coveralls commented Sep 5, 2019

Coverage Status

Coverage decreased (-0.3%) to 99.177% when pulling d559606 on feature/restore-labels into fc6e6d8 on master.

@danpalmer
Copy link
Contributor

The approach looks bang on. I think the important details in resurrecting a label like this are:

  • Ensuring metadata is "cleaned" correctly.
  • Ensuring we can't un-delete a non-deleted label.
  • Maintaining the history correctly.

This does all of the above. I'd suggest maybe we want to check that the label is label.deleted == True for completeness but theoretically this should be in sync with current_state being None so it's more just for completeness than correctness.

We'll need to expose this in the API. The status codes/error messages might take some work to get right to communicate to callers the right details about what is happening, but the existing API functions should be a good starting point, if not already enough.

One thing I can imagine being a little surprising here is that elsewhere we always merge metadata, but this does (and should) replace the whole metadata. Maybe for the function implemented so far we could change the signature to this to enforce the kwarg usage/rename, making it clear what's happening.

def restore_label(app: App, label: LabelRef, *, replacement_metadata: Metadata) -> Metadata:
    ...

I think the other functions here return the metadata because they may have merged it and the callsite might need to know the new merged metadata value, so we might want to change the return value here to further communicate this difference, but I'm not sure I prefer that. Just something to think about.

Do you think we should communicate that we're restarting the label in the state machine? The function here could be restore_label_and_restart? What do you think?

@stephencookdev
Copy link
Author

stephencookdev commented Sep 5, 2019

The approach looks bang on

😄

I'd suggest maybe we want to check that the label is label.deleted == True for completeness

Good shout, I agree

One thing I can imagine being a little surprising here is that elsewhere we always merge metadata, but this does (and should) replace the whole metadata.

FWIW, the delete_label call actually blitzes the metadata to {}. So you could argue this function does still "merge" the metadata into that empty dict.

But I've no issue with making the param name more explicit.

so we might want to change the return value here to further communicate this difference, but I'm not sure I prefer that. Just something to think about.

Yeah I was just trying to be consistent with what the create_label function does. IMO that would be the best schema to match, for this.

Do you think we should communicate that we're restarting the label in the state machine? The function here could be restore_label_and_restart? What do you think?

I think that's a good idea, yeah. It makes it explicit that it's not going to open in the same state as before, or whatever

@stephencookdev stephencookdev changed the title [DRAFT] Proposal for restore_label functionality Proposal for restore_label_and_restart functionality Sep 6, 2019
Copy link
Contributor

@PeterJCLaw PeterJCLaw left a comment

Choose a reason for hiding this comment

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

Thanks for putting this together.
Have we considered what the relationship between creating labels, reinstating labels and getting the lists of labels is? Would it make sense to have a way to enumerate the labels which are available for reinstating? (I'm not actually sure we do, just raising it as something to consider)

@@ -157,11 +157,21 @@ def create_label(state_machine_name, label_name):
try:
initial_state_name = \
app.config.state_machines[state_machine_name].states[0].name
metadata = state_machine.create_label(app, label, initial_metadata)
if 'restore_label_and_restart' in data:
Copy link
Contributor

Choose a reason for hiding this comment

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

Have we considered moving this to a separate endpoint? Creating a label is a considerably different operation to reinstating a deleted one and has rather different failure modes.

Copy link
Author

Choose a reason for hiding this comment

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

I did consider it, but in my mind creating and restoring aren't really that different. Either way you're saying "I want this label to be a thing that I can track the state of", it's just in one case you have to also say "and I know it was previously a thing that I deleted, it's fine"

Happy to make it a POST /{label}/restore or PUT /{label} with deleted=false or something, if people disagree, though. I don't have hugely strong feelings.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I agree with Peter that making this a separate API would be better...

  • A failure means quite a different thing
  • It's a destructive operation as we delete the original metadata (where both create and delete aren't really destructive)
  • We still need the restore_label_and_restart, so we're still needing to track that the label previously existed in the client.

On the last point, I think that forcing the client to track this is probably desirable – if two run-throughs are entirely independent at the application level then the label IDs should probably be different, whereas in this case styleme needs to be kinda careful about this, so making restoration more explicit will probably be handy.

POST /{label}/restore sounds good to me.

Copy link
Author

Choose a reason for hiding this comment

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

POST /{label}/restore sounds good to me.

Cool beans - I'm split it out into that 🙂

It's a destructive operation as we delete the original metadata (where both create and delete aren't really destructive)

Just to be clear, the delete operation is destructive. It deletes the metadata. The restore operation is overwriting what is guaranteed to be an empty dict.

Copy link
Contributor

Choose a reason for hiding this comment

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

Just to be clear, the delete operation is destructive. It deletes the metadata.

Oh! I had forgotten about that!

I think it's still good that we do a full replacement on restore anyway for safety, and I think it's still worth splitting out the API, but that's good to know. Sorry for the confusion!

Copy link
Author

Choose a reason for hiding this comment

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

d559606 🙂

routemaster/server/endpoints.py Outdated Show resolved Hide resolved
@stephencookdev
Copy link
Author

Would it make sense to have a way to enumerate the labels which are available for reinstating?

I agree, that sounds like a good idea. I think it might make sense to hold off on that feature though, just purely on the basis of:

  1. we don't need it right now
  2. it's easy to add to an API, but hard to remove

@@ -157,11 +157,21 @@ def create_label(state_machine_name, label_name):
try:
initial_state_name = \
app.config.state_machines[state_machine_name].states[0].name
metadata = state_machine.create_label(app, label, initial_metadata)
if 'restore_label_and_restart' in data:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I agree with Peter that making this a separate API would be better...

  • A failure means quite a different thing
  • It's a destructive operation as we delete the original metadata (where both create and delete aren't really destructive)
  • We still need the restore_label_and_restart, so we're still needing to track that the label previously existed in the client.

On the last point, I think that forcing the client to track this is probably desirable – if two run-throughs are entirely independent at the application level then the label IDs should probably be different, whereas in this case styleme needs to be kinda careful about this, so making restoration more explicit will probably be handy.

POST /{label}/restore sounds good to me.

@@ -326,6 +326,42 @@ def test_create_label_409_for_deleted_label(client, create_label):
assert response.status_code == 409


def test_restore_label(app, client, create_label, delete_label):
create_label('foo', 'test_machine', {})
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to test thoroughly, I'd put something in the metadata here so that we're testing that metadata isn't merged in the API, but actually replaced.

Copy link
Author

Choose a reason for hiding this comment

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

label: LabelRef,
replacement_metadata: Metadata,
) -> Metadata:
"""Restores a label that was previously deleted."""
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd probably extend this with a bit about why we're not merging the metadata like in the other API functions, but rather replacing entirely.

Copy link
Author

Choose a reason for hiding this comment

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


# Record the label as having been restored and add its initial metadata
if not row.deleted:
raise AssertionError(f"Label {label} is not marked as deleted!")
Copy link
Contributor

Choose a reason for hiding this comment

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

This could maybe be LabelAlreadyExists or a new exception. Same with the one below as well.

Copy link
Author

Choose a reason for hiding this comment

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

@danpalmer
Copy link
Contributor

Agreed that we don't need the listing of deleted labels for now.

@stephencookdev
Copy link
Author

@danpalmer is there a release process for this repo? Or am I good to just merge the branch, and be done with it?

Copy link
Contributor

@PeterJCLaw PeterJCLaw left a comment

Choose a reason for hiding this comment

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

Thanks for putting this together. I think the bulk of the changes here are great.

I'm concerned that the new restore_label_and_restart function isn't going to correctly restart the label on its journey through the state machine though. If that's intentional, I think we should add either a comment or something to the docstring about why that's the case.


Regarding the release process -- there are some docs in this repo at docs/releasing.md.

def _create_and_delete(
name: str,
state_machine_name: str,
metadata: dict = {},
Copy link
Contributor

Choose a reason for hiding this comment

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

Please could we type the metadata value more correctly? dict isn't really a valid type annotation (we should prefer a generic Dict in general) and in this case I think we have a Metadata type somewhere?

@@ -167,6 +167,51 @@ def create_label(state_machine_name, label_name):
abort(409, msg)


@server.route(
'/state-machines/<state_machine_name>/labels/<label_name>/restore_and_restart', # noqa
Copy link
Contributor

Choose a reason for hiding this comment

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

Please could we pin down this noqa with a clarification of specifically which error we're silencing? Just as we would prefer not to blanket catch exceptions, we should prefer not to blanket silence errors.

In case you're not aware, the syntax is like: noqa: E123.

initial state machine location.

Returns:
- 201 Created: if the label is successfully restored and restarted
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- 201 Created: if the label is successfully restored and restarted
- 201 Created: if the label is successfully restored and restarted.

assert label.deleted is False


def test_restore_unknown_label(client):
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice tests, would be good to maybe also cover the case of an unrecognised state machine? (I think we do that explicitly for the other endpoints; if we don't then this is less important).

content_type='application/json',
)

assert response.status_code == 404
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe also assert that a label of the given name was not created? (This would help clarify the difference between this endpoint and create_label)

old_state=None,
new_state=state_machine.states[0].name,
))

Copy link
Contributor

Choose a reason for hiding this comment

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

In order to restart the label within the state machine, I think we also need to process_transitions here, just like we do in create_label. Is that not the case?

label = app.session.query(Label).one()
assert label.metadata == label_metadata
assert label.deleted is False

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe also assert the state that the label ends up in? (see also my question in state_machine/api.py about whether we should process the initial state transition of the label)

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

Successfully merging this pull request may close these issues.

None yet

4 participants