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

Enforce new connection when last activity > than (idle interval + TTL) #719

Merged
merged 11 commits into from
May 14, 2018

Conversation

funkyboy
Copy link
Contributor

Enforces a new connection when the last activity is > than (_maxIdleInterval + _connectionStateTtl)

Mimics this behavior in JS, called when a connection gets started.

cc @paddybyers

@funkyboy funkyboy requested a review from ricardopereira April 26, 2018 15:11
NSTimeInterval intervalSinceLast = [[NSDate date] timeIntervalSinceDate:_lastActivity];

// We want to enforce a new connection also when there hasn't been activity for longer than (idle interval + TTL)
if(self.connection.state_nosync == ARTRealtimeClosing || intervalSinceLast > (_maxIdleInterval + _connectionStateTtl)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I think that could easily create too many connections. I'll check that in more detail tomorrow.

Copy link
Contributor

Choose a reason for hiding this comment

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

Confirmed, tests are failing and from the tests...

[15:26:38]: ▸ ✗ Connection__connection_request_fails__should_transition_to_Failed_state_because_the_token_is_invalid_and_not_renewable, expected to equal <15>, got <50>
[15:26:38]: ▸ ✗ Connection__connection_request_fails__should_transition_to_Failed_state_because_the_token_is_invalid_and_not_renewable, Should have only one connection request fail

this is not good.

NSTimeInterval intervalSinceLast = [[NSDate date] timeIntervalSinceDate:_lastActivity];

// We want to enforce a new connection also when there hasn't been activity for longer than (idle interval + TTL)
if(self.connection.state_nosync == ARTRealtimeClosing || intervalSinceLast > (_maxIdleInterval + _connectionStateTtl)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Confirmed, tests are failing and from the tests...

[15:26:38]: ▸ ✗ Connection__connection_request_fails__should_transition_to_Failed_state_because_the_token_is_invalid_and_not_renewable, expected to equal <15>, got <50>
[15:26:38]: ▸ ✗ Connection__connection_request_fails__should_transition_to_Failed_state_because_the_token_is_invalid_and_not_renewable, Should have only one connection request fail

this is not good.

@funkyboy
Copy link
Contributor Author

Took another stab deb79a7

@paddybyers
Copy link
Member

Have a look at the tests in ably/ably-ruby#146 for the cases we need to test

@funkyboy
Copy link
Contributor Author

@paddybyers Will do

@paddybyers
Copy link
Member

ricardopereira approved these changes 32 minutes ago

I do think we should have test cases included before approving the PR.

@mattheworiordan
Copy link
Member

@funkyboy I think we should get into the habit with spec changes of writing tests that fail, then making them pass. This approach IMHO is in most cases wrong. If you look back at how we worked with @ricardopereira in the past, he wrote tests that failed but met the spec, then Toni wrote the code. I am not sure anyone can review this code in any meaningful way without tests.

@funkyboy
Copy link
Contributor Author

@mattheworiordan will do. Won't merge this without having tests, which I am working on.

@funkyboy
Copy link
Contributor Author

@mattheworiordan you can read

delay(reconnectAfterInterval) {
  client.connect()
}

as the counterpart of the following Ruby

sleep(reconnectAfterInterval)
client.connect

@mattheworiordan
Copy link
Member

you can read
delay(reconnectAfterInterval) {
client.connect()
}
as the counterpart of the following Ruby
sleep(reconnectAfterInterval)
client.connect

Ok, so then if the client is running in it's own thread, whilst the test is sleeping for X time, what's stopping it from reconnecting itself automatically?

@funkyboy
Copy link
Contributor Author

It puts to sleep the whole test context, on the main thread: https://github.com/ably/ably-ios/blob/develop/Spec/TestUtilities.swift#L498. The client also belongs to that thread.

@funkyboy
Copy link
Contributor Author

@ricardopereira added test. Ready for another look.
cc @paddybyers

@mattheworiordan
Copy link
Member

Ready for another look.

@funkyboy I don't think that is sufficient. See https://github.com/ably/ably-ruby/pull/146/files#diff-6994e351b062efe17b0b5c88ac75991f for comparison.

I test both positive & negative conditions, as well as the channel attach requirements in the specification. I don't believe this is comprehensive enough.

@funkyboy
Copy link
Contributor Author

funkyboy commented Apr 27, 2018

Oh you have added more Ruby tests since yesterday :)

  • Add test when the reconnection happens before (idle interval + TTL) has passed
  • Add test to check it still reattaches to channels after a new connection has been established. See Ruby example

@funkyboy
Copy link
Contributor Author

funkyboy commented May 2, 2018

@mattheworiordan @paddybyers @ricardopereira ready for another look

Copy link
Member

@mattheworiordan mattheworiordan left a comment

Choose a reason for hiding this comment

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

Few small comments

client.connection.once(.connected) { _ in
expect(client.connection.id).toNot(beNil())
connectionId = client.connection.id!
client.connectionStateTtl = customTtlInterval
Copy link
Member

Choose a reason for hiding this comment

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

Does the client really allow these intervals to be changed once it is instanced? Or is this a test thing that allows us to "hack" these values on a running client? Obviously if customers tried the same thing they'd get very odd and unpredictable behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For our customers connectionStateTtl is a private readonly property.

We have a bunch of methods to expose and set private variables for whitebox testing.
connectionStateTtl is one of those https://github.com/ably/ably-ios/blob/develop/Source/ARTRealtime%2BPrivate.h#L51

Copy link
Member

Choose a reason for hiding this comment

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

Ok, thanks for clarifying

@@ -2690,6 +2690,42 @@ class RealtimeClientConnection: QuickSpec {
}
}
}

it("reattaches to the same channels after a new connection has been established") {
Copy link
Member

Choose a reason for hiding this comment

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

Should we not includ the specification IDs in all tests so they are greppable?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

client.connection.once(.connecting) { _ in
client.connection.once(.connected) { _ in
expect(client.connection.id).toNot(equal(connectionId))
let newChannel = client.channels.get(channelName)
Copy link
Member

Choose a reason for hiding this comment

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

Why are you instancing a new channel? The title of this tests says "reattaches to the same channels after a new connection has been established", yet you've just called get to get a new channel. Should you not be checking that the existing channel object you have emits an additional attached event once the connection becomes connected, indicating that it's been reattached by the client? See https://github.com/ably/ably-ruby/pull/146/files#diff-6994e351b062efe17b0b5c88ac75991fR714

Copy link
Contributor Author

Choose a reason for hiding this comment

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

// We want this to be > than the sum of the two above
let reconnectAfterInterval: TimeInterval = 5.0
client = AblyTests.newRealtime(options)
client.connect()
Copy link
Contributor

Choose a reason for hiding this comment

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

@funkyboy I usually add a defer closing the current connection. The connection will remain active. Something like defer { client.close() }.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@ricardopereira ricardopereira left a comment

Choose a reason for hiding this comment

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

Looks good but I think you should close the current realtime connections.

@funkyboy
Copy link
Contributor Author

funkyboy commented May 9, 2018

@mattheworiordan If you don't have any further observations, I'd merge this.

@mattheworiordan
Copy link
Member

@funkyboy I was not issued with a request to review this again so I was not aware it was awaiting a review. This looks good to me.

Copy link
Member

@paddybyers paddybyers left a comment

Choose a reason for hiding this comment

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

LGTM, thanks

@funkyboy
Copy link
Contributor Author

funkyboy commented May 10, 2018

After talking to @paddybyers and after working on the Java version of this feature I realized that these tests are wrong, in that they should wait for the suspended state (after disconnecting) and then perform checks. Here's a new todo list:

  • Change tests to follow this sequence:
    1. connect
    2. extract data / mock values
    3. disconnect
    4. wait for the connection to go into suspended mode
    5. reconnect and wait for the connection to reconnect
    6. verify assertions
  • Change implementation accordingly

@funkyboy
Copy link
Contributor Author

funkyboy commented May 11, 2018

After another discussion about RTN15g it turns out that this (fresh connection after connectionTtl + idle timer has passed) doesn't need to go through the suspended state.

I tweaked the tests so that the reconnection happens via the state machine instead of calling connect().
That surfaced a bug (that I fixed) because the freshness check was performed only when connect() was called directly.

cc @paddybyers @mattheworiordan @ricardopereira

@funkyboy funkyboy force-pushed the add-connection-freshness-check branch from 3917a81 to 4ea8dc0 Compare May 11, 2018 16:56
@mattheworiordan
Copy link
Member

Ok, logic seems good

Copy link
Member

@paddybyers paddybyers left a comment

Choose a reason for hiding this comment

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

LGTM, thanks

@funkyboy funkyboy merged commit 67d1d94 into develop May 14, 2018
@funkyboy funkyboy deleted the add-connection-freshness-check branch May 14, 2018 16:11
Copy link
Member

@mattheworiordan mattheworiordan left a comment

Choose a reason for hiding this comment

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

I know this is merged, but I believe there are issues here

expect(client.maxIdleInterval).to(equal(customIdleInterval))
client.connection.once(.connecting) { _ in
let reconnectionInterval = Date().timeIntervalSince(disconnectedAt)
expect(reconnectionInterval).to(beGreaterThan(client.connectionStateTtl + client.maxIdleInterval))
Copy link
Member

Choose a reason for hiding this comment

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

Why is this the case? I don't see any explicit request to pause / delay the reconnecting. What has caused this delay then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mattheworiordan It was this option, which tries the reconnection after 5s the client has been in a DISCONNECTED state https://github.com/ably/ably-ios/blob/develop/Spec/RealtimeClientConnection.swift#L2657

Copy link
Member

Choose a reason for hiding this comment

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

But this is not according to the spec. The client will retry to reconnect immediately after disconnect, see https://docs.ably.io/client-lib-development-guide/features/#RTN15a, not wait disconnectedRetryTimeout. It only waits disconnectedRetryTimeout on subsequent requests.

}
}
}
client.onDisconnected()
Copy link
Member

Choose a reason for hiding this comment

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

Why is this done before the attach is completed? The sequence of events for this test is wrong i.e. the initial attach may only happen once the connection becomes connected again, in fact that is very likely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mattheworiordan I don't understand. Would you prefer to see this within the attach block and right after this assertion? https://github.com/ably/ably-ios/blob/develop/Spec/RealtimeClientConnection.swift#L2708

Copy link
Member

@mattheworiordan mattheworiordan May 15, 2018

Choose a reason for hiding this comment

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

Yes, exactly, otherwise how do we know that the channel attached first and thus gets reattached after reconnected?

client.connection.once(.disconnected) { _ in
client.connection.once(.connecting) { _ in
client.connection.once(.connected) { _ in
expect(client.connection.id).toNot(equal(connectionId))
Copy link
Member

Choose a reason for hiding this comment

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

Why is the connection ID changing? There is no pause as far as I can see.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

As above, there should be no pause according to https://docs.ably.io/client-lib-development-guide/features/#RTN15a

client.connection.once(.connecting) { _ in
client.connection.once(.connected) { _ in
expect(client.connection.id).toNot(equal(connectionId))
channel.once(.attached) { _ in
Copy link
Member

Choose a reason for hiding this comment

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

You are not checking that the attached ChannelStateChange has a flag indicating loss of continuity on the channel.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mattheworiordan you mean I should check if resumed is true?

Copy link
Member

Choose a reason for hiding this comment

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

options.disconnectedRetryTimeout = 5.0
var client: ARTRealtime!
var connectionId = ""
let customTtlInterval: TimeInterval = 20.0
Copy link
Member

Choose a reason for hiding this comment

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

Why even set this, surely a standard connection should reconnect automatically without any custom settings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mattheworiordan good spot. This is not needed.

@funkyboy funkyboy mentioned this pull request May 15, 2018
4 tasks
@funkyboy
Copy link
Contributor Author

@mattheworiordan Made an issue with your feedback on this PR after merge.

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.

4 participants