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

Implement AMP version 2 #1417

Merged
merged 24 commits into from
Oct 19, 2020
Merged

Implement AMP version 2 #1417

merged 24 commits into from
Oct 19, 2020

Conversation

jothan
Copy link
Contributor

@jothan jothan commented Oct 2, 2020

Implement AMP v2 as described here: https://amp-protocol.net/conversations_v2.html

Contributor Checklist:

@jothan jothan changed the title 2529 jothan ampv2 Implement AMP version 2 Oct 2, 2020
@jothan jothan marked this pull request as ready for review October 2, 2020 15:56
Copy link
Contributor

@rodrigc rodrigc 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 the contribution

src/twisted/protocols/amp.py Outdated Show resolved Hide resolved
src/twisted/protocols/amp.py Outdated Show resolved Hide resolved
src/twisted/protocols/amp.py Outdated Show resolved Hide resolved
src/twisted/protocols/amp.py Outdated Show resolved Hide resolved
src/twisted/test/test_amp.py Outdated Show resolved Hide resolved
src/twisted/test/test_amp.py Outdated Show resolved Hide resolved
@jothan
Copy link
Contributor Author

jothan commented Oct 2, 2020

@rodrigc I have added the type annotations as you requested. May I resolve the comments you added ?

@@ -2613,6 +2634,35 @@ def connectionLost(self, reason):
self.transport = None


class AMPv2(AMP):
Copy link
Member

Choose a reason for hiding this comment

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

This class and all added methods here need docstrings.

@rodrigc
Copy link
Contributor

rodrigc commented Oct 2, 2020

@jothan At the top-level Twisted directory, you can run tox -e mypy to check things before mypy before pushing to GitHub. That will save a few cycles.

@jothan
Copy link
Contributor Author

jothan commented Oct 2, 2020

@rodrigc Thanks for the tip. However, I'm a bit stumped by the remaining errors, do you have any tips ?

@jothan
Copy link
Contributor Author

jothan commented Oct 2, 2020

Alright, I figured out which type annotations I needed, mypy is now happy.

@jothan jothan requested a review from rodrigc October 2, 2020 19:01
src/twisted/protocols/amp.py Outdated Show resolved Hide resolved
@rodrigc
Copy link
Contributor

rodrigc commented Oct 2, 2020

Thanks for being patient and submitting the annotations. This is slowly making the Twisted codebase better, and easier to understand.

@jothan
Copy link
Contributor Author

jothan commented Oct 2, 2020

@rodrigc I totally understand the need for those annotations, especially in a large code base.

My AMP implementation is in Rust with types everywhere ;P https://github.com/jothan/amp-async

@jothan jothan requested a review from rodrigc October 2, 2020 19:34
Copy link
Contributor

@rodrigc rodrigc left a comment

Choose a reason for hiding this comment

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

I think this looks OK, but I'll leave open for another reviewer to give it a once over.

Nice to see you have experience writing AMP protocol parsers in Rust. I need to learn Rust one of these days. :)

@jothan jothan requested a review from glyph October 4, 2020 00:26
@rodrigc rodrigc merged commit f6980e3 into twisted:trunk Oct 19, 2020
Copy link
Member

@glyph glyph left a comment

Choose a reason for hiding this comment

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

I'm sorry that I didn't have time to get in a code review before this landed, but my attention was brought to it and I have some post-hoc feedback.

@@ -0,0 +1 @@
Implement the AMP version 2 protocol to allow values greater than 64k.
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't have duplicate NEWS entries like this, as it produces useless duplicative noise in the changelog. The correct thing to do here is to close the second ticket as a duplicate in trac, then have there be one NEWS file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You might want to update the guidelines in this case, from the news fragment instructions: " You should replace with the ticket number which is being resolved by the change (if multiple tickets are resolved, multiple files with the same contents should be added). "

Copy link
Member

Choose a reason for hiding this comment

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

@jothan Hmm. Apparently I'm wrong about this. I thought that this was an idiom that predated our usage of towncrier, but apparently towncrier adopted this same idiom of hashing the contents of the files; I just created a sample project to test it out and it does the right thing in all the edge cases that I thought would not be handled. Sorry for the mistake! I am still probably going to be rolling this back out for the other reasons discussed below, but at least we can forget about this...

@@ -239,6 +239,7 @@ def trapZero(result):

__all__ = [
"AMP",
"AMPv2",
Copy link
Member

Choose a reason for hiding this comment

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

This should not be exposed as a separate class, but rather a protocol-version parameter.

@@ -608,7 +609,7 @@ class IncompatibleVersions(AmpError):
PROTOCOL_ERRORS = {UNHANDLED_ERROR_CODE: UnhandledCommand}


class AmpBox(dict):
class AmpBox(Dict[bytes, bytes]):
Copy link
Member

Choose a reason for hiding this comment

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

We really should have landed type annotations as a separate, prior change. They're not related to "AMPv2".

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 know, sorry for the noise, I wanted to follow trunk closely with tests passing.

@@ -657,15 +658,15 @@ def copy(self):
newBox.update(self)
return newBox

def serialize(self):
def serialize(self, version2: bool = False) -> bytes:
Copy link
Member

Choose a reason for hiding this comment

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

This "version2" parameter needs a @param in the docstring.

vio = BytesIO(v)
del v

# If the value is an exact multiple of 65535, the last
Copy link
Member

Choose a reason for hiding this comment

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

how is the value "an exact multiple" of 65535 if it is not 65535 itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Guess I should remove the word "exact" here.

# (i.e. when read() returns EOF).
chunk = vio.read(MAX_VALUE_LENGTH)
while True:
w(pack("!H", len(chunk)))
Copy link
Member

Choose a reason for hiding this comment

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

I realize that this is coming from the "spec" on amp-protocol.net, but there were never any discussions I saw about how to implement a V2. Given that this is neither backwards compatible nor has any wire-level negotiation it seems like a pretty bad feature for a version bump.

Copy link
Member

Choose a reason for hiding this comment

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

(A better way to have done this would be to normalize the meaning of repeated keys, but I digress.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, it is mostly backwards compatible as long as values are smaller than 65534 bytes. Considering it is a breaking change, it should be signaled via a compatibility version change. I would have called AMP v1.1, but AMP v2 already existed conceptually.

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 have a key which is just a bit over 64k bytes, you end up screwing up the alternation of keys and values, which means that the failure mode involves the peer hanging forever; that's a pretty bad failure mode.

There's a way to do this which is entirely backwards compatible, though; just send the same key repeatedly, or send a key with .1, .2, etc appended to it. This ends up adding 0.02% or so worth of overhead to the data (and if you have really large data, you want what's described on https://twistedmatrix.com/trac/ticket/2529 anyway, not just a way to circumvent the length-prefix limit).

You can also do this as a new parameter-type declaration, which also allows you to interrogate your peer's capabilities by checking for an UNHANDLED response if you have something you want to upgrade.

If we wanted to do something dramatically better, netstrings are the way to go; arguably AMP should have used these in the first place.

def callRemoteString(self, command, requiresAnswer=True, **kw):
def callRemoteString(
self, command: bytes, requiresAnswer: bool = True, **kw: bytes
) -> Optional[Deferred]:
Copy link
Member

Choose a reason for hiding this comment

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

Optional[Deferred] is the wrong way to specify this signature; what you want is an @overload to correlate the value of requiresAnswer to the type of the response.

assert self.transport is not None
self.transport.write(box.serialize(version2=True))

def proto_value(self, string: bytes) -> str:
Copy link
Member

Choose a reason for hiding this comment

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

These methods do not belong on an AMP subclass, if they belong on a subclass at all, but a BinaryBoxProtocol subclass.

There's a mea-culpa here too; the inheritance-for-convenience design of AMP was a mistake, and should not have created this dichotomy; but, this is also why this should not be a new class, though, and instead just a parameter to BinaryBoxProtocol that AMP can propagate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I got hit by that. My initial unpublished patch was version parameters everywhere.

@glyph
Copy link
Member

glyph commented Oct 20, 2020

@jothan in case you are not on the Twisted mailing list, I began a discussion about this here: https://twistedmatrix.com/pipermail/twisted-python/2020-October/065288.html

I'm leaning towards doing a revert based on that.

Thanks for your contribution to Twisted, sorry to burden it with more work!

@glyph
Copy link
Member

glyph commented Oct 21, 2020

@jothan Would you mind re-submitting just the type-annotation bits of this PR first, since they're not really related to the other stuff happening here, and would be useful to integrate on their own?

@jothan jothan mentioned this pull request Oct 21, 2020
6 tasks
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