From f674117136f9b2699ab42a32f6f08f6c0b5a84b3 Mon Sep 17 00:00:00 2001 From: Lingzhi Deng Date: Tue, 28 Apr 2020 21:20:45 -0400 Subject: SERVER-47577: readConcernCounters metric in serverStatus --- .../noPassthrough/server_read_concern_metrics.js | 515 +++++++++------------ src/mongo/db/commands.h | 8 + src/mongo/db/commands/count_cmd.cpp | 4 + src/mongo/db/commands/distinct.cpp | 4 + src/mongo/db/commands/find_cmd.cpp | 6 +- src/mongo/db/commands/haystack.cpp | 4 + src/mongo/db/commands/map_reduce_command_base.h | 4 + src/mongo/db/commands/pipeline_command.cpp | 4 + src/mongo/db/repl/read_concern_args.h | 12 + src/mongo/db/service_entry_point_common.cpp | 25 +- src/mongo/db/stats/read_concern_stats.idl | 37 +- src/mongo/db/stats/server_read_concern_metrics.cpp | 59 ++- src/mongo/db/stats/server_read_concern_metrics.h | 19 +- 13 files changed, 377 insertions(+), 324 deletions(-) diff --git a/jstests/noPassthrough/server_read_concern_metrics.js b/jstests/noPassthrough/server_read_concern_metrics.js index 0c33d81d879..1314d3fcc82 100644 --- a/jstests/noPassthrough/server_read_concern_metrics.js +++ b/jstests/noPassthrough/server_read_concern_metrics.js @@ -5,38 +5,113 @@ // Verifies that the server status response has the fields that we expect. function verifyServerStatusFields(serverStatusResponse) { - assert(serverStatusResponse.hasOwnProperty("opReadConcernCounters"), - "Expected the serverStatus response to have a 'opReadConcernCounters' field\n" + - tojson(serverStatusResponse)); - assert( - serverStatusResponse.opReadConcernCounters.hasOwnProperty("available"), - "The 'opReadConcernCounters' field in serverStatus did not have the 'available' field\n" + - tojson(serverStatusResponse.opReadConcernCounters)); + assert(serverStatusResponse.hasOwnProperty("readConcernCounters"), + "Missing 'readConcernCounters' from serverStatus\n" + tojson(serverStatusResponse)); + + assert(serverStatusResponse.readConcernCounters.hasOwnProperty("nonTransactionOps"), + "Missing 'nonTransactionOps' from 'readConcernCounters'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("none"), + "Missing 'none' from 'readConcernCounters.nonTransactionOps'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("local"), + "Missing 'local' from 'readConcernCounters.nonTransactionOps'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("available"), + "Missing 'available' from 'readConcernCounters.nonTransactionOps'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("majority"), + "Missing 'majority' from 'readConcernCounters.nonTransactionOps'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("snapshot"), + "Missing 'snapshot' from 'readConcernCounters.nonTransactionOps'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.nonTransactionOps.snapshot.hasOwnProperty( + "withClusterTime"), + "Missing 'withClusterTime' from 'readConcernCounters.nonTransactionOps.snapshot'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.nonTransactionOps.snapshot.hasOwnProperty( + "withoutClusterTime"), + "Missing 'withoutClusterTime' from 'readConcernCounters.nonTransactionOps.snapshot'\n" + + tojson(serverStatusResponse.readConcernCounters)); assert( - serverStatusResponse.opReadConcernCounters.hasOwnProperty("linearizable"), - "The 'opReadConcernCounters' field in serverStatus did not have the 'linearizable' field\n" + - tojson(serverStatusResponse.opReadConcernCounters)); - assert(serverStatusResponse.opReadConcernCounters.hasOwnProperty("local"), - "The 'opReadConcernCounters' field in serverStatus did not have the 'local' field\n" + - tojson(serverStatusResponse.opReadConcernCounters)); - assert(serverStatusResponse.opReadConcernCounters.hasOwnProperty("majority"), - "The 'opReadConcernCounters' field in serverStatus did not have the 'majority' field\n" + - tojson(serverStatusResponse.opReadConcernCounters)); - assert(serverStatusResponse.opReadConcernCounters.hasOwnProperty("snapshot"), - "The 'opReadConcernCounters' field in serverStatus did not have the 'snapshot' field\n" + - tojson(serverStatusResponse.opReadConcernCounters)); - assert(serverStatusResponse.opReadConcernCounters.hasOwnProperty("none"), - "The 'opReadConcernCounters' field in serverStatus did not have the 'none' field\n" + - tojson(serverStatusResponse.opReadConcernCounters)); + serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("linearizable"), + "Missing 'linearizable' from 'readConcernCounters.nonTransactionOps'\n" + + tojson(serverStatusResponse.readConcernCounters)); + + assert(serverStatusResponse.readConcernCounters.hasOwnProperty("transactionOps"), + "Missing 'transactionOps' from 'readConcernCounters'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.transactionOps.hasOwnProperty("none"), + "Missing 'none' from 'readConcernCounters.transactionOps'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.transactionOps.hasOwnProperty("local"), + "Missing 'local' from 'readConcernCounters.transactionOps'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.transactionOps.hasOwnProperty("majority"), + "Missing 'majority' from 'readConcernCounters.transactionOps'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.transactionOps.hasOwnProperty("snapshot"), + "Missing 'snapshot' from 'readConcernCounters.transactionOps'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.transactionOps.snapshot.hasOwnProperty( + "withClusterTime"), + "Missing 'withClusterTime' from 'readConcernCounters.transactionOps.snapshot'\n" + + tojson(serverStatusResponse.readConcernCounters)); + assert(serverStatusResponse.readConcernCounters.transactionOps.snapshot.hasOwnProperty( + "withoutClusterTime"), + "Missing 'withoutClusterTime' from 'readConcernCounters.transactionOps.snapshot'\n" + + tojson(serverStatusResponse.readConcernCounters)); } // Verifies that the given value of the server status response is incremented in the way // we expect. -function verifyServerStatusChange(initialStats, newStats, valueName, expectedIncrement) { - assert.eq(initialStats[valueName] + expectedIncrement, - newStats[valueName], - "expected " + valueName + " to increase by " + expectedIncrement + - ", initialStats: " + tojson(initialStats) + ", newStats: " + tojson(newStats)); +function verifyServerStatusChange(initialStatus, + newStatus, + field, + expectedIncrement, + {isTransaction = false, atClusterTime = false} = {}) { + verifyServerStatusFields(newStatus); + let initialCounters = initialStatus.readConcernCounters; + let newCounters = newStatus.readConcernCounters; + let initialOps, newOps; + let fieldPath = "serverStatus.readConcernCounters"; + if (isTransaction) { + initialOps = initialCounters.transactionOps; + newOps = newCounters.transactionOps; + fieldPath = fieldPath + ".transactionOps"; + } else { + initialOps = initialCounters.nonTransactionOps; + newOps = newCounters.nonTransactionOps; + fieldPath = fieldPath + ".nonTransactionOps"; + } + + if (field === "snapshot") { + initialOps = initialOps.snapshot; + newOps = newOps.snapshot; + fieldPath = fieldPath + ".snapshot"; + if (atClusterTime) { + field = "withClusterTime"; + } else { + field = "withoutClusterTime"; + } + } + + fieldPath = fieldPath + "." + field; + assert.eq(initialOps[field] + expectedIncrement, + newOps[field], + "expected " + fieldPath + " to increase by " + expectedIncrement + + ", initialStats: " + tojson(initialCounters) + + ", newStats: " + tojson(newCounters)); + + // Update the value of the field to the new value so we can compare the rest of the fields using + // assert.docEq. + initialOps[field] = newOps[field]; + + assert.docEq(initialCounters, + newCounters, + "Expected docEq after updating " + fieldPath + ", initialCounters: " + + tojson(initialCounters + ", newCounters: " + tojson(newCounters))); } const rst = new ReplSetTest({nodes: 1}); @@ -53,7 +128,14 @@ const sessionOptions = { const session = testDB.getMongo().startSession(sessionOptions); const sessionDb = session.getDatabase(dbName); const sessionColl = sessionDb[collName]; -testDB.runCommand({drop: collName, writeConcern: {w: "majority"}}); + +testDB.runCommand({drop: collName}); +assert.commandWorked(testDB.createCollection(collName)); +assert.commandWorked(testDB.runCommand({ + createIndexes: collName, + indexes: [{key: {haystack: "geoHaystack", a: 1}, name: "haystack_geo", bucketSize: 1}], + writeConcern: {w: "majority"} +})); assert.commandWorked(testColl.insert({_id: 0})); // Run an initial transaction to get config.transactions state into memory. @@ -71,293 +153,152 @@ function getServerStatus(conn) { let serverStatus = getServerStatus(testDB); verifyServerStatusFields(serverStatus); -// Run a find with no readConcern. -assert.eq(testColl.find().itcount(), 1); -let newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 1); -serverStatus = newStatus; - -// Run a find with a readConcern with no level. -assert.commandWorked( - testDB.runCommand({find: collName, readConcern: {afterClusterTime: Timestamp(1, 1)}})); -newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 1); -serverStatus = newStatus; +let newStatus; // Run a legacy query. primary.forceReadMode("legacy"); assert.eq(testColl.find().itcount(), 1); newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 1); +verifyServerStatusChange(serverStatus, newStatus, "none", 1); primary.forceReadMode("commands"); serverStatus = newStatus; -// Run a find with a readConcern level available. -assert.eq(testColl.find().readConcern("available").itcount(), 1); +// Run a command without a readConcern. +assert.eq(testColl.find().itcount(), 1); newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 1); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); +verifyServerStatusChange(serverStatus, newStatus, "none", 1); serverStatus = newStatus; -// Run a find with a readConcern level linearizable. -assert.eq(testColl.find().readConcern("linearizable").itcount(), 1); -newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 1); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); -serverStatus = newStatus; +// Non-transaction reads. +for (let level of ["none", "local", "available", "snapshot", "majority", "linearizable"]) { + jsTestLog("Testing non-transaction reads with readConcern " + level); + let readConcern = {}; + if (level !== "none") { + readConcern = {level: level}; + } -// Run a find with a readConcern level local. -assert.eq(testColl.find().readConcern("local").itcount(), 1); -newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 1); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); -serverStatus = newStatus; + assert.commandWorked(testDB.runCommand({find: collName, readConcern: readConcern})); + newStatus = getServerStatus(testDB); + verifyServerStatusChange(serverStatus, newStatus, level, 1); + serverStatus = newStatus; -// Run a find with a readConcern level majority. -assert.eq(testColl.find().readConcern("majority").itcount(), 1); -newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 1); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); -serverStatus = newStatus; + assert.commandWorked(testDB.runCommand( + {aggregate: collName, pipeline: [], cursor: {}, readConcern: readConcern})); + newStatus = getServerStatus(testDB); + verifyServerStatusChange(serverStatus, newStatus, level, 1); + serverStatus = newStatus; -// Run a find in a transaction with readConcern level snapshot. -session.startTransaction({readConcern: {level: "snapshot"}}); -assert.eq(sessionColl.find().itcount(), 1); -newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 1); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); -assert.commandWorked(session.abortTransaction_forTesting()); -serverStatus = newStatus; + assert.commandWorked(testDB.runCommand({count: collName, readConcern: readConcern})); + newStatus = getServerStatus(testDB); + verifyServerStatusChange(serverStatus, newStatus, level, 1); + serverStatus = newStatus; -// Run a find in a transaction with no specified readConcern level. -session.startTransaction(); -assert.eq(sessionColl.find().itcount(), 1); -newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 1); -assert.commandWorked(session.abortTransaction_forTesting()); -serverStatus = newStatus; + assert.commandWorked( + testDB.runCommand({distinct: collName, key: "_id", readConcern: readConcern})); + newStatus = getServerStatus(testDB); + verifyServerStatusChange(serverStatus, newStatus, level, 1); + serverStatus = newStatus; -// Run a find in a transaction with readConcern level local. -session.startTransaction({readConcern: {level: "local"}}); -assert.eq(sessionColl.find().itcount(), 1); -newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 1); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); -assert.commandWorked(session.abortTransaction_forTesting()); -serverStatus = newStatus; + assert.commandWorked(testDB.runCommand({ + geoSearch: collName, + near: [0, 0], + maxDistance: 1, + search: {a: 1}, + readConcern: readConcern + })); + newStatus = getServerStatus(testDB); + verifyServerStatusChange(serverStatus, newStatus, level, 1); + serverStatus = newStatus; -// Run a find in a transaction with readConcern level majority. -session.startTransaction({readConcern: {level: "majority"}}); -assert.eq(sessionColl.find().itcount(), 1); -newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 1); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); -serverStatus = newStatus; + if (level in ["none", "local", "available"]) { + // mapReduce only support local and available. + assert.commandWorked(testDB.runCommand({ + mapReduce: collName, + map: function() { + emit(this.a, this.a); + }, + reduce: function(key, vals) { + return 1; + }, + out: {inline: 1}, + readConcern: readConcern + })); + newStatus = getServerStatus(testDB); + verifyServerStatusChange(serverStatus, newStatus, level, 1); + serverStatus = newStatus; + } -// Run a second find in the same transaction. It will inherit the readConcern from the -// transaction. -assert.eq(sessionColl.find().itcount(), 1); -newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 1); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); -assert.commandWorked(session.abortTransaction_forTesting()); -serverStatus = newStatus; + // getMore does not count toward readConcern metrics. getMore inherits the readConcern of the + // originating command. + let res = assert.commandWorked( + testDB.runCommand({find: collName, batchSize: 0, readConcern: readConcern})); + serverStatus = getServerStatus(testDB); + assert.commandWorked(testDB.runCommand({getMore: res.cursor.id, collection: collName})); + newStatus = getServerStatus(testDB); + verifyServerStatusChange(serverStatus, newStatus, level, 0); +} -// Aggregation does not count toward readConcern metrics. Aggregation is counted as a 'command' -// in the 'opCounters' serverStatus section, and we only track the readConcern of queries -// tracked in 'opCounters.query'. -assert.eq(testColl.aggregate([]).itcount(), 1); +// Test non-transaction snapshot with atClusterTime. +let insertTimestamp = + assert.commandWorked(testDB.runCommand({insert: "atClusterTime", documents: [{_id: 0}]})) + .operationTime; +jsTestLog("Testing non-transaction reads with atClusterTime"); +serverStatus = getServerStatus(testDB); +assert.commandWorked(testDB.runCommand( + {find: "atClusterTime", readConcern: {level: "snapshot", atClusterTime: insertTimestamp}})); newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); +verifyServerStatusChange(serverStatus, newStatus, "snapshot", 1, {atClusterTime: true}); serverStatus = newStatus; -// The count command does not count toward readConcern metrics. The count command is counted as -// a 'command' in the 'opCounters' serverStatus section, and we only track the readConcern of -// queries tracked in 'opCounters.query'. -assert.eq(testColl.count({_id: 0}), 1); +// Transaction reads. +for (let level of ["none", "local", "snapshot", "majority"]) { + jsTestLog("Testing transaction reads with readConcern " + level); + if (level === "none") { + session.startTransaction(); + } else { + session.startTransaction({readConcern: {level: level}}); + } + assert.eq(sessionColl.find().itcount(), 1); + newStatus = getServerStatus(testDB); + verifyServerStatusChange(serverStatus, newStatus, level, 1, {isTransaction: true}); + serverStatus = newStatus; + + // Run a second find in the same transaction. It will inherit the readConcern from the + // transaction. + assert.eq(sessionColl.find().itcount(), 1); + newStatus = getServerStatus(testDB); + verifyServerStatusChange(serverStatus, newStatus, level, 1, {isTransaction: true}); + serverStatus = newStatus; + + // Run an insert in the same transaction. This should not count toward the readConcern metrics. + assert.commandWorked( + sessionDb.runCommand({insert: "transaction", documents: [{level: level}]})); + newStatus = getServerStatus(testDB); + verifyServerStatusChange(serverStatus, newStatus, level, 0, {isTransaction: true}); + assert.commandWorked(session.abortTransaction_forTesting()); + serverStatus = newStatus; +} + +// Test transaction snapshot with atClusterTime. +insertTimestamp = + assert.commandWorked(testDB.runCommand({insert: "atClusterTime", documents: [{_id: 1}]})) + .operationTime; +jsTestLog("Testing transaction reads with atClusterTime"); +session.startTransaction({readConcern: {level: "snapshot", atClusterTime: insertTimestamp}}); +serverStatus = getServerStatus(testDB); + +assert.eq(sessionColl.find().itcount(), 1); newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); + serverStatus, newStatus, "snapshot", 1, {isTransaction: true, atClusterTime: true}); serverStatus = newStatus; -// getMore does not count toward readConcern metrics. getMore inherits the readConcern of the -// originating command. It is not counted in 'opCounters.query'. -let res = assert.commandWorked(testDB.runCommand({find: collName, batchSize: 0})); -serverStatus = getServerStatus(testDB); -assert.commandWorked(testDB.runCommand({getMore: res.cursor.id, collection: collName})); +// Perform another read in the same transaction. +assert.eq(sessionColl.find().itcount(), 1); newStatus = getServerStatus(testDB); -verifyServerStatusFields(newStatus); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "available", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "linearizable", 0); verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "local", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "majority", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "snapshot", 0); -verifyServerStatusChange( - serverStatus.opReadConcernCounters, newStatus.opReadConcernCounters, "none", 0); + serverStatus, newStatus, "snapshot", 1, {isTransaction: true, atClusterTime: true}); +assert.commandWorked(session.abortTransaction_forTesting()); session.endSession(); rst.stopSet(); diff --git a/src/mongo/db/commands.h b/src/mongo/db/commands.h index 37f85f0bd05..609371c6071 100644 --- a/src/mongo/db/commands.h +++ b/src/mongo/db/commands.h @@ -381,6 +381,14 @@ public: return true; } + /** + * Override and return true if the readConcernCounters in serverStatus should not be incremented + * on behalf of this command. + */ + virtual bool shouldAffectReadConcernCounter() const { + return false; + } + /** * Return true if the command requires auth. */ diff --git a/src/mongo/db/commands/count_cmd.cpp b/src/mongo/db/commands/count_cmd.cpp index 3063da6955b..aab21671ba9 100644 --- a/src/mongo/db/commands/count_cmd.cpp +++ b/src/mongo/db/commands/count_cmd.cpp @@ -94,6 +94,10 @@ public: return ReadConcernSupportResult::allSupportedAndDefaultPermitted(); } + bool shouldAffectReadConcernCounter() const override { + return true; + } + bool supportsReadMirroring(const BSONObj&) const override { return true; } diff --git a/src/mongo/db/commands/distinct.cpp b/src/mongo/db/commands/distinct.cpp index 0d3c2318bff..964edb9dcb6 100644 --- a/src/mongo/db/commands/distinct.cpp +++ b/src/mongo/db/commands/distinct.cpp @@ -93,6 +93,10 @@ public: return ReadConcernSupportResult::allSupportedAndDefaultPermitted(); } + bool shouldAffectReadConcernCounter() const override { + return true; + } + bool supportsReadMirroring(const BSONObj&) const override { return true; } diff --git a/src/mongo/db/commands/find_cmd.cpp b/src/mongo/db/commands/find_cmd.cpp index d83db1ac6e2..de42810ba9b 100644 --- a/src/mongo/db/commands/find_cmd.cpp +++ b/src/mongo/db/commands/find_cmd.cpp @@ -169,6 +169,10 @@ public: return false; } + bool shouldAffectReadConcernCounter() const override { + return true; + } + class Invocation final : public CommandInvocation { public: Invocation(const FindCmd* definition, const OpMsgRequest& request, StringData dbName) @@ -301,8 +305,6 @@ public: CommandHelpers::handleMarkKillOnClientDisconnect(opCtx); // Although it is a command, a find command gets counted as a query. globalOpCounters.gotQuery(); - ServerReadConcernMetrics::get(opCtx)->recordReadConcern( - repl::ReadConcernArgs::get(opCtx)); // Parse the command BSON to a QueryRequest. Pass in the parsedNss in case _request.body // does not have a UUID. diff --git a/src/mongo/db/commands/haystack.cpp b/src/mongo/db/commands/haystack.cpp index 93d3df3ff65..2346e048772 100644 --- a/src/mongo/db/commands/haystack.cpp +++ b/src/mongo/db/commands/haystack.cpp @@ -85,6 +85,10 @@ public: return ReadConcernSupportResult::allSupportedAndDefaultPermitted(); } + bool shouldAffectReadConcernCounter() const override { + return true; + } + ReadWriteType getReadWriteType() const { return ReadWriteType::kRead; } diff --git a/src/mongo/db/commands/map_reduce_command_base.h b/src/mongo/db/commands/map_reduce_command_base.h index a1c45276634..bcc0870dfe5 100644 --- a/src/mongo/db/commands/map_reduce_command_base.h +++ b/src/mongo/db/commands/map_reduce_command_base.h @@ -62,6 +62,10 @@ public: {kDefaultReadConcernNotPermitted}}; } + bool shouldAffectReadConcernCounter() const override { + return true; + } + virtual bool supportsWriteConcern(const BSONObj& cmd) const override { return map_reduce_common::mrSupportsWriteConcern(cmd); } diff --git a/src/mongo/db/commands/pipeline_command.cpp b/src/mongo/db/commands/pipeline_command.cpp index ebc251e65ea..915457281fe 100644 --- a/src/mongo/db/commands/pipeline_command.cpp +++ b/src/mongo/db/commands/pipeline_command.cpp @@ -78,6 +78,10 @@ public: this, opMsgRequest, std::move(aggregationRequest), std::move(privileges)); } + bool shouldAffectReadConcernCounter() const override { + return true; + } + class Invocation final : public CommandInvocation { public: Invocation(Command* cmd, diff --git a/src/mongo/db/repl/read_concern_args.h b/src/mongo/db/repl/read_concern_args.h index 04a2c3e131a..bdc3c5bf4eb 100644 --- a/src/mongo/db/repl/read_concern_args.h +++ b/src/mongo/db/repl/read_concern_args.h @@ -185,8 +185,18 @@ public: void setArgsAtClusterTimeForSnapshot(Timestamp ts) { invariant(_level && _level == ReadConcernLevel::kSnapshotReadConcern); invariant(!_atClusterTime); + invariant(!_atClusterTimeSelected); _afterClusterTime = boost::none; _atClusterTime = LogicalTime(ts); + _atClusterTimeSelected = true; + } + + /** + * Return whether an atClusterTime has been selected by the server for a snapshot read. This + * function returns false if the atClusterTime was specified by the client. + */ + bool wasAtClusterTimeSelected() const { + return _atClusterTimeSelected; } private: @@ -223,6 +233,8 @@ private: bool _specified; ReadWriteConcernProvenance _provenance; + + bool _atClusterTimeSelected = false; }; } // namespace repl diff --git a/src/mongo/db/service_entry_point_common.cpp b/src/mongo/db/service_entry_point_common.cpp index 55959347ec9..1e9f51a137a 100644 --- a/src/mongo/db/service_entry_point_common.cpp +++ b/src/mongo/db/service_entry_point_common.cpp @@ -615,13 +615,21 @@ void invokeWithSessionCheckedOut(OperationContext* opCtx, if (!opCtx->getClient()->isInDirectClient()) { const auto& readConcernArgs = repl::ReadConcernArgs::get(opCtx); + auto command = invocation->definition(); + // Record readConcern usages for commands run inside transactions after unstashing the + // transaction resources. + if (command->shouldAffectReadConcernCounter() && opCtx->inMultiDocumentTransaction()) { + ServerReadConcernMetrics::get(opCtx)->recordReadConcern(readConcernArgs, + true /* isTransaction */); + } + // For replica sets, we do not receive the readConcernArgs of our parent transaction // statements until we unstash the transaction resources. The below check is necessary to // ensure commands, including those occurring after the first statement in their respective // transactions, are checked for readConcern support. Presently, only `create` and // `createIndexes` do not support readConcern inside transactions. // TODO(SERVER-46971): Consider how to extend this check to other commands. - auto cmdName = invocation->definition()->getName(); + auto cmdName = command->getName(); auto readConcernSupport = invocation->supportsReadConcern(readConcernArgs.getLevel()); if (readConcernArgs.hasLevel() && (cmdName == "create"_sd || cmdName == "createIndexes"_sd)) { @@ -714,6 +722,15 @@ bool runCommandImpl(OperationContext* opCtx, const bool shouldWaitForWriteConcern = invocation->supportsWriteConcern() || command->getLogicalOp() == LogicalOp::opGetMore; + // Record readConcern usages for commands run outside of transactions, excluding DBDirectClient. + // For commands inside a transaction, they inherit the readConcern from the transaction. So we + // will record their readConcern usages after we have unstashed the transaction resources. + if (!opCtx->getClient()->isInDirectClient() && command->shouldAffectReadConcernCounter() && + !opCtx->inMultiDocumentTransaction()) { + ServerReadConcernMetrics::get(opCtx)->recordReadConcern(repl::ReadConcernArgs::get(opCtx), + false /* isTransaction */); + } + if (shouldWaitForWriteConcern) { auto lastOpBeforeRun = repl::ReplClientInfo::forClient(opCtx->getClient()).getLastOp(); @@ -1436,7 +1453,11 @@ DbResponse receivedQuery(OperationContext* opCtx, const ServiceEntryPointCommon::Hooks& behaviors) { invariant(!nss.isCommand()); globalOpCounters.gotQuery(); - ServerReadConcernMetrics::get(opCtx)->recordReadConcern(repl::ReadConcernArgs::get(opCtx)); + + if (!opCtx->getClient()->isInDirectClient()) { + ServerReadConcernMetrics::get(opCtx)->recordReadConcern(repl::ReadConcernArgs::get(opCtx), + false /* isTransaction */); + } DbMessage d(m); QueryMessage q(d); diff --git a/src/mongo/db/stats/read_concern_stats.idl b/src/mongo/db/stats/read_concern_stats.idl index 60636cf0c08..30ec3c47a05 100644 --- a/src/mongo/db/stats/read_concern_stats.idl +++ b/src/mongo/db/stats/read_concern_stats.idl @@ -38,26 +38,45 @@ imports: structs: - ReadConcernStats: - description: "A struct representing the section of the server status - command with information about readConcern levels used by operations" + SnapshotOps: + description: "A struct representing the number of operations used with snapshot readConcern" strict: true fields: - available: + withClusterTime: type: long default: 0 - linearizable: + withoutClusterTime: + type: long + default: 0 + + ReadConcernOps: + description: "A struct representing readConcern level usages by read operations" + strict: true + fields: + none: type: long default: 0 local: type: long default: 0 + available: + type: long + optional: true majority: type: long default: 0 snapshot: + type: SnapshotOps + linearizable: type: long - default: 0 - none: - type: long - default: 0 + optional: true + + ReadConcernStats: + description: "A struct representing the section of the server status + command with information about readConcern levels used by operations" + strict: true + fields: + nonTransactionOps: + type: ReadConcernOps + transactionOps: + type: ReadConcernOps diff --git a/src/mongo/db/stats/server_read_concern_metrics.cpp b/src/mongo/db/stats/server_read_concern_metrics.cpp index 1dd4c9fff8e..a29b7ab781a 100644 --- a/src/mongo/db/stats/server_read_concern_metrics.cpp +++ b/src/mongo/db/stats/server_read_concern_metrics.cpp @@ -50,31 +50,41 @@ ServerReadConcernMetrics* ServerReadConcernMetrics::get(OperationContext* opCtx) return get(opCtx->getServiceContext()); } -void ServerReadConcernMetrics::recordReadConcern(const repl::ReadConcernArgs& readConcernArgs) { +void ServerReadConcernMetrics::recordReadConcern(const repl::ReadConcernArgs& readConcernArgs, + bool isTransaction) { + auto& ops = isTransaction ? _transactionOps : _nonTransactionOps; + if (!readConcernArgs.hasLevel()) { - _noLevelCount.fetchAndAdd(1); + ops.noLevelCount.fetchAndAdd(1); return; } switch (readConcernArgs.getLevel()) { case repl::ReadConcernLevel::kAvailableReadConcern: - _levelAvailableCount.fetchAndAdd(1); + invariant(!isTransaction); + ops.levelAvailableCount.fetchAndAdd(1); break; case repl::ReadConcernLevel::kLinearizableReadConcern: - _levelLinearizableCount.fetchAndAdd(1); + invariant(!isTransaction); + ops.levelLinearizableCount.fetchAndAdd(1); break; case repl::ReadConcernLevel::kLocalReadConcern: - _levelLocalCount.fetchAndAdd(1); + ops.levelLocalCount.fetchAndAdd(1); break; case repl::ReadConcernLevel::kMajorityReadConcern: - _levelMajorityCount.fetchAndAdd(1); + ops.levelMajorityCount.fetchAndAdd(1); break; case repl::ReadConcernLevel::kSnapshotReadConcern: - _levelSnapshotCount.fetchAndAdd(1); + if (readConcernArgs.getArgsAtClusterTime() && + !readConcernArgs.wasAtClusterTimeSelected()) { + ops.atClusterTimeCount.fetchAndAdd(1); + } else { + ops.levelSnapshotCount.fetchAndAdd(1); + } break; default: @@ -83,20 +93,35 @@ void ServerReadConcernMetrics::recordReadConcern(const repl::ReadConcernArgs& re } void ServerReadConcernMetrics::updateStats(ReadConcernStats* stats, OperationContext* opCtx) { - stats->setAvailable(_levelAvailableCount.load()); - stats->setLinearizable(_levelLinearizableCount.load()); - stats->setLocal(_levelLocalCount.load()); - stats->setMajority(_levelMajorityCount.load()); - stats->setSnapshot(_levelSnapshotCount.load()); - stats->setNone(_noLevelCount.load()); + ReadConcernOps nonTransactionOps; + SnapshotOps nonTransactionSnapshotOps; + nonTransactionSnapshotOps.setWithoutClusterTime(_nonTransactionOps.levelSnapshotCount.load()); + nonTransactionSnapshotOps.setWithClusterTime(_nonTransactionOps.atClusterTimeCount.load()); + nonTransactionOps.setNone(_nonTransactionOps.noLevelCount.load()); + nonTransactionOps.setAvailable(_nonTransactionOps.levelAvailableCount.load()); + nonTransactionOps.setLinearizable(_nonTransactionOps.levelLinearizableCount.load()); + nonTransactionOps.setLocal(_nonTransactionOps.levelLocalCount.load()); + nonTransactionOps.setMajority(_nonTransactionOps.levelMajorityCount.load()); + nonTransactionOps.setSnapshot(nonTransactionSnapshotOps); + stats->setNonTransactionOps(nonTransactionOps); + + ReadConcernOps transactionOps; + SnapshotOps transactionSnapshotOps; + transactionSnapshotOps.setWithoutClusterTime(_transactionOps.levelSnapshotCount.load()); + transactionSnapshotOps.setWithClusterTime(_transactionOps.atClusterTimeCount.load()); + transactionOps.setNone(_transactionOps.noLevelCount.load()); + transactionOps.setLocal(_transactionOps.levelLocalCount.load()); + transactionOps.setMajority(_transactionOps.levelMajorityCount.load()); + transactionOps.setSnapshot(transactionSnapshotOps); + stats->setTransactionOps(transactionOps); } namespace { -class OpReadConcernCountersSSS : public ServerStatusSection { +class ReadConcernCountersSSS : public ServerStatusSection { public: - OpReadConcernCountersSSS() : ServerStatusSection("opReadConcernCounters") {} + ReadConcernCountersSSS() : ServerStatusSection("readConcernCounters") {} - ~OpReadConcernCountersSSS() override = default; + ~ReadConcernCountersSSS() override = default; bool includeByDefault() const override { return true; @@ -109,7 +134,7 @@ public: return stats.toBSON(); } -} opReadConcernCountersSSS; +} ReadConcernCountersSSS; } // namespace } // namespace mongo diff --git a/src/mongo/db/stats/server_read_concern_metrics.h b/src/mongo/db/stats/server_read_concern_metrics.h index 1a247f79df6..7782b959bfd 100644 --- a/src/mongo/db/stats/server_read_concern_metrics.h +++ b/src/mongo/db/stats/server_read_concern_metrics.h @@ -52,7 +52,7 @@ public: /** * Updates counter for the level of 'readConcernArgs'. */ - void recordReadConcern(const repl::ReadConcernArgs& readConcernArgs); + void recordReadConcern(const repl::ReadConcernArgs& readConcernArgs, bool isTransaction); /** * Appends the accumulated stats to a readConcern stats object. @@ -60,12 +60,17 @@ public: void updateStats(ReadConcernStats* stats, OperationContext* opCtx); private: - AtomicWord _levelAvailableCount{0}; - AtomicWord _levelLinearizableCount{0}; - AtomicWord _levelLocalCount{0}; - AtomicWord _levelMajorityCount{0}; - AtomicWord _levelSnapshotCount{0}; - AtomicWord _noLevelCount{0}; + struct readConcernCounters { + AtomicWord levelAvailableCount{0}; + AtomicWord levelLinearizableCount{0}; + AtomicWord levelLocalCount{0}; + AtomicWord levelMajorityCount{0}; + AtomicWord levelSnapshotCount{0}; + AtomicWord atClusterTimeCount{0}; + AtomicWord noLevelCount{0}; + }; + readConcernCounters _nonTransactionOps; + readConcernCounters _transactionOps; }; } // namespace mongo -- cgit v1.2.1