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

Add pdd protocol and multistream example as .md files for easier review and upgrade collaboratively #12

Merged
merged 6 commits into from
Jun 27, 2015

Conversation

daviddias
Copy link
Member

Continuation of the work started here:

#11 (comment)

@wking
Copy link
Contributor

wking commented Jun 11, 2015

I still think there's confusion about the transition between
socket-connection and agent assignment.

On Thu, Jun 11, 2015 at 06:21:47AM -0700, David Dias wrote 1:

@wking your questions show a very valid point, which is the fact
that since we had some side channel conversations of the protocol,
without describing its implementation on the Protocol Spec or not
point to a reference implementation, q uestions like "how we know
the role of each side", appear. A lot of that is answered here:
ipfs/js-ipfs#13 (comment)
. One of my goals with PDD is also to answer those questions as soon
as possible

Looking through the referenced comment, it sounds like the goal is
just that anyone can start talking on a new stream. For example (with
‘→’ for “sent by the connector”):

  • Connector is a speaker in a push-stream, listener is mute:

    → stream 0 /multistream/1.0.0
    → fork 0 1 /bird/3.2.1
    → stream 0 close
    → stream 1 hey, how is it going?

  • Connector is a speaker in a push-stream, listener is chatty and
    current:

    → stream 0 /multistream/1.0.0
    ← stream 0 /multistream/1.0.0
    → fork 0 1 /bird/3.2.1
    → stream 0 close
    → stream 1 hey, how is it going?
    ← stream 0 closed
    → stream 1 /bird/3.2.1

  • Connector is a speaker in a push-stream, listener is chatty and
    old:

    → stream 0 /multistream/1.0.0
    ← stream 0 /multistream/1.0.0
    → fork 0 1 /bird/3.2.1
    → stream 0 close
    → stream 1 hey, how is it going?
    ← stream 1 /bird/2.8.0
    ← stream 0 closed
    → stream 1 /bird/2.8.0
    → stream 1 message: hey, how is it going?

  • Connector is a speaker in a push-stream, listener is chatty, very
    old, and slow:

    → stream 0 /multistream/1.0.0
    → fork 0 1 /bird/3.2.1
    → stream 0 close
    → stream 1 hey, how is it going?
    ← stream 0 /multistream/0.3.2
    [dialer hangs up, or maybe resends with 0.3.2 format?]

Supporting mute ends in the protocol means that there is the sender
has to to opportunistically continue transmission assuming that the
listener is understanding the protocol choices. But for slow
listeners, it may be a while before the listener gets around to
responding to protocol negotiation requests, so the sender may need to
cache (all?) the subsequent messages if it wants to re-send them after
the protocol version has been re-negotiated.

We may also need to keep the multistream channel around (even if we
don't want to fork more child streams) to avoid namespace collisions
between child protocols and stream closures.

→ stream 0 /multistream/1.0.0
← stream 0 /multistream/1.0.0
→ fork 0 1 /bird/3.2.1
← stream 1 /bird/3.2.1
→ stream 1 hey, how is it going?
→ stream 0 close 1
← stream 0 closed 1

That way you know pairs that agree on a multistream version will be
able to cleanup dangling streams.

@wking
Copy link
Contributor

wking commented Jun 11, 2015

On Thu, Jun 11, 2015 at 09:24:55AM -0700, W. Trevor King wrote:

Looking through the referenced comment, it sounds like the goal is
just that anyone can start talking on a new stream. For example (with
‘→’ for “sent by the connector”):

Ah, I left out “conectee is a speaker in a push-stream”. For the
mute-listener case with an optimistic speaker, that looks like:

← stream 0 /multistream/1.0.0
← fork 0 1 /bird/3.2.1
← stream 1 hey, how is it going?
← stream 0 close 1
← stream 0 close 0

But and with a chatty connector that tries to re-negotiate the
protocol, that's:

← stream 0 /multistream/1.0.0
→ stream 0 /multistream/1.0.0
← fork 0 1 /bird/3.2.1
← stream 1 hey, how is it going?
→ stream 1 /bird/2.8.0
← stream 1 /bird/2.8.0
← stream 1 message: hey, how is it going?
← stream 0 close 1
← stream 0 close 0
→ stream 0 closed 1
→ stream 0 closed 0

However, with a mute connector (e.g. you have a mute client that wants
to follow bird messages) and a conservative connectee (e.g. it also
sends dog and cat messages, and doesn't want to just jump in
broadcasting them all), you're just going to get silence. In that
case, you need out-of-band communication to establish that a given
connectee is an optimistic bird speaker, although I guess the mute
connector could just timeout after a long enough period of silence.

@wking
Copy link
Contributor

wking commented Jun 11, 2015

In #11 I suggested an ABNF-like syntax 1 for defining the protocol
(vs. the trace-style user stories currently used here). Looking
around for existing approaches, Wikipedia also references ABNF and
also ASN.1 2. I'm not familiar with ASN.1 3, but after reading
over it it seems like the distinction vs. ABNF 4 is syntactic, and
not logicial or semantic. However, I'm not sure how folks handle
streams and such in ABNF. For example, RFC 7230 defines Connection
and HTTP-message rules, but doesn't tie them together into a protocol
rule (e.g. where would you expect to see an HTTP-message?) 5.
Anyhow, this field (formally defining protocl schemas and and example
exchanges) is new to me, but some searching around turned up a Prolac
thesis that is likely worth a thorough read 6. Finding similar work
written since 1997 would also be useful ;).

@wking
Copy link
Contributor

wking commented Jun 11, 2015

On Thu, Jun 11, 2015 at 10:15:37AM -0700, W. Trevor King wrote:

Anyhow, this field (formally defining protocl schemas and and example
exchanges) is new to me, but some searching around turned up a Prolac
thesis that is likely worth a thorough read [6]. Finding similar work
written since 1997 would also be useful ;).


[6]: http://pdos.csail.mit.edu/prolac/prolac-the.pdf

Likely these folks have some interesting papers 1, since they
mention work in progress under their “Protocol Compilers and
Specification Languages” section. For example, this paper 2 may be
a reasonable starting point for discovering the current state of
affairs.

> /dogs/0.1.0
< /dogs/0.1.0
> hey
```
Copy link
Member

Choose a reason for hiding this comment

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

ideally the test could try to fuzz all possible message orderings. e.g.:

# order 1
< /multistream/1.0.0
> /multistream/1.0.0
> ls
< ["/dogs/0.1.0","/cats/1.2.11"]
> /mouse/1.1.0
< na
> /dogs/0.1.0
< /dogs/0.1.0
> hey

# order 2
> /multistream/1.0.0
< /multistream/1.0.0
> ls
< ["/dogs/0.1.0","/cats/1.2.11"]
> /mouse/1.1.0
< na
> /dogs/0.1.0
< /dogs/0.1.0
> hey

# order 3
> /multistream/1.0.0
> ls
< /multistream/1.0.0
< ["/dogs/0.1.0","/cats/1.2.11"]
> /mouse/1.1.0
< na
> /dogs/0.1.0
< /dogs/0.1.0
> hey

... and so on down ...

# another (this one is weird from an "interactivity" perspective
# but --for correctness-- it should be tested to ensure things behave
# as expected).
> /multistream/1.0.0
> ls
> /mouse/1.1.0
> /dogs/0.1.0
> hey
< /multistream/1.0.0
< ["/dogs/0.1.0","/cats/1.2.11"]
< na
< /dogs/0.1.0

not sure if this is worth worrying about or not

Copy link
Contributor

Choose a reason for hiding this comment

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

On Thu, Jun 11, 2015 at 08:55:04PM -0700, Juan Batiz-Benet wrote:

ideally the test could try to fuzz all possible message orderings…

How do you know what the possible message orderings are without a
protocol spec to tell you?

@jbenet
Copy link
Member

jbenet commented Jun 12, 2015

@wking

We may also need to keep the multistream channel around (even if we
don't want to fork more child streams) to avoid namespace collisions
between child protocols and stream closures.

stream closing is a tricky problem and so i just left it out of multistream. in practice, multistream-select + a stream muxer (like SPDY/QUIC/etc) gives us proper EOF.

Basically, to speak more than one protocol, the user must use a muxing protocol, even if that is sequential:

Hypothetical sequential muxer:

/sequential-muxer
/A
<a-msg>
<a-msg>
<a-msg>
<seq-mux-stream-EOF>
/B
<a-msg>
<a-msg>
<a-msg>
<seq-mux-stream-EOF>

@jbenet
Copy link
Member

jbenet commented Jun 12, 2015

Likely these folks have some interesting papers [1], since they
mention work in progress under their “Protocol Compilers and
Specification Languages” section. For example, this paper [2] may be
a reasonable starting point for discovering the current state of
affairs.

Eddie Kohler is one of the best people in networks/systems research. Not sure if he's still interested in all this stuff, but I'm sure he'd have a great perspective. I'll reach out.

@jbenet
Copy link
Member

jbenet commented Jun 12, 2015

@wking you're basically totally right here. we should be doing this right. protocol compilation is absolutely the right way to think about this. My only worry is "how usable" are all these things. (i.e. in this specific case, how do we justify using "the right thing" when "worse is better". time is a huge factor in all these sorts of decisions).

This tradeoff is a really hard question. Looking at massively successful systems (git, linux, go, js), none of them is fully the right thing and the practices were instead "good enough" for the most part (though with relentless testing in the git + linux side).

I think @diasdavid's attempt is a pretty good start to something "good enough" for our use case here, so maybe we can use it as a very simple tool. I would be wary of making it much bigger without deeply understanding the relevant literature, understanding what's available already, and thinking from there.

@wking
Copy link
Contributor

wking commented Jun 12, 2015

On Thu, Jun 11, 2015 at 09:12:51PM -0700, Juan Batiz-Benet wrote:

I would be wary of making it much bigger without deeply
understanding the relevant literature, understanding what's
available already, and thinking from there.

I just don't understand how the current suggestion is going to work.
But I'm game to try. I agree that absorbing the relevant literature
would take a while. Best bet is to find someone you trust who's
already absorbed it, and pump them for ideas ;), but it sounds like
you're already working in that direction.

@daviddias
Copy link
Member Author

@wking I've added a more detailed description on the "## Protocol" segment, that I hope it clarifies how Stream Multiplexing is handled (which is not directly by multistream) and how multistream lives on top of the transport.

As far for being ['silent', 'broadcast', 'interactive', 'select'], these modes come in pairs and the decision of how one of the ends operates comes beforehand. e.g: I will that tcp://time.service:8888 offers a service in broadcast multistream mode that gives me current time, so I know that I should connect silently.

@wking
Copy link
Contributor

wking commented Jun 12, 2015

On Fri, Jun 12, 2015 at 06:21:13AM -0700, David Dias wrote:

@wking I've added a more detailed description on the "## Protocol"
segment, that I hope it clarifies how Stream Multiplexing is handled
(which is not directly by multistream) and how multistream lives on
top of the transport.

As far for being ['silent', 'broadcast', 'interactive', 'select'],
these modes come in pairs and the decision of how one of the ends
operates comes beforehand.

Right. My issue here is that in order to test an implementation
you'll have to be able to:

  • Inform the test suite of the implementation's characteristics
    (e.g. “it's a silent consumer of /multistream and /bird” or “it's a
    select producer of /multistream, /bird, and /dog”).
  • Inform the test suite of how to manage any lower-level protocols
    involved (e.g. “the implementation will listenting for TCP
    connections on $HOST:$PORT”).

For flexible-enough implementations, you may also have to pass that
information to the implementation as part of your fixture loading /
seed data [1](e.g. “I know you usually listen on 0.0.0.0:8000, but in
this case I want you to listen on localhost:91312”).

Thinking this over last night, I think you may be able to convey most
of the silent/broadcast/interactive distinctions by looking at the
underlying transport. For example, if you're pointed at an O_WRONLY
stream or get told to send UDP packets, you're going to have to be in
broadcast mode. If you're pointed at an O_RDONLY stream or get told
to listen for UDP packets, you're going to have to be in silent mode.
For potentially interactive channels (TCP, Unix sockets, other O_RDRW
streams), actors interested in one-way communication should close off
the direction they don't intend to use. For example, a silent
listener connecting to localhost:91312 over TCP would immediately
shutdown(sock, SHUT_WR). Then the other side would get the FIN, know
that it would never hear from the client again, and should either
switch into broadcast mode or just close the connection. Since
there's no TCP message for SHUT_RD, this approach breaks down if both
sides want to be broadcasters, so you'd still need out-of-band
communication there ;).

@daviddias
Copy link
Member Author

Right. My issue here is that in order to test an implementation
you'll have to be able to:

  • Inform the test suite of the implementation's characteristics
    (e.g. “it's a silent consumer of /multistream and /bird” or “it's a
    select producer of /multistream, /bird, and /dog”).
  • Inform the test suite of how to manage any lower-level protocols
    involved (e.g. “the implementation will listening for TCP
    connections on $HOST:$PORT”).

Indeed, but is it really a issue? Most expectations library depart from the assumption that we "prepare a scenario" and "expect" some given results. I don't necessarily see that asking a implementation developer to recreate a scenario for a given test would be problem, but maybe I'm not seeing all the implications.

For multistream case, it only expects a duplexStream interface, so there wouldn't be a need to use network even (which would make impl more resilient since they won't be able to make assumptions on the network stack). In the end, for multistream test, the question is "If I throw all of this messages at you, will you tell me what I'm expecting to hear?" kind of test.

Thinking this over last night, I think you may be able to convey most
of the silent/broadcast/interactive distinctions by looking at the
underlying transport....

This would be very smart! But then again means that multistream behaviour would be dependent of the underlying transport and with cases such as laying multistream on top of spdy or http/2, transports would have to be monkey patched to accept behave as multistream expects them too.

@wking
Copy link
Contributor

wking commented Jun 12, 2015

On Fri, Jun 12, 2015 at 09:44:25AM -0700, David Dias wrote:

Right. My issue here is that in order to test an implementation
you'll have to be able to:

  • Inform the test suite of the implementation's characteristics
    (e.g. “it's a silent consumer of /multistream and /bird” or
    “it's a select producer of /multistream, /bird, and /dog”).
  • Inform the test suite of how to manage any lower-level protocols
    involved (e.g. “the implementation will listening for TCP
    connections on $HOST:$PORT”).

Indeed, but is it really a issue? Most expectations library depart
from the assumption that we "prepare a scenario" and "expect" some
given results. I don't necessarily see that asking a implementation
developer to recreate a scenario for a given test would be problem,
but maybe I'm not seeing all the implications.

It's obviously not an insolvable problem ;), but with the current spec
I don't see a way to encode it in the test definition. Some things
(e.g. port numbers for network-based connections) can be handled
purely by the test suite, although you'd need a standard way to inform
the implementation of your choice or override the test suite's default
logic. However, I think you do need a way to encode information like
“the following transcript is the expected conversation between a
broadcast agent listening on TCP after a connection from a silent
listener”. Then the test suite has to setup that situation before it
can measure the real conversation. So I think the PPD schema needs to
include a machine-parsable representation of that information.

For multistream case, it only expects a duplexStream interface, so
there wouldn't be a need to use network even…

What's a duplexStream interface? Are there C bindings? What if my
implementation is in C? Allowing the test suite to become more
efficient by bypassing the network stack and jumping straight into the
implementation's native stream protocol is fine, but supporting the
network stack allows you to avoid writing native-stream bindings for
every implementation language.

Thinking this over last night, I think you may be able to convey
most of the silent/broadcast/interactive distinctions by looking
at the underlying transport....

This would be very smart! But then again means that multistream
behaviour would be dependent of the underlying transport and with
cases such as laying multistream on top of spdy or http/2,
transports would have to be monkey patched to accept behave as
multistream expects them too.

Is there no way to SHUT_WR (etc.) a particular virtual stream inside
HTTP/2? I'm not familiar with the HTTP/2 spec. But obviously any
test suite will have to make some assumptions about the underlying
transport (e.g. “one agent can connect to another”, “both agents can
send messages over the connection”, “all messages sent by one agent
will arrive for reading by the other agent”, “either agent can signal
that it no longer intends to read / write to the connection, and the
other agent will be able to notice that signal”, …). Some of those
assumptions are common to many streaming protocols, but not all of
them are universal. And trying to be too generic ends up being an
unproductive race to the bottom. So what constraints do we want to
apply to our transport layer, and are those constraints satisfied by
UDP, TCP, Unix sockets, filesystem files, HTTP/2, …?

@jbenet
Copy link
Member

jbenet commented Jun 12, 2015

@wking i think this discussion is a huge distraction. I think what @diasdavid has in mind works well for now.

│ message length││ ││
│└───────────────┘└───────┘
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
```
Copy link
Member

Choose a reason for hiding this comment

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

one comment: we don't need this frame for every message. the multistream idea is just to make "multistream protocols" that all begin with the recognizable header, but after that could pipe your own thing. the reason for it is that sometimes protocols already have their own framing layer so dont want to waste too much on that.

(maybe you already mean this -- wasnt getting it from the above)

Copy link
Contributor

Choose a reason for hiding this comment

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

On Fri, Jun 12, 2015 at 04:45:29PM -0700, Juan Batiz-Benet wrote:

one comment: we don't need this frame for every message. the
multistream idea is just to make "multistream protocols" that all
begin with the recognizable header…

That's enough for the test-suite's multistream-select and multistream
drivers (that get messages from the transcript and encode them
correctly for those protocols).

… but after that could pipe your own thing.

So the test suite will also need protocol drivers for any protocols
its testing. Each per-protocol driver is responsible for testing both
the syntactic and semantic correctness of the testee's implementation
of that protocol, and the interface for a per-protocol driver has to
accept messages that the test suite is reading from the transcript
(e.g. ‘hey, how is it going?’) and return messages that it receives
from the testee.

Copy link
Member Author

Choose a reason for hiding this comment

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

one comment: we don't need this frame for every message.
... (maybe you already mean this -- wasnt getting it from the above)

Yes, I hope now it's more clear, once a protocol is handshake'd, it is up to that protocol to control the stream and encode however they want.

@jbenet
Copy link
Member

jbenet commented Jun 12, 2015

@diasdavid this LGTM

└────────────────────────────────┘
```

multistream doesn't cover stream multiplexing over the same connection, however, we can achieve this by leveraging functionality offered by SPDY or HTTP/2.
Copy link
Contributor

Choose a reason for hiding this comment

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

So it should probably just be multistream (and not multistream-select in the figure above). Without a stream-multiplexing transport layer, using multistream-select will be difficult (impossible?).

Copy link
Member

Choose a reason for hiding this comment

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

Without a stream-multiplexing transport layer, using multistream-select will be difficult (impossible?).

no. it will not. easy, as the examples suggest. the stream will just be locked into that protocol until the stream terminates.

Copy link
Contributor

Choose a reason for hiding this comment

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

On Sat, Jun 13, 2015 at 08:43:29PM -0700, Juan Batiz-Benet wrote:

Without a stream-multiplexing transport layer, using
multistream-select will be difficult (impossible?).

no. it will not. easy, as the examples suggest stream will just be
locked into that protocol until the stream terminates.

Ah, I was just thinking that multistream-select assumed it would
always be running for stream maintenance. But looking over [1,2], it
looks like ‘multistream’ is just “write a protocol-describing header
line before sending any other data down the stream”. So that's fine.
Then ‘multistream-select’ adds the ‘ls’ listing and ‘na\n’. The
handoff between the two is still a bit murky to me. If the
uniplex-transport conversation is:

← /multistream/1.0.0
← /bird/3.2.1
← hey, how is it going?

Switching to a /bird/ protocol is a multistream-select situation. So
it should probalby be:

← /multistream-select/1.0.0
← /bird/3.2.1
← hey, how is it going?

And the listener handlers will be:

  1. Open with a multistream-select driver on the socket.
  2. Receive ‘/multistream-select/1.0.0’. Good, that's the language we
    speak.
  3. Receive ‘/bird/3.2.1’, that's a protocol we support. Pop ourselves
    off the socket (and into a protocol stack?), and attach the
    bird-3.2.1 driver.
  4. Replay the ‘/bird/3.2.1’ message into the bird driver?
  5. Receive ‘hey, how is it going?’ and process with the current bird
    driver.

The sticky bit is (4), which I don't think we actually need. But if
the ‘/bird/3.2.1’ is only sent on the multistream-select “stream”,
then the bird stream itself is just ‘hey, how is it going?’, which
isn't a multistream protocol. That's fine, it doesn't have to be a
multistream protocol, but if the only multistream protocol is
multistream-select, it seems a bit odd ;).

I'm also not sure how, in the uniplexed transport, a subprotocol is
supposed to open a new child protocol. For example, if the logicial
stream spawning structure was like:

← /multistream-select/1.0.0
← /bird/3.2.1
← /parrot/1.0.0

If you have a parallel stream that's still in multistream-select, you
can just ask for a new stream with the child protocol there:

← /multistream-select/1.0.0
← /bird/3.2.1
… bird stream …
← /parrot/1.0.0
… new parrot stream …

That means the agent requesting the opening needs to reach back up to
the multistream-select stream, but that's ok. Over uniplexed
transport, it seems like you'd either have to build subprotocol
spawning into your spawning protocol:

← /multistream-select/1.0.0
← /bird/3.2.1
… bird stream …
← /parrot/1.0.0 # handled by bird driver
… new parrot stream …

Or close the parent:

← /multistream-select/1.0.0
← /bird/3.2.1
… bird stream …
← exit # or whatever the bird-driver needs to shut down
← /parrot/1.0.0 # handled by multistream-select driver
… new parrot stream …

And I'm a bit nervous about both of those choices. Maybe that's just
stretching multistream-select over unplexed transport too far?

@whyrusleeping whyrusleeping mentioned this pull request Jun 15, 2015
50 tasks
@jbenet
Copy link
Member

jbenet commented Jun 27, 2015

@diasdavid i'm going to merge this

jbenet added a commit that referenced this pull request Jun 27, 2015
Add pdd protocol and multistream example as .md files for easier review and upgrade collaboratively
@jbenet jbenet merged commit 67e1a33 into ipfs:master Jun 27, 2015
@jbenet jbenet removed the backlog label Jun 27, 2015
@daviddias
Copy link
Member Author

👍 :)

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.

4 participants