Merge branch 'devel' into oplog-limits-buffered

This commit is contained in:
David Glasser
2014-03-01 18:21:52 -08:00
22 changed files with 221 additions and 171 deletions

View File

@@ -1,5 +1,21 @@
## v.NEXT
* Use "faye-websocket" (0.7.2) npm module instead of "websocket" (1.0.8) for
server-to-server DDP.
* minimongo: Support {a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}} #1875
* minimongo: Support {a: {$regex: '', $options: 'i'}} #1874
## v0.7.1.2
* Fix bug in tool error handling that caused `meteor` to crash on Mac
OSX when no computer name is set.
* Work around a bug that caused MongoDB to fail an assertion when using
tailable cursors on non-oplog collections.
## v0.7.1.1
* Integrate with Meteor developer accounts, a new way of managing your
@@ -29,7 +45,7 @@
* Add and improve support for minimongo operators.
- Support `$comment`.
- Support `obj` name in `$where`.
- `$regexp` matches actual regexps properly.
- `$regex` matches actual regexps properly.
- Improve support for `$nin`, `$ne`, `$not`.
- Support using `{ $in: [/foo/, /bar/] }`. #1707
- Support `{$exists: false}`.

View File

@@ -1 +1 @@
0.7.1.1
0.7.1.2

View File

@@ -1,5 +1,5 @@
// While galaxy apps are on their own special meteor releases, override
// Meteor.release here.
if (Meteor.isClient) {
Meteor.release = Meteor.release ? "0.7.1.1" : undefined;
Meteor.release = Meteor.release ? "0.7.1.2" : undefined;
}

View File

@@ -1 +1 @@
0.7.1.1
0.7.1.2

View File

@@ -1 +1 @@
0.7.1.1
0.7.1.2

View File

@@ -1 +1 @@
0.7.1.1
0.7.1.2

View File

@@ -1 +1 @@
0.7.1.1
0.7.1.2

View File

@@ -16,8 +16,13 @@
}
}
},
"websocket": {
"version": "1.0.8"
"faye-websocket": {
"version": "0.7.2",
"dependencies": {
"websocket-driver": {
"version": "0.3.2"
}
}
}
}
}

View File

@@ -49,7 +49,10 @@ var Connection = function (url, options) {
self._stream = new LivedataTest.ClientStream(url, {
retry: options.retry,
headers: options.headers,
_sockjsOptions: options._sockjsOptions
_sockjsOptions: options._sockjsOptions,
// To keep some tests quiet (because we don't have a real API for handling
// client-stream-level errors).
_dontPrintErrors: options._dontPrintErrors
});
}

View File

@@ -1160,7 +1160,7 @@ _.extend(Server.prototype, {
// drop all future data coming over this connection on the
// floor. We don't want to confuse things.
socket.removeAllListeners('data');
setTimeout(function () {
Meteor.setTimeout(function () {
socket.send(stringifyDDP({msg: 'failed', version: version}));
socket.close();
}, timeout);

View File

@@ -683,9 +683,10 @@ if (Meteor.isServer) {
testAsyncMulti("livedata - connect fails to unknown place", [
function (test, expect) {
var self = this;
self.conn = DDP.connect("example.com");
self.conn = DDP.connect("example.com", {_dontPrintErrors: true});
Meteor.setTimeout(expect(function () {
test.isFalse(self.conn.status().connected, "Not connected");
self.conn.close();
}), 500);
}
]);

View File

@@ -3,7 +3,11 @@ Package.describe({
internal: true
});
Npm.depends({sockjs: "0.3.8", websocket: "1.0.8"});
// We use 'faye-websocket' for connections in server-to-server DDP, mostly
// because it's the same library used as a server in sockjs, and it's easiest to
// deal with a single websocket implementation. (Plus, its maintainer is easy
// to work with on pull requests.)
Npm.depends({sockjs: "0.3.8", "faye-websocket": "0.7.2"});
Package.on_use(function (api) {
api.use(['check', 'random', 'ejson', 'json', 'underscore', 'deps',

View File

@@ -11,43 +11,16 @@
// ping frames or with DDP-level messages.)
LivedataTest.ClientStream = function (endpoint, options) {
var self = this;
options = options || {};
self.options = _.extend({
retry: true
}, options);
// WebSocket-Node https://github.com/Worlize/WebSocket-Node
// Chosen because it can run without native components. It has a
// somewhat idiosyncratic API. We may want to use 'ws' instead in the
// future.
//
// Since server-to-server DDP is still an experimental feature, we only
// require the module if we actually create a server-to-server
// connection. This is a minor efficiency improvement, but moreover: while
// 'websocket' doesn't require native components, it tries to use some
// optional native components and prints a warning if it can't load
// them. Since native components in packages don't work when transferred to
// other architectures yet, this means that require('websocket') prints a
// spammy log message when deployed to another architecture. Delaying the
// require means you only get the log message if you're actually using the
// feature.
self.client = new (Npm.require('websocket').client)();
self.client = null; // created in _launchConnection
self.endpoint = endpoint;
self.currentConnection = null;
options = options || {};
self.headers = options.headers || {};
self.client.on('connect', Meteor.bindEnvironment(
function (connection) {
return self._onConnect(connection);
},
"stream connect callback"
));
self.client.on('connectFailed', function (error) {
// XXX: Make this do something better than make the tests hang if it does not work.
return self._lostConnection();
});
self.headers = self.options.headers || {};
self._initCommon();
@@ -63,7 +36,7 @@ _.extend(LivedataTest.ClientStream.prototype, {
send: function (data) {
var self = this;
if (self.currentStatus.connected) {
self.currentConnection.send(data);
self.client.send(data);
}
},
@@ -73,80 +46,37 @@ _.extend(LivedataTest.ClientStream.prototype, {
self.endpoint = url;
},
_onConnect: function (connection) {
_onConnect: function (client) {
var self = this;
if (client !== self.client) {
// This connection is not from the last call to _launchConnection.
// But _launchConnection calls _cleanup which closes previous connections.
// It's our belief that this stifles future 'open' events, but maybe
// we are wrong?
throw new Error("Got open from inactive client");
}
if (self._forcedToDisconnect) {
// We were asked to disconnect between trying to open the connection and
// actually opening it. Let's just pretend this never happened.
connection.close();
self.client.close();
self.client = null;
return;
}
if (self.currentStatus.connected) {
// We already have a connection. It must have been the case that
// we started two parallel connection attempts (because we
// wanted to 'reconnect now' on a hanging connection and we had
// no way to cancel the connection attempt.) Just ignore/close
// the latecomer.
connection.close();
return;
// We already have a connection. It must have been the case that we
// started two parallel connection attempts (because we wanted to
// 'reconnect now' on a hanging connection and we had no way to cancel the
// connection attempt.) But this shouldn't happen (similarly to the client
// !== self.client check above).
throw new Error("Two parallel connections?");
}
if (self.connectionTimer) {
clearTimeout(self.connectionTimer);
self.connectionTimer = null;
}
var onError = Meteor.bindEnvironment(
function (_this, error) {
if (self.currentConnection !== _this)
return;
Meteor._debug("stream error", error.toString(),
(new Date()).toDateString());
self._lostConnection();
},
"stream error callback"
);
connection.on('error', function (error) {
// We have to pass in `this` explicitly because bindEnvironment
// doesn't propagate it for us.
onError(this, error);
});
var onClose = Meteor.bindEnvironment(
function (_this) {
if (self.options._testOnClose)
self.options._testOnClose();
if (self.currentConnection !== _this)
return;
self._lostConnection();
},
"stream close callback"
);
connection.on('close', function () {
// We have to pass in `this` explicitly because bindEnvironment
// doesn't propagate it for us.
onClose(this);
});
connection.on('message', function (message) {
if (self.currentConnection !== this)
return; // old connection still emitting messages
if (message.type === "utf8") // ignore binary frames
_.each(self.eventCallbacks.message, function (callback) {
callback(message.utf8Data);
});
});
self._clearConnectionTimer();
// update status
self.currentConnection = connection;
self.currentStatus.status = "connected";
self.currentStatus.connected = true;
self.currentStatus.retryCount = 0;
@@ -161,10 +91,10 @@ _.extend(LivedataTest.ClientStream.prototype, {
var self = this;
self._clearConnectionTimer();
if (self.currentConnection) {
var conn = self.currentConnection;
self.currentConnection = null;
conn.close();
if (self.client) {
var client = self.client;
self.client = null;
client.close();
}
},
@@ -181,25 +111,63 @@ _.extend(LivedataTest.ClientStream.prototype, {
var self = this;
self._cleanup(); // cleanup the old socket, if there was one.
// launch a connect attempt. we have no way to track it. we either
// get an _onConnect event, or we don't.
// Since server-to-server DDP is still an experimental feature, we only
// require the module if we actually create a server-to-server
// connection.
var FayeWebSocket = Npm.require('faye-websocket');
// XXX: set up a timeout on this.
// We would like to specify 'ddp' as the subprotocol here. The npm module we
// used to use as a client would fail the handshake if we ask for a
// subprotocol and the server doesn't send one back (and sockjs doesn't).
// Faye doesn't have that behavior; it's unclear from reading RFC 6455 if
// Faye is erroneous or not. So for now, we don't specify protocols.
var client = self.client = new FayeWebSocket.Client(
toWebsocketUrl(self.endpoint),
[/*no subprotocols*/],
{headers: self.headers}
);
// we would like to specify 'ddp' as the protocol here, but
// unfortunately WebSocket-Node fails the handshake if we ask for
// a protocol and the server doesn't send one back (and sockjs
// doesn't). also, related: I guess we have to accept that
// 'stream' is ddp-specific
self.client.connect(toWebsocketUrl(self.endpoint),
undefined, // protocols
undefined, // origin
self.headers);
if (self.connectionTimer)
clearTimeout(self.connectionTimer);
self.connectionTimer = setTimeout(
self._clearConnectionTimer();
self.connectionTimer = Meteor.setTimeout(
_.bind(self._lostConnection, self),
self.CONNECT_TIMEOUT);
self.client.on('open', Meteor.bindEnvironment(function () {
return self._onConnect(client);
}, "stream connect callback"));
var clientOnIfCurrent = function (event, description, f) {
self.client.on(event, Meteor.bindEnvironment(function () {
// Ignore events from any connection we've already cleaned up.
if (client !== self.client)
return;
f.apply(this, arguments);
}, description));
};
clientOnIfCurrent('error', 'stream error callback', function (error) {
if (!self.options._dontPrintErrors)
Meteor._debug("stream error", error.message);
// XXX: Make this do something better than make the tests hang if it does
// not work.
self._lostConnection();
});
clientOnIfCurrent('close', 'stream close callback', function () {
self._lostConnection();
});
clientOnIfCurrent('message', 'stream message callback', function (message) {
// Ignore binary frames, where message.data is a Buffer
if (typeof message.data !== "string")
return;
_.each(self.eventCallbacks.message, function (callback) {
callback(message.data);
});
});
}
});

View File

@@ -1,17 +1,24 @@
var Fiber = Npm.require('fibers');
Tinytest.addAsync("stream client - callbacks run in a fiber", function (test, onComplete) {
stream = new LivedataTest.ClientStream(
Meteor.absoluteUrl(),
{
_testOnClose: function () {
test.isTrue(Fiber.current);
onComplete();
}
}
);
stream.on('reset', function () {
test.isTrue(Fiber.current);
stream.disconnect();
});
});
testAsyncMulti("stream client - callbacks run in a fiber", [
function (test, expect) {
var stream = new LivedataTest.ClientStream(Meteor.absoluteUrl());
var messageFired = false;
var resetFired = false;
stream.on('message', expect(function () {
test.isTrue(Fiber.current);
if (resetFired)
stream.disconnect();
messageFired = true;
}));
stream.on('reset', expect(function () {
test.isTrue(Fiber.current);
if (messageFired)
stream.disconnect();
resetFired = true;
}));
}
]);

View File

@@ -16,7 +16,10 @@ isIndexable = function (x) {
return isArray(x) || isPlainObject(x);
};
isOperatorObject = function (valueSelector) {
// Returns true if this is an object with at least one key and all keys begin
// with $. Unless inconsistentOK is set, throws if some keys begin with $ and
// others don't.
isOperatorObject = function (valueSelector, inconsistentOK) {
if (!isPlainObject(valueSelector))
return false;
@@ -26,7 +29,10 @@ isOperatorObject = function (valueSelector) {
if (theseAreOperators === undefined) {
theseAreOperators = thisIsOperator;
} else if (theseAreOperators !== thisIsOperator) {
throw new Error("Inconsistent operator: " + valueSelector);
if (!inconsistentOK)
throw new Error("Inconsistent operator: " +
JSON.stringify(valueSelector));
theseAreOperators = false;
}
});
return !!theseAreOperators; // {} has no operators

View File

@@ -678,6 +678,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
nomatch({a: {$regex: 'a'}}, {a: 'cut'});
nomatch({a: {$regex: 'a'}}, {a: 'CAT'});
match({a: {$regex: 'a', $options: 'i'}}, {a: 'CAT'});
match({a: {$regex: '', $options: 'i'}}, {a: 'foo'});
nomatch({a: {$regex: '', $options: 'i'}}, {});
nomatch({a: {$regex: '', $options: 'i'}}, {a: 5});
nomatch({a: /undefined/}, {});
nomatch({a: {$regex: 'undefined'}}, {});
nomatch({a: /xxx/}, {});
@@ -817,6 +820,12 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
nomatch({$or: [{a: 2}, {a: 3}], b: 2}, {a: 1, b: 2});
nomatch({$or: [{a: 1}, {a: 2}], b: 3}, {a: 1, b: 2});
// Combining $or with equality
match({x: 1, $or: [{a: 1}, {b: 1}]}, {x: 1, b: 1});
match({$or: [{a: 1}, {b: 1}], x: 1}, {x: 1, b: 1});
nomatch({x: 1, $or: [{a: 1}, {b: 1}]}, {b: 1});
nomatch({x: 1, $or: [{a: 1}, {b: 1}]}, {x: 1});
// $or and $lt, $lte, $gt, $gte
match({$or: [{a: {$lte: 1}}, {a: 2}]}, {a: 1});
nomatch({$or: [{a: {$lt: 1}}, {a: 2}]}, {a: 1});
@@ -1100,6 +1109,16 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
nomatch({a: {$elemMatch: {x: 5}}}, {a: {x: 5}});
match({a: {$elemMatch: {0: {$gt: 5, $lt: 9}}}}, {a: [[6]]});
match({a: {$elemMatch: {'0.b': {$gt: 5, $lt: 9}}}}, {a: [[{b:6}]]});
match({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}},
{a: [{x: 1, b: 1}]});
match({a: {$elemMatch: {$or: [{a: 1}, {b: 1}], x: 1}}},
{a: [{x: 1, b: 1}]});
nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}},
{a: [{b: 1}]});
nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}},
{a: [{x: 1}]});
nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}},
{a: [{x: 1}, {b: 1}]});
// $comment
match({a: 5, $comment: "asdf"}, {a: 5});

View File

@@ -384,7 +384,7 @@ var VALUE_OPERATORS = {
},
// $options just provides options for $regex; its logic is inside $regex
$options: function (operand, valueSelector) {
if (!valueSelector.$regex)
if (!_.has(valueSelector, '$regex'))
throw Error("$options needs a $regex");
return everythingMatcher;
},
@@ -653,7 +653,7 @@ var ELEMENT_OPERATORS = {
throw Error("$elemMatch need an object");
var subMatcher, isDocMatcher;
if (isOperatorObject(operand)) {
if (isOperatorObject(operand, true)) {
subMatcher = compileValueSelector(operand, matcher);
isDocMatcher = false;
} else {

View File

@@ -57,7 +57,7 @@ _.extend(Retry.prototype, {
var timeout = self._timeout(count);
if (self.retryTimer)
clearTimeout(self.retryTimer);
self.retryTimer = setTimeout(fn, timeout);
self.retryTimer = Meteor.setTimeout(fn, timeout);
return timeout;
}

View File

@@ -1,7 +1,4 @@
=> Meteor 0.7.1.1: Extend oplog tailing driver to support most common
MongoDB queries. Introduce Meteor developer accounts, a new way of
managing your meteor.com deployed sites. When you use `meteor
deploy`, you will be prompted to create a developer account.
=> Meteor 0.7.1.2: Fix crash on OSX machines with no hostname set.
This release is being downloaded in the background. Update your
project to Meteor 0.7.1.1 by running 'meteor update'.
project to Meteor 0.7.1.2 by running 'meteor update'.

View File

@@ -88,6 +88,9 @@
"http://jquery.com/upgrade-guide/1.9/"]
}
},
{
"release": "0.7.1.2"
},
{
"release": "NEXT"
}

View File

@@ -78,7 +78,7 @@ var ServiceConnection = function (galaxy, service) {
// from the hostname of endpointUrl, and run the login command for
// that galaxy.
if (! authToken)
throw new Error("not logged in to galaxy?")
throw new Error("not logged in to galaxy?");
self.connection = Package.livedata.DDP.connect(endpointUrl, {
headers: {
@@ -117,10 +117,12 @@ _.extend(ServiceConnection.prototype, {
var args = _.toArray(arguments);
var name = args.shift();
self.connection.apply(name, args, function (err, result) {
if (err)
if (err) {
fut['throw'](err);
else
} else {
self._cleanUpTimer();
fut['return'](result);
}
});
return fut.wait();
@@ -141,6 +143,7 @@ _.extend(ServiceConnection.prototype, {
args.push({
onReady: function () {
ready = true;
self._cleanUpTimer();
fut['return']();
},
onError: function (e) {
@@ -151,8 +154,16 @@ _.extend(ServiceConnection.prototype, {
}
});
self.connection.subscribe.apply(self.connection, args);
return fut.wait();
var sub = self.connection.subscribe.apply(self.connection, args);
fut.wait();
return sub;
},
_cleanUpTimer: function () {
var self = this;
var Package = getPackage();
Package.meteor.Meteor.clearTimeout(self.connectionTimer);
self.connectionTimer = null;
},
close: function () {
@@ -163,9 +174,7 @@ _.extend(ServiceConnection.prototype, {
}
if (self.connectionTimer) {
// Clean up the timer so that Node can exit cleanly
var Package = getPackage();
Package.meteor.Meteor.clearTimeout(self.connectionTimer);
self.connectionTimer = null;
self._cleanUpTimer();
}
}
});
@@ -456,17 +465,6 @@ exports.logs = function (options) {
throw new Error("Can't listen to messages on the logs collection");
var logsSubscription = null;
// In case of reconnect recover the state so user sees only new logs
logReader.connection.onReconnect = function () {
logsSubscription && logsSubscription.stop();
var opts = { streaming: options.streaming };
if (lastLogId)
opts.resumeAfterId = lastLogId;
// XXX correctly handle errors on resubscribe
logsSubscription = logReader.subscribeAndWait("logsForApp",
options.app, opts);
};
try {
logsSubscription =
logReader.subscribeAndWait("logsForApp", options.app,
@@ -477,6 +475,29 @@ exports.logs = function (options) {
});
}
// In case of reconnect recover the state so user sees only new logs.
// Only set up the onReconnect handler after the subscribe and wait
// has returned; if we set it up before, then we'll end up with two
// subscriptions, because the onReconnect handler will run for the
// first time before the subscribeAndWait returns.
logReader.connection.onReconnect = function () {
logsSubscription && logsSubscription.stop();
var opts = { streaming: options.streaming };
if (lastLogId)
opts.resumeAfterId = lastLogId;
// Don't use subscribeAndWait here; it'll deadlock. We can't
// process the sub messages until `onReconnect` returns, and
// `onReconnect` won't return unless the sub messages have been
// processed. There's no reason we need to wait for the sub to be
// ready here anyway.
// XXX correctly handle errors on resubscribe
logsSubscription = logReader.connection.subscribe(
"logsForApp",
options.app,
opts
);
};
return options.streaming ? null : 0;
} finally {
// If not streaming, close the connection to log-reader so that

View File

@@ -191,7 +191,7 @@ WatchSet.fromJSON = function (json) {
set.files = _.clone(json.files);
var reFromJSON = function (j) {
if (j.$regex)
if (_.has(j, '$regex'))
return new RegExp(j.$regex, j.$options);
return new RegExp(j);
};