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 connection freshness check #390

Merged
merged 26 commits into from
May 18, 2018
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
664c5e6
Add connection freshness check
funkyboy May 9, 2018
41b28a1
Test connectionId after ttl + idle interval has passed
funkyboy May 9, 2018
2b15846
Fix reconnection test after ttl
funkyboy May 9, 2018
dbeb79a
Test reconnection before ttl + idleInterval has passed
funkyboy May 9, 2018
aa24aaa
Fix detection of connection freshness
funkyboy May 9, 2018
ec4cb43
Improve connection state tests
funkyboy May 10, 2018
b16bdec
Add test for RTN15g3
funkyboy May 10, 2018
c75d55e
Fix implementation fo connection freshness
funkyboy May 10, 2018
cc38a40
Refactor stale connection check
funkyboy May 14, 2018
9efc78a
Fix test when reconnecting and connection is stale
funkyboy May 14, 2018
6a2c3e4
Fix indentation
funkyboy May 15, 2018
0a1c622
Add log message when clearing stale connection
funkyboy May 15, 2018
6892ecc
Improve after TTL test
funkyboy May 15, 2018
0a14d26
Explicitly wait for channel to attach in reconnection test
funkyboy May 15, 2018
77f3cf8
Explicitly wait for close in reconnection test
funkyboy May 15, 2018
b1b9085
Set lastActivity from SocketTransport
funkyboy May 15, 2018
3a868ee
Improve reattach test
funkyboy May 15, 2018
09d76b2
Fix suspendAll() (used for RTN15c3 and RTN15g3) to ensure previously …
paddybyers May 16, 2018
79d490b
Add test for RTN15c3
paddybyers May 16, 2018
f94e877
Merge branch 'add-rtn15c-test' into add-connection-freshness-check
funkyboy May 16, 2018
6ce56a8
Ongoing tests
funkyboy May 16, 2018
056edff
Implement RTN15g3: when suppressing resume due to a stale connection,…
paddybyers May 17, 2018
6ab31f7
Clean up log statements and clarify comments
funkyboy May 17, 2018
0521e14
Add suspended channels to RTN15g3 test
funkyboy May 17, 2018
3e41787
To kick a Travis build
funkyboy May 17, 2018
b681cd8
Use disconnectAndSuppressRetries to pause connectionManager in RTN15g…
funkyboy May 18, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,3 @@ notifications:
- [email protected]
on_success: change
on_failure: always

branches:
only:
- master
- develop
4 changes: 2 additions & 2 deletions lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@ public void onChannelMessage(ITransport transport, ProtocolMessage msg) {
channel.onChannelMessage(msg);
}

public void suspendAll(ErrorInfo error) {
public void suspendAll(ErrorInfo error, boolean notifyStateChange) {
for(Iterator<Map.Entry<String, Channel>> it = entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, Channel> entry = it.next();
entry.getValue().setSuspended(error);
entry.getValue().setSuspended(error, notifyStateChange);
}
}
}
Expand Down
28 changes: 18 additions & 10 deletions lib/src/main/java/io/ably/lib/realtime/Channel.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ public class Channel extends EventEmitter<ChannelEvent, ChannelStateListener> {
*
*/
private void setState(ChannelState newState, ErrorInfo reason) {
setState(newState, reason, false);
setState(newState, reason, false, true);
}
private void setState(ChannelState newState, ErrorInfo reason, boolean resumed) {
setState(newState, reason, resumed, true);
}
private void setState(ChannelState newState, ErrorInfo reason, boolean resumed, boolean notifyStateChange) {
Log.v(TAG, "setState(): channel = " + name + "; setting " + newState);
ChannelStateListener.ChannelStateChange stateChange;
synchronized(this) {
Expand All @@ -72,8 +75,10 @@ private void setState(ChannelState newState, ErrorInfo reason, boolean resumed)
this.reason = stateChange.reason;
}

/* broadcast state change */
emit(newState, stateChange);
if(notifyStateChange) {
/* broadcast state change */
emit(newState, stateChange);
}
}

/************************************
Expand Down Expand Up @@ -115,13 +120,12 @@ private void attachImpl(final CompletionListener listener) throws AblyException
/* check preconditions */
switch(state) {
case attaching:
if (listener != null) {
if(listener != null) {
on(new ChannelStateCompletionListener(listener, ChannelState.attached, ChannelState.failed));
}

return;
case attached:
if (listener != null) {
if(listener != null) {
listener.onSuccess();
}
return;
Expand Down Expand Up @@ -321,7 +325,7 @@ public void run() {
return;
attachTimer = null;
if(state == ChannelState.attaching) {
setSuspended(new ErrorInfo(errorMessage, 91200));
setSuspended(new ErrorInfo(errorMessage, 91200), true);
reattachAfterTimeout();
}
}
Expand Down Expand Up @@ -450,14 +454,18 @@ public void setConnectionClosed(ErrorInfo reason) {

/** (RTL3c) If the connection state enters the SUSPENDED state, then an
* ATTACHING or ATTACHED channel state will transition to SUSPENDED.
* (RTN15c3) The client library should initiate an attach for channels
* that are in the SUSPENDED state. For all channels in the ATTACHING
* or ATTACHED state, the client library should fail any previously queued
* messages for that channel and initiate a new attach.
* This also gets called when a connection enters CONNECTED but with a
* non-fatal error for a failed reconnect (RTN16e). */
public synchronized void setSuspended(ErrorInfo reason) {
public synchronized void setSuspended(ErrorInfo reason, boolean notifyStateChange) {
clearAttachTimers();
if (state == ChannelState.attached || state == ChannelState.attaching) {
Log.v(TAG, "setSuspended(); channel = " + name);
setState(ChannelState.suspended, reason);
failQueuedMessages(reason);
setState(ChannelState.suspended, reason, false, notifyStateChange);
failQueuedMessages(reason);
presence.setSuspended(reason);
}
}
Expand Down
80 changes: 68 additions & 12 deletions lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ private void setState(StateIndication newState) {
/* (RTL3c) If the connection state enters the SUSPENDED
* state, then an ATTACHING or ATTACHED channel state
* will transition to SUSPENDED. */
channel.setSuspended(state.defaultErrorInfo);
channel.setSuspended(state.defaultErrorInfo, true);
break;
}
}
Expand Down Expand Up @@ -508,7 +508,7 @@ private void onChannelMessage(ProtocolMessage message) {
if (connection.key != null)
connection.recoveryKey = connection.key + ":" + message.connectionSerial;
}
ably.channels.onChannelMessage(transport, message);
ably.channels.onChannelMessage(transport, message);
}

private synchronized void onConnected(ProtocolMessage message) {
Expand All @@ -519,17 +519,30 @@ private synchronized void onConnected(ProtocolMessage message) {
* - otherwise (the realtime host has been overridden or has fallen
* back), set http to the same as realtime.
*/
if (pendingConnect.host == options.realtimeHost)
if (pendingConnect.host == options.realtimeHost) {
ably.httpCore.setHost(options.restHost);
else
} else {
ably.httpCore.setHost(pendingConnect.host);
}

/* if there was a (non-fatal) connection error
* that invalidates an existing connection id, then
* remove all channels attached to the previous id */
/* if the returned connection id differs from
* the existing connection id, then this means
* we need to suspend all existing attachments to
* the old connection.
* If realtime did not reply with an error, it
* signifies that this was a result of an earlier
* connection being invalidated due to being stale.
*
* Suspend all channels attached to the previous id;
* this will be reattached in setConnection() */
ErrorInfo error = message.error;
if(error != null && !message.connectionId.equals(connection.id))
ably.channels.suspendAll(error);
if(connection.id != null && !message.connectionId.equals(connection.id)) {
/* we need to suspend the original connection */
if(error == null) {
error = REASON_SUSPENDED;
}
ably.channels.suspendAll(error, false);
}

/* set the new connection id */
ConnectionDetails connectionDetails = message.connectionDetails;
Expand Down Expand Up @@ -669,12 +682,37 @@ private void handleStateRequest() {
requestedState = null;
}

private boolean checkConnectionStale() {
if(lastActivity == 0) {
return false;
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 not true, as if the last activity is not recorded, I assume the connection is stale / not initialised? In fact, what does the boolean result even represent. This method is called checkConnectionStale, yet returns a boolean value. So does that mean connection is stale if true, if so, should this not be isConnectionStale. And if it is that, it should not have side effects. This method IMHO is doing two things and it should not. Is it an accessor or a modifier?

}
long now = System.currentTimeMillis();
long intervalSinceLastActivity = now - lastActivity;
if(intervalSinceLastActivity > (maxIdleInterval + connectionStateTtl)) {
/* RTN15g1, RTN15g2 Force a new connection if the previous one is stale;
* Clearing connection.key will ensure that we don't attempt to resume;
* leaving the original connection.id will mean that we notice at
* connection time that the connectionId has changed */
if(connection.key != null) {
Log.v(TAG, "Clearing stale connection key to suppress resume");
connection.key = null;
connection.recoveryKey = null;
}
return true;
}
return false;
}

private void handleStateChange(StateIndication stateChange) {
/* if we have had a disconnected state indication
* from the transport then we have to decide whether
* to transition to closed, disconnected to suspended depending
* on when we last had a successful connection */
if(stateChange.state == ConnectionState.disconnected) {
if(checkConnectionStale()) {
Copy link
Member

Choose a reason for hiding this comment

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

Appears to be typos in the comments above.

Copy link
Contributor Author

@funkyboy funkyboy May 15, 2018

Choose a reason for hiding this comment

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

@mattheworiordan @paddybyers This is quite a rare case. Might happen when the app is open, the connection is active but nothing is being sent for quite a while. I don't see it harmful but I agree with Matt that it's beyond the scope of this spec change.

Copy link
Member

Choose a reason for hiding this comment

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

Well I misunderstood that stateChange was a connection state change, if it's a transport state change I am in fact OK with this. Sorry for the noise

requestState(ConnectionState.suspended);
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 add a Log.v(TAG) statement here for debugging purposes?

Copy link
Member

Choose a reason for hiding this comment

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

It would be useful in future to know why a connection moved suddenly to suspended

Copy link
Member

Choose a reason for hiding this comment

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

The more I think about this change the less I like it. The spec changes for stale connection state are at https://github.com/ably/docs/pull/331/files and make no mention of this change, so is it really sensible now to change this? As far as I can tell from the spec at https://docs.ably.io/client-lib-development-guide/features/#RTN14e, we state a connection moves to the SUSPENDED state only following a connection failure attempt, yet now we are moving to the suspended state as a result of DISCONNECTED. Additionally, what side effects may there be now that are unexpected? For example, previously when a connection first became disconnected it would immediately retry to connect, so if a device went to sleep, as soon as it wakes up, it will reconnect following it moving to the DISCONNECTED state. Now, we can't be sure that that happens because moving to the SUSPENDED state will result in the connection only being tried every 30 seconds. So have we now with this change potentially made our clients 30s less responsive by default when a device comes out of sleep? I would really like to follow why we are explicitly making this change here in this PR when as far as I can tell, this change has nothing to do with the spec change.

Copy link
Member

Choose a reason for hiding this comment

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

See https://www.ably.io/documentation/realtime/connection#connection-state-explained btw. In the examples we provide in the sequence of events, we never show a state change from disconnected -> suspended either.

Copy link
Member

Choose a reason for hiding this comment

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

@funkyboy FYI @paddybyers explained to me that this on state change handler is for the transport, not the connection, so my comments above are incorrect. Sorry, I misunderstood

return;
}
switch(state.state) {
case connecting:
stateChange = checkSuspend(stateChange);
Expand All @@ -692,9 +730,11 @@ private void handleStateChange(StateIndication stateChange) {
stateChange = null;
break;
case connected:
/* we were connected, so retry immediately */
setSuspendTime();
requestState(ConnectionState.connecting);
/* we were connected, so retry immediately */
if(!suppressRetry) {
requestState(ConnectionState.connecting);
}
break;
case suspended:
/* Don't allow a second disconnected to make the state come out of suspended. */
Expand Down Expand Up @@ -800,7 +840,7 @@ public void run() {
}

/* if our state wants us to retry on timer expiry, do that */
if(state.retry) {
if(state.retry && !suppressRetry) {
requestState(ConnectionState.connecting);
continue;
}
Expand Down Expand Up @@ -903,6 +943,7 @@ private boolean connectImpl(StateIndication request) {
String host = request.fallback;
if (host == null)
host = hosts.getHost();
checkConnectionStale();
pendingConnect = new ConnectParams(options);
pendingConnect.host = host;
lastUsedHost = host;
Expand Down Expand Up @@ -970,6 +1011,10 @@ protected boolean checkConnectivity() {
}
}

protected void setLastActivity(long lastActivityTime) {
this.lastActivity = lastActivityTime;
}

/******************
* event queueing
******************/
Expand Down Expand Up @@ -1176,6 +1221,15 @@ public synchronized void reset(long oldMsgSerial, ErrorInfo err) {

}

/*******************
* for tests only
******************/

void disconnectAndSuppressRetries() {
requestState(ConnectionState.disconnected);
suppressRetry = true;
}

/*******************
* internal
******************/
Expand Down Expand Up @@ -1210,9 +1264,11 @@ private boolean isFatalError(ErrorInfo err) {
private StateIndication indicatedState, requestedState;
private ConnectParams pendingConnect;
private boolean pendingReauth;
private boolean suppressRetry; /* for tests only; modified via reflection */
private ITransport transport;
private long suspendTime;
private long msgSerial;
private long lastActivity;

/* for debug/test only */
private RawProtocolListener protocolListener;
Expand Down
1 change: 1 addition & 0 deletions lib/src/main/java/io/ably/lib/transport/ITransport.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,5 @@ public static interface ConnectListener {
public String getURL();

public String getHost();

}
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ private void dispose() {

private void flagActivity() {
lastActivityTime = System.currentTimeMillis();
connectionManager.setLastActivity(lastActivityTime);
if (timer == null && connectionManager.maxIdleInterval != 0) {
/* No timer currently running because previously there was no
* maxIdleInterval configured, but now there is a
Expand Down
Loading