Uploaded image for project: 'Core Server'
  1. Core Server
  2. SERVER-37948

Linearizable read concern is not satisfied by getMores on a cursor

    • Type: Icon: Bug Bug
    • Resolution: Fixed
    • Priority: Icon: Major - P3 Major - P3
    • 4.1.9
    • Affects Version/s: 4.1.4
    • Component/s: Querying, Replication
    • None
    • Fully Compatible
    • ALL
    • Hide
      /*
       * Test linearizability of getMore on a cursor.
       */
      (function() {
          'use strict';
      
          var num_nodes = 3;
          var name = 'linearizable_read_concern';
          var replTest = new ReplSetTest({name: name, nodes: num_nodes, useBridge: true});
          var config = replTest.getReplSetConfig();
      
          // Increased election timeout to avoid having the primary step down while we are
          // testing linearizable functionality on an isolated primary.
          config.settings = {electionTimeoutMillis: 20000};
      
          replTest.startSet();
          replTest.initiate(config);
          replTest.awaitReplication();
          var primary = replTest.getPrimary();
          var secondaries = replTest.getSecondaries();
      
          // Do a write on the original primary.
          assert.writeOK(
              primary.getDB("test").foo.insert({_id: 0, x: 0}, {"writeConcern": {"w": "majority"}}));
          primary = replTest.getPrimary();
      
          // Open a cursor on the original primary at 'linearizable' read concern.
          let result = primary.getDB("test").runCommand(
              {"find": "foo", filter: {_id: 0}, "readConcern": {level: "linearizable"}, batchSize: 0});
          assert.commandWorked(result);
          let cursorId = result.cursor.id;
      
          jsTestLog(
              "Setting up partitions such that the primary is isolated: [Secondary-Secondary] [Primary]");
          secondaries[0].disconnect(primary);
          secondaries[1].disconnect(primary);
      
          jsTestLog("Step up a new primary, and wait until it can accept writes.");
          assert.commandWorked(secondaries[0].adminCommand({replSetStepUp: 1}));
          assert.soonNoExcept(function() {
              return secondaries[0].adminCommand({isMaster: 1}).ismaster;
          });
          let newPrimary = secondaries[0];
          jsTestLog("New node " + newPrimary + " should now be primary");
      
          jsTestLog("Do a majority write to the new primary.");
          assert.writeOK(newPrimary.getDB("test").foo.update(
              {_id: 0}, {$set: {x: 1}}, {"writeConcern": {"w": "majority"}}));
      
          jsTestLog("Do a linearizable read on the new primary.");
          result = newPrimary.getDB("test").runCommand(
              {"find": "foo", filter: {_id: 0}, "readConcern": {level: "linearizable"}, batchSize: 1});
          assert.commandWorked(result);
          // A linearizable read should return the effects of the most recent majority committed write.
          assert.docEq([{_id: 0, x: 1}], result.cursor.firstBatch);
      
          jsTestLog("Do a linearizable getMore read on the old primary.");
          result =
              primary.getDB("test").runCommand({"getMore": cursorId, collection: "foo", batchSize: 1});
          assert.commandWorked(result);
          // A linearizable read should return the effects of the most recent majority committed write
          // (this should fail if getMores do not uphold linearizability guarantee correctly).
          assert.docEq([{_id: 0, x: 1}], result.cursor.nextBatch);
      
          // Re-connect and shut down.
          secondaries[0].reconnect(primary);
          secondaries[1].reconnect(primary);
          replTest.stopSet();
      }());
      
      Show
      /* * Test linearizability of getMore on a cursor. */ ( function () { 'use strict' ; var num_nodes = 3; var name = 'linearizable_read_concern' ; var replTest = new ReplSetTest({name: name, nodes: num_nodes, useBridge: true }); var config = replTest.getReplSetConfig(); // Increased election timeout to avoid having the primary step down while we are // testing linearizable functionality on an isolated primary. config.settings = {electionTimeoutMillis: 20000}; replTest.startSet(); replTest.initiate(config); replTest.awaitReplication(); var primary = replTest.getPrimary(); var secondaries = replTest.getSecondaries(); // Do a write on the original primary. assert.writeOK( primary.getDB( "test" ).foo.insert({_id: 0, x: 0}, { "writeConcern" : { "w" : "majority" }})); primary = replTest.getPrimary(); // Open a cursor on the original primary at 'linearizable' read concern. let result = primary.getDB( "test" ).runCommand( { "find" : "foo" , filter: {_id: 0}, "readConcern" : {level: "linearizable" }, batchSize: 0}); assert.commandWorked(result); let cursorId = result.cursor.id; jsTestLog( "Setting up partitions such that the primary is isolated: [Secondary-Secondary] [Primary]" ); secondaries[0].disconnect(primary); secondaries[1].disconnect(primary); jsTestLog( "Step up a new primary, and wait until it can accept writes." ); assert.commandWorked(secondaries[0].adminCommand({replSetStepUp: 1})); assert.soonNoExcept( function () { return secondaries[0].adminCommand({isMaster: 1}).ismaster; }); let newPrimary = secondaries[0]; jsTestLog( "New node " + newPrimary + " should now be primary" ); jsTestLog( "Do a majority write to the new primary." ); assert.writeOK(newPrimary.getDB( "test" ).foo.update( {_id: 0}, {$set: {x: 1}}, { "writeConcern" : { "w" : "majority" }})); jsTestLog( "Do a linearizable read on the new primary." ); result = newPrimary.getDB( "test" ).runCommand( { "find" : "foo" , filter: {_id: 0}, "readConcern" : {level: "linearizable" }, batchSize: 1}); assert.commandWorked(result); // A linearizable read should return the effects of the most recent majority committed write. assert.docEq([{_id: 0, x: 1}], result.cursor.firstBatch); jsTestLog( "Do a linearizable getMore read on the old primary." ); result = primary.getDB( "test" ).runCommand({ "getMore" : cursorId, collection: "foo" , batchSize: 1}); assert.commandWorked(result); // A linearizable read should return the effects of the most recent majority committed write // ( this should fail if getMores do not uphold linearizability guarantee correctly). assert.docEq([{_id: 0, x: 1}], result.cursor.nextBatch); // Re-connect and shut down. secondaries[0].reconnect(primary); secondaries[1].reconnect(primary); replTest.stopSet(); }());
    • Repl 2019-01-14, Repl 2019-02-11, Repl 2019-02-25

      When a cursor is opened with "linearizable" read concern, we guarantee that any data returned will reflect all successful majority-acknowledged writes that completed prior to the start of the read operation. If we do subsequent getMore operations on this cursor, however, we do not satisfy this linearizability guarantee.

      I believe this bug is caused by the fact that when we wait for linearizable read concern here, we check the read concern arguments on the OperationContext of the running command. getMore commands, however, do not include a read concern directly. Their read concern is stored on the cursor object they are associated with. Since we only check the read concern from the OperationContext, we will presumably just return local read concern, bypassing the logic to satisfy the linearizability guarantee.

            Assignee:
            jason.chan@mongodb.com Jason Chan
            Reporter:
            william.schultz@mongodb.com Will Schultz
            Votes:
            0 Vote for this issue
            Watchers:
            12 Start watching this issue

              Created:
              Updated:
              Resolved: