mirror of https://github.com/mongodb/mongo
SERVER-43720 Add default read write concern commands to mongos and config server
This commit is contained in:
parent
3eecd38739
commit
15d3feea93
|
|
@ -3755,6 +3755,16 @@ var authCommandsLib = {
|
||||||
{runOnDb: secondDbName, roles: {}}
|
{runOnDb: secondDbName, roles: {}}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
testname: "getDefaultRWConcern",
|
||||||
|
command: {getDefaultRWConcern: 1},
|
||||||
|
skipUnlessReplicaSet: true,
|
||||||
|
testcases: [
|
||||||
|
{runOnDb: adminDbName, roles: roles_all, privileges: []},
|
||||||
|
{runOnDb: firstDbName, roles: {}},
|
||||||
|
{runOnDb: secondDbName, roles: {}}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
testname: "getDiagnosticData",
|
testname: "getDiagnosticData",
|
||||||
command: {getDiagnosticData: 1},
|
command: {getDiagnosticData: 1},
|
||||||
|
|
@ -5093,6 +5103,16 @@ var authCommandsLib = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
testname: "setDefaultRWConcern",
|
||||||
|
command: {setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}},
|
||||||
|
skipUnlessReplicaSet: true,
|
||||||
|
testcases: [
|
||||||
|
{runOnDb: adminDbName, roles: roles_all, privileges: []},
|
||||||
|
{runOnDb: firstDbName, roles: {}},
|
||||||
|
{runOnDb: secondDbName, roles: {}}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
testname: "setFeatureCompatibilityVersion",
|
testname: "setFeatureCompatibilityVersion",
|
||||||
command: {setFeatureCompatibilityVersion: "x"},
|
command: {setFeatureCompatibilityVersion: "x"},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,326 @@
|
||||||
|
// Tests the basic API of the getDefaultRWConcern and setDefaultRWConcern commands against different
|
||||||
|
// topologies.
|
||||||
|
//
|
||||||
|
// @tags: [requires_fcv_44]
|
||||||
|
(function() {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Asserts a set/get default RWC command response contains the expected fields. Assumes a default
|
||||||
|
// read or write concern has been set previously.
|
||||||
|
function verifyResponseFields(res, {expectRC, expectWC}) {
|
||||||
|
// These fields are always set once a read or write concern has been set at least once.
|
||||||
|
const expectedFields = ["epoch", "setTime", "localSetTime"];
|
||||||
|
const unexpectedFields = [];
|
||||||
|
|
||||||
|
if (expectRC) {
|
||||||
|
expectedFields.push("defaultReadConcern");
|
||||||
|
} else {
|
||||||
|
unexpectedFields.push("defaultReadConcern");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectWC) {
|
||||||
|
expectedFields.push("defaultWriteConcern");
|
||||||
|
} else {
|
||||||
|
unexpectedFields.push("defaultWriteConcern");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.hasFields(res, expectedFields);
|
||||||
|
unexpectedFields.forEach(field => {
|
||||||
|
assert(!res.hasOwnProperty(field),
|
||||||
|
`response unexpectedly had field '${field}', res: ${tojson(res)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyDefaultRWCommandsInvalidInput(conn) {
|
||||||
|
//
|
||||||
|
// Test invalid parameters for getDefaultRWConcern.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Invalid inMemory.
|
||||||
|
assert.commandFailedWithCode(conn.adminCommand({getDefaultRWConcern: 1, inMemory: "true"}),
|
||||||
|
ErrorCodes.TypeMismatch);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Test invalid parameters for setDefaultRWConcern.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Must include either wc or rc.
|
||||||
|
assert.commandFailedWithCode(conn.adminCommand({setDefaultRWConcern: 1}), ErrorCodes.BadValue);
|
||||||
|
|
||||||
|
// Invalid write concern.
|
||||||
|
assert.commandFailedWithCode(
|
||||||
|
conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: 1}),
|
||||||
|
ErrorCodes.TypeMismatch);
|
||||||
|
|
||||||
|
// w less than 1.
|
||||||
|
assert.commandFailedWithCode(conn.adminCommand({
|
||||||
|
setDefaultRWConcern: 1,
|
||||||
|
defaultWriteConcern: {w: 0},
|
||||||
|
}),
|
||||||
|
ErrorCodes.BadValue);
|
||||||
|
|
||||||
|
// Invalid read concern.
|
||||||
|
assert.commandFailedWithCode(conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: 1}),
|
||||||
|
ErrorCodes.TypeMismatch);
|
||||||
|
|
||||||
|
// Non-existent level.
|
||||||
|
assert.commandFailedWithCode(
|
||||||
|
conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "dummy"}}),
|
||||||
|
ErrorCodes.FailedToParse);
|
||||||
|
|
||||||
|
// Unsupported level.
|
||||||
|
assert.commandFailedWithCode(
|
||||||
|
conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "linearizable"}}),
|
||||||
|
ErrorCodes.BadValue);
|
||||||
|
assert.commandFailedWithCode(
|
||||||
|
conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "snapshot"}}),
|
||||||
|
ErrorCodes.BadValue);
|
||||||
|
|
||||||
|
// Fields other than level.
|
||||||
|
assert.commandFailedWithCode(conn.adminCommand({
|
||||||
|
setDefaultRWConcern: 1,
|
||||||
|
defaultReadConcern: {level: "local", afterClusterTime: Timestamp(50, 1)}
|
||||||
|
}),
|
||||||
|
ErrorCodes.BadValue);
|
||||||
|
assert.commandFailedWithCode(conn.adminCommand({
|
||||||
|
setDefaultRWConcern: 1,
|
||||||
|
defaultReadConcern: {level: "snapshot", atClusterTime: Timestamp(50, 1)}
|
||||||
|
}),
|
||||||
|
ErrorCodes.BadValue);
|
||||||
|
assert.commandFailedWithCode(conn.adminCommand({
|
||||||
|
setDefaultRWConcern: 1,
|
||||||
|
defaultReadConcern: {level: "local", afterOpTime: {ts: Timestamp(50, 1), t: 1}}
|
||||||
|
}),
|
||||||
|
ErrorCodes.BadValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets a default read and write concern.
|
||||||
|
function setDefaultRWConcern(conn) {
|
||||||
|
assert.commandWorked(conn.adminCommand({
|
||||||
|
setDefaultRWConcern: 1,
|
||||||
|
defaultReadConcern: {level: "local"},
|
||||||
|
defaultWriteConcern: {w: 1}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsets the default read and write concerns.
|
||||||
|
function unsetDefaultRWConcern(conn) {
|
||||||
|
assert.commandWorked(conn.adminCommand(
|
||||||
|
{setDefaultRWConcern: 1, defaultReadConcern: {}, defaultWriteConcern: {}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifies no fields are returned if neither a default read nor write concern has been set.
|
||||||
|
function verifyDefaultResponses(conn) {
|
||||||
|
const res = assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1}));
|
||||||
|
const inMemoryRes =
|
||||||
|
assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: true}));
|
||||||
|
|
||||||
|
const unexpectedFields =
|
||||||
|
["defaultReadConcern", "defaultWriteConcern", "epoch", "setTime", "localSetTime"];
|
||||||
|
unexpectedFields.forEach(field => {
|
||||||
|
assert(!res.hasOwnProperty(field),
|
||||||
|
`response unexpectedly had field '${field}', res: ${tojson(res)}`);
|
||||||
|
assert(!inMemoryRes.hasOwnProperty(field),
|
||||||
|
`inMemory=true response unexpectedly had field '${field}', res: ${tojson(res)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyDefaultRWCommandsValidInput(conn) {
|
||||||
|
//
|
||||||
|
// Test parameters for getDefaultRWConcern.
|
||||||
|
//
|
||||||
|
|
||||||
|
// No parameters is allowed.
|
||||||
|
assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1}));
|
||||||
|
|
||||||
|
// inMemory parameter is allowed.
|
||||||
|
assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: true}));
|
||||||
|
assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: false}));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Test parameters for setDefaultRWConcern.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Setting only rc is allowed.
|
||||||
|
assert.commandWorked(
|
||||||
|
conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}}));
|
||||||
|
assert.commandWorked(
|
||||||
|
conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "majority"}}));
|
||||||
|
|
||||||
|
// Setting only wc is allowed.
|
||||||
|
assert.commandWorked(conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {w: 1}}));
|
||||||
|
assert.commandWorked(
|
||||||
|
conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {w: 1, j: false}}));
|
||||||
|
assert.commandWorked(
|
||||||
|
conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {w: "majority"}}));
|
||||||
|
|
||||||
|
// Setting both wc and rc is allowed.
|
||||||
|
assert.commandWorked(conn.adminCommand({
|
||||||
|
setDefaultRWConcern: 1,
|
||||||
|
defaultWriteConcern: {w: 1},
|
||||||
|
defaultReadConcern: {level: "local"}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Empty write concern is allowed.
|
||||||
|
assert.commandWorked(conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {}}));
|
||||||
|
|
||||||
|
// Empty read concern is allowed.
|
||||||
|
assert.commandWorked(conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyDefaultRWCommandsSuccessfulResponses(conn) {
|
||||||
|
//
|
||||||
|
// Test responses for getDefaultRWConcern.
|
||||||
|
//
|
||||||
|
|
||||||
|
// When neither read nor write concern is set.
|
||||||
|
unsetDefaultRWConcern(conn);
|
||||||
|
verifyResponseFields(assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1})),
|
||||||
|
{expectRC: false, expectWC: false});
|
||||||
|
verifyResponseFields(
|
||||||
|
assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: true})),
|
||||||
|
{expectRC: false, expectWC: false});
|
||||||
|
|
||||||
|
// When only read concern is set.
|
||||||
|
assert.commandWorked(conn.adminCommand(
|
||||||
|
{setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}, defaultWriteConcern: {}}));
|
||||||
|
verifyResponseFields(assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1})),
|
||||||
|
{expectRC: true, expectWC: false});
|
||||||
|
verifyResponseFields(
|
||||||
|
assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: true})),
|
||||||
|
{expectRC: true, expectWC: false});
|
||||||
|
|
||||||
|
// When only write concern is set.
|
||||||
|
assert.commandWorked(conn.adminCommand(
|
||||||
|
{setDefaultRWConcern: 1, defaultReadConcern: {}, defaultWriteConcern: {w: 1}}));
|
||||||
|
verifyResponseFields(assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1})),
|
||||||
|
{expectRC: false, expectWC: true});
|
||||||
|
verifyResponseFields(
|
||||||
|
assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: true})),
|
||||||
|
{expectRC: false, expectWC: true});
|
||||||
|
|
||||||
|
// When both read and write concern are set.
|
||||||
|
assert.commandWorked(conn.adminCommand({
|
||||||
|
setDefaultRWConcern: 1,
|
||||||
|
defaultReadConcern: {level: "local"},
|
||||||
|
defaultWriteConcern: {w: 1}
|
||||||
|
}));
|
||||||
|
verifyResponseFields(assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1})),
|
||||||
|
{expectRC: true, expectWC: true});
|
||||||
|
verifyResponseFields(
|
||||||
|
assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: true})),
|
||||||
|
{expectRC: true, expectWC: true});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Test responses for setDefaultRWConcern.
|
||||||
|
//
|
||||||
|
|
||||||
|
// When unsetting both read and write concern.
|
||||||
|
setDefaultRWConcern(conn);
|
||||||
|
verifyResponseFields(
|
||||||
|
assert.commandWorked(conn.adminCommand(
|
||||||
|
{setDefaultRWConcern: 1, defaultReadConcern: {}, defaultWriteConcern: {}})),
|
||||||
|
{expectRC: false, expectWC: false});
|
||||||
|
|
||||||
|
// When unsetting only read concern.
|
||||||
|
setDefaultRWConcern(conn);
|
||||||
|
verifyResponseFields(
|
||||||
|
assert.commandWorked(conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {}})),
|
||||||
|
{expectRC: false, expectWC: true});
|
||||||
|
|
||||||
|
// When unsetting only write concern.
|
||||||
|
setDefaultRWConcern(conn);
|
||||||
|
verifyResponseFields(
|
||||||
|
assert.commandWorked(conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {}})),
|
||||||
|
{expectRC: true, expectWC: false});
|
||||||
|
|
||||||
|
// When setting only write concern.
|
||||||
|
unsetDefaultRWConcern(conn);
|
||||||
|
verifyResponseFields(assert.commandWorked(conn.adminCommand(
|
||||||
|
{setDefaultRWConcern: 1, defaultWriteConcern: {w: 1}})),
|
||||||
|
{expectRC: false, expectWC: true});
|
||||||
|
|
||||||
|
// When setting only read concern.
|
||||||
|
unsetDefaultRWConcern(conn);
|
||||||
|
verifyResponseFields(assert.commandWorked(conn.adminCommand(
|
||||||
|
{setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}})),
|
||||||
|
{expectRC: true, expectWC: false});
|
||||||
|
|
||||||
|
// When setting both read and write concern.
|
||||||
|
unsetDefaultRWConcern(conn);
|
||||||
|
verifyResponseFields(assert.commandWorked(conn.adminCommand({
|
||||||
|
setDefaultRWConcern: 1,
|
||||||
|
defaultReadConcern: {level: "local"},
|
||||||
|
defaultWriteConcern: {w: 1}
|
||||||
|
})),
|
||||||
|
{expectRC: true, expectWC: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifies the error code returned by connections to nodes that do not support the get/set default
|
||||||
|
// rw concern commands.
|
||||||
|
function verifyDefaultRWCommandsFailWithCode(conn, {failureCode}) {
|
||||||
|
assert.commandFailedWithCode(conn.adminCommand({getDefaultRWConcern: 1}), failureCode);
|
||||||
|
assert.commandFailedWithCode(
|
||||||
|
conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}}),
|
||||||
|
failureCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsTestLog("Testing standalone mongod...");
|
||||||
|
{
|
||||||
|
const standalone = MongoRunner.runMongod();
|
||||||
|
|
||||||
|
// Standalone node fails.
|
||||||
|
verifyDefaultRWCommandsFailWithCode(standalone, {failureCode: 51300});
|
||||||
|
|
||||||
|
MongoRunner.stopMongod(standalone);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsTestLog("Testing standalone replica set...");
|
||||||
|
{
|
||||||
|
const rst = new ReplSetTest({nodes: 2});
|
||||||
|
rst.startSet();
|
||||||
|
rst.initiate();
|
||||||
|
|
||||||
|
// Primary succeeds.
|
||||||
|
verifyDefaultResponses(rst.getPrimary());
|
||||||
|
verifyDefaultRWCommandsValidInput(rst.getPrimary());
|
||||||
|
verifyDefaultRWCommandsInvalidInput(rst.getPrimary());
|
||||||
|
verifyDefaultRWCommandsSuccessfulResponses(rst.getPrimary());
|
||||||
|
|
||||||
|
// Secondary succeeds.
|
||||||
|
assert.commandWorked(rst.getSecondary().adminCommand({getDefaultRWConcern: 1}));
|
||||||
|
// TODO SERVER-44890 Assert setDefaultRWConcern fails with NotMaster instead.
|
||||||
|
assert.commandWorked(rst.getSecondary().adminCommand(
|
||||||
|
{setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}}));
|
||||||
|
|
||||||
|
rst.stopSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
jsTestLog("Testing sharded cluster...");
|
||||||
|
{
|
||||||
|
const st = new ShardingTest({shards: 1, rs: {nodes: 2}});
|
||||||
|
|
||||||
|
// Mongos succeeds.
|
||||||
|
verifyDefaultResponses(st.s);
|
||||||
|
verifyDefaultRWCommandsValidInput(st.s);
|
||||||
|
verifyDefaultRWCommandsInvalidInput(st.s);
|
||||||
|
verifyDefaultRWCommandsSuccessfulResponses(st.s);
|
||||||
|
|
||||||
|
// Shard node fails.
|
||||||
|
verifyDefaultRWCommandsFailWithCode(st.rs0.getPrimary(), {failureCode: 51301});
|
||||||
|
verifyDefaultRWCommandsFailWithCode(st.rs0.getSecondary(), {failureCode: 51301});
|
||||||
|
|
||||||
|
// Config server primary succeeds.
|
||||||
|
verifyDefaultRWCommandsValidInput(st.configRS.getPrimary());
|
||||||
|
verifyDefaultRWCommandsInvalidInput(st.configRS.getPrimary());
|
||||||
|
verifyDefaultRWCommandsSuccessfulResponses(st.configRS.getPrimary());
|
||||||
|
|
||||||
|
// Config server secondary succeeds.
|
||||||
|
assert.commandWorked(st.configRS.getSecondary().adminCommand({getDefaultRWConcern: 1}));
|
||||||
|
// TODO SERVER-44890 Assert setDefaultRWConcern fails instead.
|
||||||
|
assert.commandWorked(st.configRS.getSecondary().adminCommand(
|
||||||
|
{setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}}));
|
||||||
|
|
||||||
|
st.stop();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -142,7 +142,6 @@ env.Library(
|
||||||
'logical_session_server_status_section.cpp',
|
'logical_session_server_status_section.cpp',
|
||||||
'mr_common.cpp',
|
'mr_common.cpp',
|
||||||
'reap_logical_session_cache_now.cpp',
|
'reap_logical_session_cache_now.cpp',
|
||||||
'rwc_defaults_commands.cpp',
|
|
||||||
'traffic_recording_cmds.cpp',
|
'traffic_recording_cmds.cpp',
|
||||||
'user_management_commands_common.cpp',
|
'user_management_commands_common.cpp',
|
||||||
env.Idlc('drop_connections.idl')[0],
|
env.Idlc('drop_connections.idl')[0],
|
||||||
|
|
@ -369,6 +368,7 @@ env.Library(
|
||||||
"oplog_note.cpp",
|
"oplog_note.cpp",
|
||||||
"resize_oplog.cpp",
|
"resize_oplog.cpp",
|
||||||
"restart_catalog_command.cpp",
|
"restart_catalog_command.cpp",
|
||||||
|
'rwc_defaults_commands.cpp',
|
||||||
"set_feature_compatibility_version_command.cpp",
|
"set_feature_compatibility_version_command.cpp",
|
||||||
"set_index_commit_quorum_command.cpp",
|
"set_index_commit_quorum_command.cpp",
|
||||||
"shutdown_d.cpp",
|
"shutdown_d.cpp",
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,24 @@
|
||||||
#include "mongo/db/commands.h"
|
#include "mongo/db/commands.h"
|
||||||
#include "mongo/db/commands/rwc_defaults_commands_gen.h"
|
#include "mongo/db/commands/rwc_defaults_commands_gen.h"
|
||||||
#include "mongo/db/read_write_concern_defaults.h"
|
#include "mongo/db/read_write_concern_defaults.h"
|
||||||
|
#include "mongo/db/repl/read_concern_args.h"
|
||||||
|
#include "mongo/db/repl/replication_coordinator.h"
|
||||||
#include "mongo/util/log.h"
|
#include "mongo/util/log.h"
|
||||||
|
|
||||||
namespace mongo {
|
namespace mongo {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
void assertNotStandaloneOrShardServer(OperationContext* opCtx, StringData cmdName) {
|
||||||
|
const auto replCoord = repl::ReplicationCoordinator::get(opCtx);
|
||||||
|
uassert(51300,
|
||||||
|
str::stream() << "'" << cmdName << "' is not supported on standalone nodes.",
|
||||||
|
replCoord->isReplEnabled());
|
||||||
|
|
||||||
|
uassert(51301,
|
||||||
|
str::stream() << "'" << cmdName << "' is not supported on shard nodes.",
|
||||||
|
serverGlobalParams.clusterRole != ClusterRole::ShardServer);
|
||||||
|
}
|
||||||
|
|
||||||
class SetDefaultRWConcernCommand : public TypedCommand<SetDefaultRWConcernCommand> {
|
class SetDefaultRWConcernCommand : public TypedCommand<SetDefaultRWConcernCommand> {
|
||||||
public:
|
public:
|
||||||
AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
|
AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
|
||||||
|
|
@ -62,17 +75,11 @@ public:
|
||||||
using InvocationBase::InvocationBase;
|
using InvocationBase::InvocationBase;
|
||||||
|
|
||||||
auto typedRun(OperationContext* opCtx) {
|
auto typedRun(OperationContext* opCtx) {
|
||||||
auto rc = request().getDefaultReadConcern();
|
assertNotStandaloneOrShardServer(opCtx, SetDefaultRWConcern::kCommandName);
|
||||||
auto wc = request().getDefaultWriteConcern();
|
|
||||||
uassert(ErrorCodes::BadValue,
|
|
||||||
str::stream() << "At least one of the \""
|
|
||||||
<< SetDefaultRWConcern::kDefaultReadConcernFieldName << "\" or \""
|
|
||||||
<< SetDefaultRWConcern::kDefaultWriteConcernFieldName
|
|
||||||
<< "\" fields must be present",
|
|
||||||
rc || wc);
|
|
||||||
|
|
||||||
auto& rwcDefaults = ReadWriteConcernDefaults::get(opCtx->getServiceContext());
|
auto& rwcDefaults = ReadWriteConcernDefaults::get(opCtx->getServiceContext());
|
||||||
auto newDefaults = rwcDefaults.setConcerns(opCtx, rc, wc);
|
auto newDefaults = rwcDefaults.setConcerns(
|
||||||
|
opCtx, request().getDefaultReadConcern(), request().getDefaultWriteConcern());
|
||||||
log() << "successfully set RWC defaults to " << newDefaults.toBSON();
|
log() << "successfully set RWC defaults to " << newDefaults.toBSON();
|
||||||
return newDefaults;
|
return newDefaults;
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +90,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
void doCheckAuthorization(OperationContext*) const override {
|
void doCheckAuthorization(OperationContext*) const override {
|
||||||
// TODO: add and use privilege action
|
// TODO SERVER-45038: add and use privilege action
|
||||||
}
|
}
|
||||||
|
|
||||||
NamespaceString ns() const override {
|
NamespaceString ns() const override {
|
||||||
|
|
@ -112,6 +119,10 @@ public:
|
||||||
using InvocationBase::InvocationBase;
|
using InvocationBase::InvocationBase;
|
||||||
|
|
||||||
auto typedRun(OperationContext* opCtx) {
|
auto typedRun(OperationContext* opCtx) {
|
||||||
|
assertNotStandaloneOrShardServer(opCtx, GetDefaultRWConcern::kCommandName);
|
||||||
|
|
||||||
|
// TODO SERVER-43720 Implement inMemory option.
|
||||||
|
|
||||||
auto& rwcDefaults = ReadWriteConcernDefaults::get(opCtx->getServiceContext());
|
auto& rwcDefaults = ReadWriteConcernDefaults::get(opCtx->getServiceContext());
|
||||||
return rwcDefaults.getDefault(opCtx);
|
return rwcDefaults.getDefault(opCtx);
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +133,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
void doCheckAuthorization(OperationContext*) const override {
|
void doCheckAuthorization(OperationContext*) const override {
|
||||||
// TODO: add and use privilege action
|
// TODO SERVER-45038: add and use privilege action
|
||||||
}
|
}
|
||||||
|
|
||||||
NamespaceString ns() const override {
|
NamespaceString ns() const override {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ imports:
|
||||||
- "mongo/db/repl/read_concern_args.idl"
|
- "mongo/db/repl/read_concern_args.idl"
|
||||||
- "mongo/db/write_concern_options.idl"
|
- "mongo/db/write_concern_options.idl"
|
||||||
|
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
setDefaultRWConcern:
|
setDefaultRWConcern:
|
||||||
description: "set the current read/write concern defaults (cluster-wide)"
|
description: "set the current read/write concern defaults (cluster-wide)"
|
||||||
|
|
@ -52,3 +51,8 @@ commands:
|
||||||
getDefaultRWConcern:
|
getDefaultRWConcern:
|
||||||
description: "get the current read/write concern defaults being applied by this node"
|
description: "get the current read/write concern defaults being applied by this node"
|
||||||
namespace: ignored
|
namespace: ignored
|
||||||
|
fields:
|
||||||
|
inMemory:
|
||||||
|
type: bool
|
||||||
|
description: "If true, return the locally cached read/write concern defaults"
|
||||||
|
optional: true
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,12 @@ void ReadWriteConcernDefaults::_setDefault(RWConcernDefault&& rwc) {
|
||||||
RWConcernDefault ReadWriteConcernDefaults::setConcerns(OperationContext* opCtx,
|
RWConcernDefault ReadWriteConcernDefaults::setConcerns(OperationContext* opCtx,
|
||||||
const boost::optional<ReadConcern>& rc,
|
const boost::optional<ReadConcern>& rc,
|
||||||
const boost::optional<WriteConcern>& wc) {
|
const boost::optional<WriteConcern>& wc) {
|
||||||
invariant(rc || wc);
|
uassert(ErrorCodes::BadValue,
|
||||||
|
str::stream() << "At least one of the \""
|
||||||
|
<< RWConcernDefault::kDefaultReadConcernFieldName << "\" or \""
|
||||||
|
<< RWConcernDefault::kDefaultWriteConcernFieldName
|
||||||
|
<< "\" fields must be present",
|
||||||
|
rc || wc);
|
||||||
|
|
||||||
if (rc) {
|
if (rc) {
|
||||||
checkSuitabilityAsDefault(*rc);
|
checkSuitabilityAsDefault(*rc);
|
||||||
|
|
@ -96,8 +101,12 @@ RWConcernDefault ReadWriteConcernDefaults::setConcerns(OperationContext* opCtx,
|
||||||
auto epoch = LogicalClock::get(opCtx->getServiceContext())->getClusterTime().asTimestamp();
|
auto epoch = LogicalClock::get(opCtx->getServiceContext())->getClusterTime().asTimestamp();
|
||||||
|
|
||||||
RWConcernDefault rwc;
|
RWConcernDefault rwc;
|
||||||
rwc.setDefaultReadConcern(rc);
|
if (rc && !rc->isEmpty()) {
|
||||||
rwc.setDefaultWriteConcern(wc);
|
rwc.setDefaultReadConcern(rc);
|
||||||
|
}
|
||||||
|
if (wc && !wc->usedDefaultW) {
|
||||||
|
rwc.setDefaultWriteConcern(wc);
|
||||||
|
}
|
||||||
rwc.setEpoch(epoch);
|
rwc.setEpoch(epoch);
|
||||||
rwc.setSetTime(now);
|
rwc.setSetTime(now);
|
||||||
rwc.setLocalSetTime(now);
|
rwc.setLocalSetTime(now);
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ public:
|
||||||
* Interface when an admin has run the command to change the defaults.
|
* Interface when an admin has run the command to change the defaults.
|
||||||
* At least one of the `rc` or `wc` params must be set.
|
* At least one of the `rc` or `wc` params must be set.
|
||||||
* Will generate and use a new epoch and setTime for the updated defaults, which are returned.
|
* Will generate and use a new epoch and setTime for the updated defaults, which are returned.
|
||||||
|
* Validates the supplied read and write concerns can serve as defaults.
|
||||||
*/
|
*/
|
||||||
RWConcernDefault setConcerns(OperationContext* opCtx,
|
RWConcernDefault setConcerns(OperationContext* opCtx,
|
||||||
const boost::optional<ReadConcern>& rc,
|
const boost::optional<ReadConcern>& rc,
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,12 @@ BSONObj ReadConcernArgs::toBSON() const {
|
||||||
return bob.obj();
|
return bob.obj();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BSONObj ReadConcernArgs::toBSONInner() const {
|
||||||
|
BSONObjBuilder bob;
|
||||||
|
_appendInfoInner(&bob);
|
||||||
|
return bob.obj();
|
||||||
|
}
|
||||||
|
|
||||||
bool ReadConcernArgs::isEmpty() const {
|
bool ReadConcernArgs::isEmpty() const {
|
||||||
return !_afterClusterTime && !_opTime && !_atClusterTime && !_level;
|
return !_afterClusterTime && !_opTime && !_atClusterTime && !_level;
|
||||||
}
|
}
|
||||||
|
|
@ -261,25 +267,27 @@ bool ReadConcernArgs::isSpeculativeMajority() const {
|
||||||
_majorityReadMechanism == MajorityReadMechanism::kSpeculative;
|
_majorityReadMechanism == MajorityReadMechanism::kSpeculative;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReadConcernArgs::appendInfo(BSONObjBuilder* builder) const {
|
void ReadConcernArgs::_appendInfoInner(BSONObjBuilder* builder) const {
|
||||||
BSONObjBuilder rcBuilder(builder->subobjStart(kReadConcernFieldName));
|
|
||||||
|
|
||||||
if (_level) {
|
if (_level) {
|
||||||
rcBuilder.append(kLevelFieldName, readConcernLevels::toString(_level.get()));
|
builder->append(kLevelFieldName, readConcernLevels::toString(_level.get()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_opTime) {
|
if (_opTime) {
|
||||||
_opTime->append(&rcBuilder, kAfterOpTimeFieldName.toString());
|
_opTime->append(builder, kAfterOpTimeFieldName.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_afterClusterTime) {
|
if (_afterClusterTime) {
|
||||||
rcBuilder.append(kAfterClusterTimeFieldName, _afterClusterTime->asTimestamp());
|
builder->append(kAfterClusterTimeFieldName, _afterClusterTime->asTimestamp());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_atClusterTime) {
|
if (_atClusterTime) {
|
||||||
rcBuilder.append(kAtClusterTimeFieldName, _atClusterTime->asTimestamp());
|
builder->append(kAtClusterTimeFieldName, _atClusterTime->asTimestamp());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReadConcernArgs::appendInfo(BSONObjBuilder* builder) const {
|
||||||
|
BSONObjBuilder rcBuilder(builder->subobjStart(kReadConcernFieldName));
|
||||||
|
_appendInfoInner(&rcBuilder);
|
||||||
rcBuilder.done();
|
rcBuilder.done();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ public:
|
||||||
bool isSpeculativeMajority() const;
|
bool isSpeculativeMajority() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends level and afterOpTime.
|
* Appends level, afterOpTime, and any other sub-fields in a 'readConcern' sub-object.
|
||||||
*/
|
*/
|
||||||
void appendInfo(BSONObjBuilder* builder) const;
|
void appendInfo(BSONObjBuilder* builder) const;
|
||||||
|
|
||||||
|
|
@ -165,9 +165,15 @@ public:
|
||||||
|
|
||||||
boost::optional<LogicalTime> getArgsAtClusterTime() const;
|
boost::optional<LogicalTime> getArgsAtClusterTime() const;
|
||||||
BSONObj toBSON() const;
|
BSONObj toBSON() const;
|
||||||
|
BSONObj toBSONInner() const;
|
||||||
std::string toString() const;
|
std::string toString() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/**
|
||||||
|
* Appends level, afterOpTime, and the other "inner" fields of the read concern args.
|
||||||
|
*/
|
||||||
|
void _appendInfoInner(BSONObjBuilder* builder) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read data after the OpTime of an operation on this replica set. Deprecated.
|
* Read data after the OpTime of an operation on this replica set. Deprecated.
|
||||||
* The only user is for read-after-optime calls using the config server optime.
|
* The only user is for read-after-optime calls using the config server optime.
|
||||||
|
|
|
||||||
|
|
@ -40,5 +40,5 @@ types:
|
||||||
description: "An object representing a read concern."
|
description: "An object representing a read concern."
|
||||||
bson_serialization_type: object
|
bson_serialization_type: object
|
||||||
cpp_type: "mongo::repl::ReadConcernArgs"
|
cpp_type: "mongo::repl::ReadConcernArgs"
|
||||||
serializer: "mongo::repl::ReadConcernArgs::toBSON"
|
serializer: "mongo::repl::ReadConcernArgs::toBSONInner"
|
||||||
deserializer: "mongo::repl::ReadConcernArgs::fromBSONThrows"
|
deserializer: "mongo::repl::ReadConcernArgs::fromBSONThrows"
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ imports:
|
||||||
structs:
|
structs:
|
||||||
RWConcernDefault:
|
RWConcernDefault:
|
||||||
description: "Represents a set of read/write concern defaults, and associated metadata"
|
description: "Represents a set of read/write concern defaults, and associated metadata"
|
||||||
|
strict: false
|
||||||
fields:
|
fields:
|
||||||
defaultReadConcern:
|
defaultReadConcern:
|
||||||
description: "The default read concern"
|
description: "The default read concern"
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ env.Library(
|
||||||
'cluster_repl_set_get_status_cmd.cpp',
|
'cluster_repl_set_get_status_cmd.cpp',
|
||||||
'cluster_reset_error_cmd.cpp',
|
'cluster_reset_error_cmd.cpp',
|
||||||
'cluster_restart_catalog_command.cpp',
|
'cluster_restart_catalog_command.cpp',
|
||||||
|
'cluster_rwc_defaults_commands.cpp',
|
||||||
'cluster_set_index_commit_quorum_cmd.cpp',
|
'cluster_set_index_commit_quorum_cmd.cpp',
|
||||||
'cluster_set_feature_compatibility_version_cmd.cpp',
|
'cluster_set_feature_compatibility_version_cmd.cpp',
|
||||||
'cluster_set_free_monitoring.cpp' if get_option("enable-free-mon") == 'on' else [],
|
'cluster_set_free_monitoring.cpp' if get_option("enable-free-mon") == 'on' else [],
|
||||||
|
|
@ -116,6 +117,7 @@ env.Library(
|
||||||
'$BUILD_DIR/mongo/db/ftdc/ftdc_server',
|
'$BUILD_DIR/mongo/db/ftdc/ftdc_server',
|
||||||
'$BUILD_DIR/mongo/db/shared_request_handling',
|
'$BUILD_DIR/mongo/db/shared_request_handling',
|
||||||
'$BUILD_DIR/mongo/db/logical_session_cache_impl',
|
'$BUILD_DIR/mongo/db/logical_session_cache_impl',
|
||||||
|
'$BUILD_DIR/mongo/db/read_write_concern_defaults',
|
||||||
'$BUILD_DIR/mongo/db/pipeline/aggregation',
|
'$BUILD_DIR/mongo/db/pipeline/aggregation',
|
||||||
'$BUILD_DIR/mongo/db/query/command_request_response',
|
'$BUILD_DIR/mongo/db/query/command_request_response',
|
||||||
'$BUILD_DIR/mongo/db/query/map_reduce_output_format',
|
'$BUILD_DIR/mongo/db/query/map_reduce_output_format',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2019-present MongoDB, Inc.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the Server Side Public License, version 1,
|
||||||
|
* as published by MongoDB, Inc.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* Server Side Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the Server Side Public License
|
||||||
|
* along with this program. If not, see
|
||||||
|
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||||
|
*
|
||||||
|
* As a special exception, the copyright holders give permission to link the
|
||||||
|
* code of portions of this program with the OpenSSL library under certain
|
||||||
|
* conditions as described in each individual source file and distribute
|
||||||
|
* linked combinations including the program with the OpenSSL library. You
|
||||||
|
* must comply with the Server Side Public License in all respects for
|
||||||
|
* all of the code used other than as permitted herein. If you modify file(s)
|
||||||
|
* with this exception, you may extend this exception to your version of the
|
||||||
|
* file(s), but you are not obligated to do so. If you do not wish to do so,
|
||||||
|
* delete this exception statement from your version. If you delete this
|
||||||
|
* exception statement from all source files in the program, then also delete
|
||||||
|
* it in the license file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kCommand
|
||||||
|
|
||||||
|
#include "mongo/platform/basic.h"
|
||||||
|
|
||||||
|
#include "mongo/db/auth/authorization_session.h"
|
||||||
|
#include "mongo/db/commands.h"
|
||||||
|
#include "mongo/db/commands/rwc_defaults_commands_gen.h"
|
||||||
|
#include "mongo/db/namespace_string.h"
|
||||||
|
#include "mongo/db/read_write_concern_defaults.h"
|
||||||
|
#include "mongo/db/rw_concern_default_gen.h"
|
||||||
|
#include "mongo/s/cluster_commands_helpers.h"
|
||||||
|
#include "mongo/s/grid.h"
|
||||||
|
#include "mongo/util/log.h"
|
||||||
|
|
||||||
|
namespace mongo {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the setDefaultRWConcern command on mongos. Inherits from BasicCommand because this
|
||||||
|
* command forwards the user's request to the config server and does not need to parse it.
|
||||||
|
*/
|
||||||
|
class ClusterSetDefaultRWConcernCommand : public BasicCommand {
|
||||||
|
public:
|
||||||
|
ClusterSetDefaultRWConcernCommand() : BasicCommand("setDefaultRWConcern") {}
|
||||||
|
|
||||||
|
bool run(OperationContext* opCtx,
|
||||||
|
const std::string& dbName,
|
||||||
|
const BSONObj& cmdObj,
|
||||||
|
BSONObjBuilder& result) override {
|
||||||
|
auto configShard = Grid::get(opCtx)->shardRegistry()->getConfigShard();
|
||||||
|
auto cmdResponse = uassertStatusOK(configShard->runCommandWithFixedRetryAttempts(
|
||||||
|
opCtx,
|
||||||
|
ReadPreferenceSetting(ReadPreference::PrimaryOnly),
|
||||||
|
NamespaceString::kAdminDb.toString(),
|
||||||
|
CommandHelpers::appendMajorityWriteConcern(
|
||||||
|
CommandHelpers::filterCommandRequestForPassthrough(cmdObj)),
|
||||||
|
Shard::RetryPolicy::kNotIdempotent));
|
||||||
|
|
||||||
|
uassertStatusOK(cmdResponse.commandStatus);
|
||||||
|
uassertStatusOK(cmdResponse.writeConcernStatus);
|
||||||
|
|
||||||
|
CommandHelpers::filterCommandReplyForPassthrough(cmdResponse.response, &result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool supportsWriteConcern(const BSONObj& cmd) const override {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status checkAuthForOperation(OperationContext* opCtx,
|
||||||
|
const std::string& dbname,
|
||||||
|
const BSONObj& cmdObj) const override {
|
||||||
|
// TODO SERVER-45038: add and use privilege action
|
||||||
|
return Status::OK();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string help() const override {
|
||||||
|
return "Sets the default read or write concern for a cluster";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool adminOnly() const override {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
|
||||||
|
return AllowedOnSecondary::kNever;
|
||||||
|
}
|
||||||
|
} clusterSetDefaultRWConcernCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the getDefaultRWConcern command on mongos.
|
||||||
|
*/
|
||||||
|
class ClusterGetDefaultRWConcernCommand final
|
||||||
|
: public TypedCommand<ClusterGetDefaultRWConcernCommand> {
|
||||||
|
public:
|
||||||
|
using Request = GetDefaultRWConcern;
|
||||||
|
using Response = RWConcernDefault;
|
||||||
|
|
||||||
|
class Invocation final : public InvocationBase {
|
||||||
|
public:
|
||||||
|
using InvocationBase::InvocationBase;
|
||||||
|
|
||||||
|
Response typedRun(OperationContext* opCtx) {
|
||||||
|
// TODO SERVER-43720 Implement inMemory option.
|
||||||
|
|
||||||
|
GetDefaultRWConcern configsvrRequest;
|
||||||
|
configsvrRequest.setDbName(request().getDbName());
|
||||||
|
|
||||||
|
auto configShard = Grid::get(opCtx)->shardRegistry()->getConfigShard();
|
||||||
|
auto cmdResponse = uassertStatusOK(configShard->runCommandWithFixedRetryAttempts(
|
||||||
|
opCtx,
|
||||||
|
ReadPreferenceSetting(ReadPreference::PrimaryOnly),
|
||||||
|
NamespaceString::kAdminDb.toString(),
|
||||||
|
applyReadWriteConcern(opCtx, this, configsvrRequest.toBSON({})),
|
||||||
|
Shard::RetryPolicy::kIdempotent));
|
||||||
|
|
||||||
|
uassertStatusOK(cmdResponse.commandStatus);
|
||||||
|
|
||||||
|
return Response::parse(IDLParserErrorContext("ClusterGetDefaultRWConcernResponse"),
|
||||||
|
cmdResponse.response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool supportsWriteConcern() const override {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void doCheckAuthorization(OperationContext*) const override {
|
||||||
|
// TODO SERVER-45038: add and use privilege action
|
||||||
|
}
|
||||||
|
|
||||||
|
NamespaceString ns() const override {
|
||||||
|
return NamespaceString(request().getDbName(), "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string help() const override {
|
||||||
|
return "Gets the default read or write concern for a cluster";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool adminOnly() const override {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
|
||||||
|
return AllowedOnSecondary::kNever;
|
||||||
|
}
|
||||||
|
} clusterGetDefaultRWConcernCommand;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace mongo
|
||||||
|
|
@ -284,7 +284,7 @@ assert = (function() {
|
||||||
|
|
||||||
if (count != arr.length) {
|
if (count != arr.length) {
|
||||||
doassert(_buildAssertionMessage(
|
doassert(_buildAssertionMessage(
|
||||||
msg, "None of values from " + tojson(arr) + " was in " + tojson(result)));
|
msg, "Not all of the values from " + tojson(arr) + " were in " + tojson(result)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue