Delay RPC sending and subject resubscription until we have an open channel on the client side, to try to work around prod appengine disappearing messages. Add a test for channel reconnection and re-subscription.
This commit is contained in:
@@ -38,7 +38,7 @@ var Cosmopolite = function(callbacks, urlPrefix, namespace) {
|
|||||||
this.urlPrefix_ = urlPrefix || '/cosmopolite';
|
this.urlPrefix_ = urlPrefix || '/cosmopolite';
|
||||||
this.namespace_ = namespace || 'cosmopolite';
|
this.namespace_ = namespace || 'cosmopolite';
|
||||||
|
|
||||||
this.channelState_ = this.ChannelState.DOWN;
|
this.channelState_ = this.ChannelState.CLOSED;
|
||||||
this.shutdown_ = false;
|
this.shutdown_ = false;
|
||||||
|
|
||||||
this.rpcQueue_ = [];
|
this.rpcQueue_ = [];
|
||||||
@@ -82,9 +82,14 @@ var Cosmopolite = function(callbacks, urlPrefix, namespace) {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
Cosmopolite.prototype.ChannelState = {
|
Cosmopolite.prototype.ChannelState = {
|
||||||
DOWN: 1,
|
// No channel open, no RPC pending
|
||||||
|
CLOSED: 1,
|
||||||
|
// No channel open, RPC pending
|
||||||
PENDING: 2,
|
PENDING: 2,
|
||||||
UP: 3,
|
// RPC complete, channel opening
|
||||||
|
OPENING: 3,
|
||||||
|
// Channel opened
|
||||||
|
OPEN: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -355,10 +360,10 @@ Cosmopolite.prototype.sendRPC_ = function(command, args, onSuccess) {
|
|||||||
'arguments': args,
|
'arguments': args,
|
||||||
'onSuccess': onSuccess,
|
'onSuccess': onSuccess,
|
||||||
};
|
};
|
||||||
if (this.namespace_ + ':client_id' in localStorage) {
|
if (this.maySendRPC_()) {
|
||||||
this.sendRPCs_([rpc]);
|
this.sendRPCs_([rpc]);
|
||||||
} else {
|
} else {
|
||||||
// Initial RPC hasn't returned. Queue instead of sending.
|
// Queue instead of sending.
|
||||||
this.rpcQueue_.push(rpc);
|
this.rpcQueue_.push(rpc);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -374,7 +379,7 @@ Cosmopolite.prototype.sendRPC_ = function(command, args, onSuccess) {
|
|||||||
* @param {number=} delay Seconds waited before executing this call (for backoff)
|
* @param {number=} delay Seconds waited before executing this call (for backoff)
|
||||||
*/
|
*/
|
||||||
Cosmopolite.prototype.sendRPCs_ = function(commands, delay) {
|
Cosmopolite.prototype.sendRPCs_ = function(commands, delay) {
|
||||||
if (this.shutdown_) {
|
if (this.shutdown_ || !commands.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var request = {
|
var request = {
|
||||||
@@ -425,11 +430,6 @@ Cosmopolite.prototype.sendRPCs_ = function(commands, delay) {
|
|||||||
}
|
}
|
||||||
if ('client_id' in data) {
|
if ('client_id' in data) {
|
||||||
localStorage[this.namespace_ + ':client_id'] = data['client_id'];
|
localStorage[this.namespace_ + ':client_id'] = data['client_id'];
|
||||||
// We may have queued RPCs for lack of a client_id.
|
|
||||||
if (this.rpcQueue_.length) {
|
|
||||||
this.sendRPCs_(this.rpcQueue_);
|
|
||||||
this.rpcQueue_ = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data['status'] == 'retry') {
|
if (data['status'] == 'retry') {
|
||||||
@@ -444,6 +444,8 @@ Cosmopolite.prototype.sendRPCs_ = function(commands, delay) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.flushRPCQueue_();
|
||||||
|
|
||||||
// Handle events that were immediately available as if they came over the
|
// Handle events that were immediately available as if they came over the
|
||||||
// channel. Fire them before the message callbacks, so clients can use
|
// channel. Fire them before the message callbacks, so clients can use
|
||||||
// events like the subscribe promise fulfillment as a barrier for initial
|
// events like the subscribe promise fulfillment as a barrier for initial
|
||||||
@@ -463,21 +465,39 @@ Cosmopolite.prototype.sendRPCs_ = function(commands, delay) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send RPCs to create a server -> client channel and (re-)subscribe to subjects
|
* Are we currently clear to put RPCs on the wire?
|
||||||
|
*
|
||||||
|
* @return {Boolean} Yes or no?
|
||||||
*/
|
*/
|
||||||
Cosmopolite.prototype.createChannel_ = function() {
|
Cosmopolite.prototype.maySendRPC_ = function() {
|
||||||
if (this.channelState_ == this.ChannelState.DOWN) {
|
if (!(this.namespace_ + ':client_id' in localStorage)) {
|
||||||
this.channelState_ = this.ChannelState.PENDING;
|
return false;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (this.channelState_ != this.ChannelState.OPEN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send queued RPCs
|
||||||
|
*/
|
||||||
|
Cosmopolite.prototype.flushRPCQueue_ = function() {
|
||||||
|
if (!this.maySendRPC_() || !this.rpcQueue_.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rpcs = [
|
this.sendRPCs_(this.rpcQueue_);
|
||||||
{
|
this.rpcQueue_ = [];
|
||||||
'command': 'createChannel',
|
};
|
||||||
'onSuccess': this.onCreateChannel_,
|
|
||||||
},
|
/**
|
||||||
];
|
* Resubscribe to subjects (i.e. after reconnection)
|
||||||
|
*/
|
||||||
|
Cosmopolite.prototype.resubscribe_ = function() {
|
||||||
|
var rpcs = [];
|
||||||
for (var subject in this.subscriptions_) {
|
for (var subject in this.subscriptions_) {
|
||||||
var subscription = this.subscriptions_[subject];
|
var subscription = this.subscriptions_[subject];
|
||||||
if (subscription.state != this.SubscriptionState.ACTIVE) {
|
if (subscription.state != this.SubscriptionState.ACTIVE) {
|
||||||
@@ -498,6 +518,26 @@ Cosmopolite.prototype.createChannel_ = function() {
|
|||||||
this.sendRPCs_(rpcs);
|
this.sendRPCs_(rpcs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send RPC to create a server -> client channel
|
||||||
|
*/
|
||||||
|
Cosmopolite.prototype.createChannel_ = function() {
|
||||||
|
if (this.channelState_ == this.ChannelState.CLOSED) {
|
||||||
|
this.channelState_ = this.ChannelState.PENDING;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rpcs = [
|
||||||
|
{
|
||||||
|
'command': 'createChannel',
|
||||||
|
'onSuccess': this.onCreateChannel_,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// sendRPCs instead of sendRPC so we don't queue.
|
||||||
|
this.sendRPCs_(rpcs);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for channel creation on the server side
|
* Callback for channel creation on the server side
|
||||||
*
|
*
|
||||||
@@ -511,7 +551,7 @@ Cosmopolite.prototype.onCreateChannel_ = function(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.channelState_ == this.ChannelState.PENDING) {
|
if (this.channelState_ == this.ChannelState.PENDING) {
|
||||||
this.channelState_ = this.ChannelState.OPEN;
|
this.channelState_ = this.ChannelState.OPENING;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -531,9 +571,19 @@ Cosmopolite.prototype.onCreateChannel_ = function(data) {
|
|||||||
*/
|
*/
|
||||||
Cosmopolite.prototype.onSocketOpen_ = function() {
|
Cosmopolite.prototype.onSocketOpen_ = function() {
|
||||||
console.log(this.loggingPrefix_(), 'channel opened');
|
console.log(this.loggingPrefix_(), 'channel opened');
|
||||||
|
|
||||||
if (this.shutdown_ && this.socket_) {
|
if (this.shutdown_ && this.socket_) {
|
||||||
this.socket_.close();
|
this.socket_.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.channelState_ == this.ChannelState.OPENING) {
|
||||||
|
this.channelState_ = this.ChannelState.OPEN;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flushRPCQueue_();
|
||||||
|
this.resubscribe_();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -547,7 +597,7 @@ Cosmopolite.prototype.onSocketClose_ = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.channelState_ == this.ChannelState.OPEN) {
|
if (this.channelState_ == this.ChannelState.OPEN) {
|
||||||
this.channelState_ = this.ChannelState.DOWN;
|
this.channelState_ = this.ChannelState.CLOSED;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,6 +351,29 @@ asyncTest('Message ordering', function() {
|
|||||||
sendNextMessage();
|
sendNextMessage();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
asyncTest('Reconnect channel', function() {
|
||||||
|
expect(2);
|
||||||
|
|
||||||
|
var subject = randstring();
|
||||||
|
var message = randstring();
|
||||||
|
|
||||||
|
var callbacks = {
|
||||||
|
'onMessage': function(msg) {
|
||||||
|
equal(msg['subject']['name'], subject, 'subject matches');
|
||||||
|
equal(msg['message'], message, 'message matches');
|
||||||
|
cosmo.shutdown();
|
||||||
|
start();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var cosmo = new Cosmopolite(callbacks, null, randstring());
|
||||||
|
cosmo.subscribe(subject, 0).then(function() {
|
||||||
|
// Reach inside to forcefully close the socket
|
||||||
|
cosmo.socket_.close();
|
||||||
|
cosmo.sendMessage(subject, message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
module('dev_appserver only');
|
module('dev_appserver only');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user