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

v1.2 event emitter for connection (RTN4) #144

Merged
merged 33 commits into from
May 7, 2020

Conversation

tcard
Copy link
Contributor

@tcard tcard commented Apr 3, 2020

And associated API fixes and test helpers.

As discussed, the spec-compliant API is being appended alongside the old API. We identify the spec-compliant API by appending a V12 to names. This way, we can work without having to refactor existing, working code, and when we're finished we can make the old API private and remove the V12 prefixes in a single automated swoop.

The RTN4a test for SUSPENDED is left failing until connection suspension is implemented.

@tcard tcard requested a review from QuintinWillison April 3, 2020 10:29
@tcard tcard self-assigned this Apr 3, 2020
@tcard
Copy link
Contributor Author

tcard commented Apr 3, 2020

@QuintinWillison I'm not sure how I should be assigning as reviewers for this work.

@QuintinWillison
Copy link
Contributor

@tcard will DM you on that.

@tcard tcard requested review from lmars and paddybyers April 3, 2020 10:34
We were asserting the state to be a connection state, so it crashes with
channels.
ably/error.go Outdated
@@ -135,3 +136,55 @@ func checkValidHTTPResponse(resp *http.Response) error {
}
return err
}

// ErrorInfoV12 is an error produced by the Ably library.
type ErrorInfoV12 struct {
Copy link
Member

Choose a reason for hiding this comment

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

Can this not just be called ErrorInfo?

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 tried to explain in the PR description, but let me expand. I'm marking names that are spec-compliant by appending V12. This way, we can fix the external API in one go, once the compliant public API is implemented, by:

  1. Renaming every exported name that doesn't have a V12 suffix to be unexported.
  2. Remove all V12 suffixes.

The main reason to do this this way is to make review easier, as it results in mostly additions, with as few changes as possible. But it also may help in keeping a separation between interface and implementation, as we're focusing on getting a stable interface first, then fixing what's underneath.

Copy link
Member

Choose a reason for hiding this comment

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

The main reason to do this this way is to make review easier, as it results in mostly additions, with as few changes as possible

I actually think the review would be easier if this was just ErrorInfo like I expect from reading the spec, but if the V12 suffixes make the job of shipping the change easier then 👍

ably/error.go Outdated
see = "See " + errorHref
}
return fmt.Sprintf("[ErrorInfo :%s code=%d statusCode=%d] %s", e.Message, e.Code, e.StatusCode, see)
}
Copy link
Member

Choose a reason for hiding this comment

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

This looks identical to proto.ErrorInfo.Error, is there a reason to not just use proto.ErrorInfo? If there is a reason to have this ErrorInfo here too, could it not just embed proto.ErrorInfo?

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 don't see a reason to put ErrorInfo in a separate, internal-ish package. Per the spec, it's a public type. Ultimately, I guess one of them will go away, but I'm focusing first on getting the spec-compliant API out and then see how we can refactor things to deduplicate.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm ok, well should we at least add a TODO: remove duplication with proto.ErrorInfo comment?

I must admit though that I don't like the idea of creating more work for ourselves down the line rather than just doing the work now when we know it needs to be done (e.g. rather than copying and pasting code around, just move or share it).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done at d8fbcf8.

Copy link
Contributor Author

@tcard tcard May 6, 2020

Choose a reason for hiding this comment

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

More changes related to this at 36c5bcc, 2eccadb.

}
}

// On registers an event listener. If event is nil, all events will trigger
Copy link
Member

Choose a reason for hiding this comment

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

We should use an explicit value rather than nil to mean "all events" (e.g. ably.ConnectionEventAll) to make the usage more readable

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 decided to use nil in order to avoid deviating too much from the spec.

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 don't have a strong opinion about what consideration is more important, though.

Copy link
Contributor

Choose a reason for hiding this comment

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

For me, allowing nil to express a desire for 'no filtering' in this case feels natural, especially if that matches what's in the spec.

Having said that, and given my lack of any experience in Go programming, nil sounds complicated so I may well be carrying assumptions with me from other languages.

Copy link
Member

Choose a reason for hiding this comment

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

Ah ok, so I definitely had my Go hat on here where I always try to avoid overloading the semantics of nil to mean something other than the zero value of a pointer.

Looking at the spec, I see it declares that On should support two method signatures:

class EventEmitter<Event, Data>:
  on((Data...) ->) // RTE4
  on(Event, (Data...) ->) // RTE4

Given this spec covers so many different languages, I think it's incorrect to assume every language supports method overloading, but anyway, for Go I think we should implement two different methods rather than trying to represent overloading with nil:

func (e *EventEmitter) On(func(data)) { }
func (e *EventEmitter) OnEvent(event, func(data)) { }

If you disagree, then keep the nil 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good point. Translating overloads from the spec as different methods rather than by adopting a "lowest common denominator" kind of thing with nils sounds good to me.

Done at f6a700e.

// The RealtimeV12 libraries establish and maintain a persistent connection
// to Ably enabling extremely low latency broadcasting of messages and presence
// state.
type RealtimeV12 = RealtimeClient
Copy link
Member

Choose a reason for hiding this comment

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

Given this is being merged into the integration/1.2 branch, I wonder whether we should just actively break the API in this branch to be compatible with the 1.2 spec, and then one day we just switch, make integration/1.2 be master and then maintain a deprecated release-1.1 branch with security fixes.

This would make the code easier to maintain (no need for multiple implementations of the same things in the same codebase but with V12 appended to their names), and makes upgrading to 1.2 a one step process (i.e. just update to the 1.2 branch in go.mod and continue to use ably.NewRealtimeClient but with newer features, rather than using ably.NewRealtimeClientV12 for a period of time and then in the future needing to go back and change it back to ably.NewRealtimeClient).

WDYT?

/cc @QuintinWillison how does this work in other repositories?

Copy link
Contributor

Choose a reason for hiding this comment

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

I've not seen anything similar elsewhere so, as far as I know, this is new territory for Ably... though others have much more experience than me in how we do things here.

I'm not sure I entirely understand the problem in hand, to be honest. And, on top of that, I'm looking at the names NewRealtimeClientV12 and NewRealtimeClient and wondering why they've even got a New prefix in the first place!? Those names already make it feel like more than a warm start, if that makes sense!

Copy link
Member

Choose a reason for hiding this comment

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

I really don't think we should be annotating type or function names with the version. If we're being forced to make a breaking change because what we had was broken in a way that prevents us being compatible, then we have to have a step where we force an upgrade and require the user to make changes.

I'm looking at the names NewRealtimeClientV12 and NewRealtimeClient and wondering why they've even got a New prefix in the first place!?

That's how factory functions are conventionally named in go.

Copy link
Member

Choose a reason for hiding this comment

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

@QuintinWillison

I'm not sure I entirely understand the problem in hand

So the main issue is maintaining backwards compatibility with people who may need to upgrade the ably-go dependency for reasons other than needing the new API we are shipping.

For example, if we push a security fix, people should be able to upgrade easily to take advantage of that fix without needing to also refactor their code because we also broke the API.

The way people upgrade in Go is typically by just pulling in master with go get -u, although recently Go got native module support which makes it easier to pull in changes from a specific branch (e.g. go get -u github.com/ably/[email protected]).

In terms of us maintaining this backwards compatibility, we have two options:

  1. have a single git branch that supports both the new and the old API (which is what @tcard is doing in this PR so far) and expect users to specify in their code which version they are using (e.g. ably.NewRealtimeClientV12())

  2. have a branch per potentially incompatible version (e.g. release-1.1 and release-1.2) and expect users to specify in their dependency file (i.e. go.mod) which version of the API they are using, and then use that in their code

Personally, I think 2) is easier to maintain, and makes a clear difference between upgrading for a security fix and expecting no breaking changes (i.e. pulling in the latest commits from the release-1.1 branch) vs upgrading for a new feature and expecting to change your code accordingly (i.e. switching to the release-1.2 branch, or master).

I also think we should have some form of policy we follow across repositories for this.

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 really don't think we should be annotating type or function names with the version.

Just to clarify, this is temporary and won't be there in any public-facing release. See this comment for rationale.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

About backwards compatibility, basically what @lmars says is ultimately the plan here, except following Go modules format for version tags. And, of course, master will point to the newest release.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ultimately we decided on a breaking/replacing approach rather than appending for implementing the new API. Removed the V12 suffixes at db867cc, 7d387f5.

@QuintinWillison
Copy link
Contributor

The discussions on this PR are really valuable in terms of defining the architectural trajectory of Ably's Go client library going forward. Talking with @paddybyers earlier, we both think that it would be helpful to get as much input as possible from Go programmers - both internal and those who have been involved with using the library externally to Ably. So, with that in mind, I'm ping'ing a few people who I know could have an interest if they have the time to do so. Please feel free to ignore this call but any input would be really valuable to us. Thanks!

@audiolion @DavidHuie @BRMatt @Jmgr @cruickshankpg 😁

@tcard
Copy link
Contributor Author

tcard commented Apr 8, 2020

@lmars @QuintinWillison @paddybyers PTAL and please let me know if there's anything blocking left.

@QuintinWillison That's a good idea. But maybe it shouldn't block the PR from being merged? We can accept this as "good enough" and keep improving with new suggestions before the release.

@tcard tcard added this to the v1.2 milestone Apr 8, 2020
@tcard tcard changed the title Spec-compliant event emitter for connection (RTN4) v1.2 event emitter for connection (RTN4) Apr 8, 2020
@audiolion
Copy link
Contributor

Thanks for the ping, I think @lmars and @tcard are doing a great job discussing how to best implement the spec and aligning on a best path forward. As noted in my other comment I am happy that a callback approach was chosen. Go makes it very simple to orchestrate blocking apis, and as Dave Cheney said, leave concurrency to the caller.

tcard added 7 commits April 18, 2020 10:03
The new-API Close wasn't doing the same as the old-API Close: it wasn't
closing the underlying connection after the Ably-level CLOSE exchange,
and it wasn't handling failure.

Now these effects are moved to the connection event loop, as 1. Close
is asynchronous on its effects, per the spec, and 2. those effects must
happen every time a CLOSED or ERROR arrives, not just when calling the
public-facing Close method.

WebSocket-level errors remain unhandled and should be dealt with in a
centralized way.

In order to keep tests that rely on the old behavior, a
FullRealtimeCloser adapter has been added.
Also, proto.ErrorInfo is no longer a Go error; it just holds information
sent by the server in order to construct real errors as public 
ErrorInfos.

The logic for decorating error messages from ErrorInfos has moved to
the public ErrorInfo, as that's the one clients will be seeing.
genericError was meant as a wrapper of builtin error, but wrapping
interface types doesn't work as intended.

Anyway, it really isn't necessary.
tcard added a commit that referenced this pull request Apr 21, 2020
This goroutine was never killed, and there's really no proper place to
kill it. It should live as long as the channel instance lives, but the
channel instance is garbage-collected.

We could use runtime.SetFinalizer to tie the goroutine's lifetime to
the instance's lifetime, but it's simpler to just not have the goroutine
in the first place.

So we replace the channel-based broadcast with callback-based broadcast,
so that the listener (now just a function) is garbage-collected too.

Eventually, we can use the callback-based EventEmitter to cover this
in a cleaner way. This should be part of #144.
@tcard tcard mentioned this pull request May 6, 2020
9 tasks
@tcard tcard requested a review from lmars May 6, 2020 23:14
@tcard
Copy link
Contributor Author

tcard commented May 6, 2020

@lmars I believe I've addressed all feedback. Could you please resolve the conversations if you think they're addressed?

return c.Connection.Close()
// ConnectV12 is the same as Connection.Connect.
func (c *Realtime) ConnectV12() {
c.Connection.ConnectV12()
Copy link
Member

Choose a reason for hiding this comment

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

Is this still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I got back to this after a long while and mistakenly thought I already finished with this. Should've checked. Done now at 7da5dbe.

return c.connect(true)
}

// ConnectV12 attempts to move the connection to the CONNECTED state, if it
// can and if it isn't already.
func (c *Connection) ConnectV12() {
Copy link
Member

Choose a reason for hiding this comment

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

Is this still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done at 7da5dbe.

// OnV12 registers an event handler for connection events of a specific kind.
//
// See package-level documentation on Event Emitter for details.
func (c *Connection) OnV12(e ConnectionEventV12, handle func(ConnectionStateChangeV12)) (off func()) {
Copy link
Member

Choose a reason for hiding this comment

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

I've left a couple of comments so far about the V12 suffixes, are these just left over that need to be cleaned up or are we keeping some of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done at 2802c07, 9842526.

tcard added 4 commits May 7, 2020 16:39
Added ablytest.ConnWaiter helper to get the previous Result-based
behavior.
The old API stays for tests, for now. It will be easier to do once
channels get their EventEmitter too and we can make ably.State private.
@tcard tcard requested a review from lmars May 7, 2020 15:38
@tcard tcard merged commit 90ac3f2 into integration/1.2 May 7, 2020
@tcard tcard deleted the feature/conn-event-emitter branch May 7, 2020 18:15
@tcard tcard mentioned this pull request Sep 11, 2020
8 tasks
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.

8 participants