Skip to content

Commit 1066804

Browse files
authored
Merge pull request #709 from share/fix/fetch-snapshot-by-timestamp-missing-ops
🐛 Serve current snapshot when fetching by timestamp after latest op
2 parents 4ca2b82 + 4b39980 commit 1066804

2 files changed

Lines changed: 42 additions & 18 deletions

File tree

lib/backend.js

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -881,29 +881,38 @@ Backend.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp
881881
var from = 0;
882882
var to = null;
883883

884-
var shouldGetLatestSnapshot = timestamp === null;
885-
if (shouldGetLatestSnapshot) {
886-
return backend.db.getSnapshot(collection, id, null, null, function(error, snapshot) {
887-
if (error) return callback(error);
888-
889-
callback(null, snapshot);
890-
});
891-
}
892-
893-
milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function(error, snapshot) {
884+
// Always fetch the current snapshot first. We request its metadata so that we
885+
// can read its mtime, which lets us serve the current snapshot directly when
886+
// the requested timestamp is after it. This avoids replaying ops when they
887+
// aren't needed, and - crucially - still works when older ops have been
888+
// deleted/TTLed and the current version can no longer be rebuilt from ops.
889+
db.getSnapshot(collection, id, null, {metadata: true}, function(error, currentSnapshot) {
894890
if (error) return callback(error);
895-
milestoneSnapshot = snapshot;
896-
if (snapshot) from = snapshot.v;
897891

898-
milestoneDb.getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp, function(error, snapshot) {
892+
var mtime = currentSnapshot.m && currentSnapshot.m.mtime;
893+
var shouldGetLatestSnapshot = timestamp === null || (mtime != null && timestamp > mtime);
894+
if (shouldGetLatestSnapshot) {
895+
// Strip the metadata that we only fetched in order to compare the mtime,
896+
// so that the returned snapshot is consistent with the op-replayed path.
897+
currentSnapshot.m = null;
898+
return callback(null, currentSnapshot);
899+
}
900+
901+
milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function(error, snapshot) {
899902
if (error) return callback(error);
900-
if (snapshot) to = snapshot.v;
903+
milestoneSnapshot = snapshot;
904+
if (snapshot) from = snapshot.v;
901905

902-
var options = {metadata: true};
903-
db.getOps(collection, id, from, to, options, function(error, ops) {
906+
milestoneDb.getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp, function(error, snapshot) {
904907
if (error) return callback(error);
905-
filterOpsInPlaceBeforeTimestamp(ops, timestamp);
906-
backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, callback);
908+
if (snapshot) to = snapshot.v;
909+
910+
var options = {metadata: true};
911+
db.getOps(collection, id, from, to, options, function(error, ops) {
912+
if (error) return callback(error);
913+
filterOpsInPlaceBeforeTimestamp(ops, timestamp);
914+
backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, callback);
915+
});
907916
});
908917
});
909918
});

test/client/snapshot-timestamp-request.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,21 @@ describe('SnapshotTimestampRequest', function() {
139139
], done);
140140
});
141141

142+
it('fetches the current snapshot when ops are missing and the timestamp is after the latest op', function(done) {
143+
var connection = backend.connect();
144+
async.series([
145+
// Simulate ops having been deleted/TTLed so the snapshot can't be rebuilt from ops.
146+
backend.db.deleteOps.bind(backend.db, 'books', 'time-machine', null, null, null),
147+
function(next) {
148+
connection.fetchSnapshotByTimestamp('books', 'time-machine', day4, function(error, snapshot) {
149+
if (error) return next(error);
150+
expect(snapshot).to.eql(v3);
151+
next();
152+
});
153+
}
154+
], done);
155+
});
156+
142157
it('fetches the most recent version when not specifying a timestamp', function(done) {
143158
var connection = backend.connect();
144159
async.waterfall([

0 commit comments

Comments
 (0)