diff options
| author | Apollon Oikonomopoulos <apoikos@debian.org> | 2018-03-22 12:04:55 +0200 |
|---|---|---|
| committer | Apollon Oikonomopoulos <apoikos@debian.org> | 2018-03-22 12:04:55 +0200 |
| commit | c49e99631589113663b1a3ac691870421965a315 (patch) | |
| tree | ac8127a5a6816f169a6656511bf131a35af2f74a | |
| parent | d982a88efa79f510c03f1c6c8c63360680ebbf88 (diff) | |
New upstream version 3.4.14upstream/3.4.14
682 files changed, 16256 insertions, 5608 deletions
@@ -4,23 +4,26 @@ Welcome to MongoDB 3.4! COMPONENTS
- mongod - The database process.
- mongos - Sharding controller.
+ mongod - The database server.
+ mongos - Sharding router.
mongo - The database shell (uses interactive javascript).
UTILITIES
- mongodump - MongoDB dump tool - for backups, snapshots, etc.
- mongorestore - MongoDB restore a dump
- mongoexport - Export a single collection to JSON or CSV
- mongoimport - Import from JSON or CSV
- mongofiles - Utility for putting and getting files from MongoDB GridFS
- mongostat - Show performance statistics
- mongoreplay - Workload capture and analysis tool
+ mongodump - Create a binary dump of the contents of a database.
+ mongorestore - Restore data from the output created by mongodump.
+ mongoexport - Export the contents of a collection to JSON or CSV.
+ mongoimport - Import data from JSON, CSV or TSV.
+ mongofiles - Put, get and delete files from GridFS.
+ mongostat - Show the status of a running mongod/mongos.
+ bsondump - Convert BSON files into human-readable formats.
+ mongooplog - Poll the oplog and apply to a local server.
+ mongoreplay - Traffic capture and replay tool.
+ mongotop - Track time spent reading and writing data.
BUILDING
- See docs/building.md, also www.mongodb.org search for "Building".
+ See docs/building.md.
RUNNING
@@ -30,7 +33,7 @@ RUNNING To run a single server database:
- $ mkdir /data/db
+ $ sudo mkdir -p /data/db
$ ./mongod
$
$ # The mongo javascript shell connects to localhost and test database by default:
@@ -40,7 +43,12 @@ RUNNING DRIVERS
Client drivers for most programming languages are available at
- mongodb.org. Use the shell ("mongo") for administrative tasks.
+ https://docs.mongodb.com/manual/applications/drivers/. Use the shell
+ ("mongo") for administrative tasks.
+
+BUG REPORTS
+
+ See https://github.com/mongodb/mongo/wiki/Submit-Bug-Reports.
PACKAGING
@@ -49,26 +57,28 @@ PACKAGING DOCUMENTATION
- http://www.mongodb.org/
+ https://docs.mongodb.com/manual/
+
+CLOUD HOSTED MONGODB
-CLOUD MANAGED MONGODB
+ https://www.mongodb.com/cloud/atlas
- http://cloud.mongodb.com/
+MAIL LISTS
-MAIL LISTS AND IRC
+ https://groups.google.com/forum/#!forum/mongodb-user
- http://dochub.mongodb.org/core/community
+ A forum for technical questions about using MongoDB.
+
+ https://groups.google.com/forum/#!forum/mongodb-dev
+
+ A forum for technical questions about building and developing MongoDB.
LEARN MONGODB
- http://university.mongodb.com/
+ https://university.mongodb.com/
LICENSE
Most MongoDB source files (src/mongo folder and below) are made available
- under the terms of the GNU Affero General Public License (AGPL). See
+ under the terms of the GNU Affero General Public License (GNU AGPLv3). See
individual files for details.
-
- As an exception, the files in the client/, debian/, rpm/,
- utils/mongoutils, and all subdirectories thereof are made available under
- the terms of the Apache License, version 2.0.
diff --git a/buildscripts/burn_in_tests.py b/buildscripts/burn_in_tests.py index d171a5a6bc1..6e39685ef8b 100644 --- a/buildscripts/burn_in_tests.py +++ b/buildscripts/burn_in_tests.py @@ -17,7 +17,7 @@ import sys import urlparse import yaml -API_SERVER_DEFAULT = "http://evergreen-api.mongodb.com:8080" +API_SERVER_DEFAULT = "https://evergreen.mongodb.com" # Get relative imports to work when the package is not installed on the PYTHONPATH. if __name__ == "__main__" and __package__ is None: @@ -418,6 +418,8 @@ def main(): tests_by_task = _load_tests_file(values.test_list_file) # If there are no tests to run, carry on. if tests_by_task is None: + test_results = {"failures": 0, "results": []} + _write_report_file(test_results, values.report_file) sys.exit(0) # Run the executor finder. @@ -438,6 +440,7 @@ def main(): # If there are no changed tests, exit cleanly. if not changed_tests: print "No new or modified tests found." + _write_report_file({}, values.test_list_outfile) sys.exit(0) suites = resmokelib.parser.get_suites(values, changed_tests) tests_by_executor = create_executor_list(suites, exclude_suites) diff --git a/buildscripts/hang_analyzer.py b/buildscripts/hang_analyzer.py index 73e1cbe6d41..09245904a7f 100755 --- a/buildscripts/hang_analyzer.py +++ b/buildscripts/hang_analyzer.py @@ -24,6 +24,7 @@ import signal import subprocess import sys import tempfile +import traceback import time from distutils import spawn from optparse import OptionParser @@ -53,7 +54,7 @@ def call(a, logger): if ret != 0: logger.error("Bad exit code %d" % (ret)) - raise Exception() + raise Exception("Bad exit code %d from %s" % (ret, " ".join(a))) # Copied from python 2.7 version of subprocess.py @@ -375,14 +376,14 @@ class GDBDumper(object): base, ext = os.path.splitext(logger.mongo_process_filename) raw_stacks_filename = base + '_raw_stacks' + ext raw_stacks_commands = [ - 'echo \\nWriting raw stacks to %s.\\n' % raw_stacks_filename, - # This sends output to log file rather than stdout until we turn logging off. - 'set logging redirect on', - 'set logging file ' + raw_stacks_filename, - 'set logging on', - 'thread apply all bt', - 'set logging off', - ] + 'echo \\nWriting raw stacks to %s.\\n' % raw_stacks_filename, + # This sends output to log file rather than stdout until we turn logging off. + 'set logging redirect on', + 'set logging file ' + raw_stacks_filename, + 'set logging on', + 'thread apply all bt', + 'set logging off', + ] cmds = [ "set interactive-mode off", @@ -732,28 +733,38 @@ def main(): # a signal handler to wait for the signal since it supports POSIX signals. if _is_windows: root_logger.info("Calling SetEvent to signal python process %s with PID %d" % - (process_name, pid)) + (process_name, pid)) signal_event_object(root_logger, pid) else: root_logger.info("Sending signal SIGUSR1 to python process %s with PID %d" % - (process_name, pid)) + (process_name, pid)) signal_process(root_logger, pid, signal.SIGUSR1) + trapped_exceptions = [] + # Dump all processes, except python & java. for (pid, process_name) in [(p, pn) for (p, pn) in processes if not re.match("^(java|python)", pn)]: process_logger = get_process_logger(options.debugger_output, pid, process_name) - dbg.dump_info( - root_logger, - process_logger, - pid, - process_name, - options.dump_core and check_dump_quota(max_dump_size_bytes, dbg.get_dump_ext())) + try: + dbg.dump_info( + root_logger, + process_logger, + pid, + process_name, + options.dump_core and check_dump_quota(max_dump_size_bytes, dbg.get_dump_ext())) + except Exception as err: + root_logger.info("Error encountered when invoking debugger %s" % err) + trapped_exceptions.append(traceback.format_exc()) # Dump java processes using jstack. for (pid, process_name) in [(p, pn) for (p, pn) in processes if pn.startswith("java")]: process_logger = get_process_logger(options.debugger_output, pid, process_name) - jstack.dump_info(root_logger, process_logger, pid, process_name) + try: + jstack.dump_info(root_logger, process_logger, pid, process_name) + except Exception as err: + root_logger.info("Error encountered when invoking debugger %s" % err) + trapped_exceptions.append(traceback.format_exc()) # Signal go processes to ensure they print out stack traces, and die on POSIX OSes. # On Windows, this will simply kill the process since python emulates SIGABRT as @@ -761,10 +772,15 @@ def main(): # Note: The stacktrace output may be captured elsewhere (i.e. resmoke). for (pid, process_name) in [(p, pn) for (p, pn) in processes if pn in go_processes]: root_logger.info("Sending signal SIGABRT to go process %s with PID %d" % - (process_name, pid)) + (process_name, pid)) signal_process(root_logger, pid, signal.SIGABRT) root_logger.info("Done analyzing all processes for hangs") + for exception in trapped_exceptions: + root_logger.info(exception) + if trapped_exceptions: + sys.exit(1) + if __name__ == "__main__": main() diff --git a/buildscripts/package_test/.kitchen.yml b/buildscripts/package_test/.kitchen.yml index e9b6989d221..3b4a476551c 100644 --- a/buildscripts/package_test/.kitchen.yml +++ b/buildscripts/package_test/.kitchen.yml @@ -2,11 +2,13 @@ driver: name: ec2 region: us-east-1 + vpc_mode: true + vpc_id: <%= ENV['KITCHEN_VPC'] %> subnet_id: <%= ENV['KITCHEN_SUBNET'] %> security_group_ids: - <%= ENV['KITCHEN_SECURITY_GROUP'] %> aws_ssh_key_id: <%= ENV['KITCHEN_SSH_KEY_ID'] %> - interface: dns + interface: private associate_public_ip: true verifier: diff --git a/buildscripts/package_test/recipes/install_mongodb.rb b/buildscripts/package_test/recipes/install_mongodb.rb index dcf7499289e..9b4ea703b2b 100644 --- a/buildscripts/package_test/recipes/install_mongodb.rb +++ b/buildscripts/package_test/recipes/install_mongodb.rb @@ -84,6 +84,16 @@ if platform_family? 'suse' EOD end + %w( + SLES12-Pool + SLES12-Updates + ).each do |repo| + execute "add #{repo}" do + command "zypper addrepo --check --refresh --name \"#{repo}\" http://smt-ec2.susecloud.net/repo/SUSE/Products/SLE-SERVER/12/x86_64/product?credentials=SMT-http_smt-ec2_susecloud_net 'SMT-http_smt-ec2_susecloud_net:#{repo}'" + not_if "zypper lr | grep #{repo}" + end + end + execute 'install mongod' do command 'zypper -n install `find . -name "*server*.rpm"`' cwd homedir diff --git a/buildscripts/resmokeconfig/suites/aggregation_read_concern_majority_passthrough.yml b/buildscripts/resmokeconfig/suites/aggregation_read_concern_majority_passthrough.yml index bc227b09279..72b838b3c07 100644 --- a/buildscripts/resmokeconfig/suites/aggregation_read_concern_majority_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/aggregation_read_concern_majority_passthrough.yml @@ -6,11 +6,28 @@ selector: - jstests/aggregation/expressions/*.js - jstests/aggregation/sources/*/*.js exclude_files: - - jstests/aggregation/bugs/server18198.js # Uses a mocked mongo client to test read preference. - - jstests/aggregation/mongos_slaveok.js # Majority read on secondary requires afterOpTime. - - jstests/aggregation/sources/facet/use_cases.js # Cannot specify write concern when - # secondaryThrottle is not set. - - jstests/aggregation/testSlave.js # Majority read on secondary requires afterOpTime. + # These tests fail due to the inability to specify a writeConcern when secondaryThrottle is not + # set as part of the moveChunk command. + - jstests/aggregation/sources/facet/use_cases.js + # These test fail because afterOpTime is required to guarantee a secondary has advanced its + # majority-committed snapshot. + - jstests/aggregation/mongos_slaveok.js + - jstests/aggregation/testSlave.js + exclude_with_any_tags: + ## + # The next three tags correspond to the special errors thrown by the + # set_read_and_write_concerns.js override when it refuses to replace the readConcern or + # writeConcern of a particular command. Above each tag are the message(s) that cause the tag to + # be warranted. + ## + # "Cowardly refusing to override read concern of command: ..." + - assumes_read_concern_unchanged + # "Cowardly refusing to override write concern of command: ..." + - assumes_write_concern_unchanged + # "Cowardly refusing to run test with overridden write concern when it uses a command that can + # only perform w=1 writes: ..." + - requires_collmod_command + - requires_eval_command executor: js_test: @@ -18,13 +35,19 @@ executor: shell_options: global_vars: TestData: + defaultReadConcernLevel: majority enableMajorityReadConcern: '' - eval: "var testingReplication = true; load('jstests/libs/override_methods/set_majority_read_and_write_concerns.js');" + eval: >- + var testingReplication = true; + load('jstests/libs/override_methods/set_read_and_write_concerns.js'); readMode: commands hooks: - - class: ValidateCollections + # The CheckReplDBHash hook waits until all operations have replicated to and have been applied + # on the secondaries, so we run the ValidateCollections hook after it to ensure we're + # validating the entire contents of the collection. - class: CheckReplOplogs - class: CheckReplDBHash + - class: ValidateCollections fixture: class: ReplicaSetFixture mongod_options: @@ -33,5 +56,3 @@ executor: enableTestCommands: 1 numInitialSyncAttempts: 1 num_nodes: 2 - # Needs to be set for any ephemeral or no-journaling storage engine - write_concern_majority_journal_default: false diff --git a/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml b/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml index f47e424e1cf..22e84bc9c63 100644 --- a/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml @@ -3,79 +3,47 @@ selector: roots: - jstests/core/**/*.js exclude_files: - # Tests that won't work with an injected 'majority' readConcern - # and/or an injected 'majority' writeConcern. Where a function is - # listed the reason is we don't have a reliable solution to override - # the write concern for that function. - - jstests/core/batch_write_command*.js # these tests use various write concerns - - jstests/core/bench_test*.js # benchRun() used for writes - - jstests/core/capped_update.js # uses godinsert and can't run under replication. - - jstests/core/crud_api.js # has specific w:0 tests - - jstests/core/error2.js # db.eval() used - - jstests/core/eval0.js # db.eval() used - - jstests/core/eval1.js # db.eval() used - - jstests/core/eval3.js # db.eval() used - - jstests/core/eval4.js # db.eval() used - - jstests/core/eval5.js # db.eval() used - - jstests/core/eval6.js # db.eval() used - - jstests/core/eval7.js # db.eval() used - - jstests/core/eval9.js # db.eval() used - - jstests/core/evala.js # db.eval() used - - jstests/core/evalb.js # db.eval() used - - jstests/core/evald.js # db.eval() used - - jstests/core/evale.js # db.eval() used - - jstests/core/evalg.js # db.eval() used - - jstests/core/eval_mr.js # db.eval() used - - jstests/core/eval_nolock.js # db.eval() used - - jstests/core/geo_s2cursorlimitskip.js # drops system.profile collection and counts ops. - - jstests/core/js3.js # db.dbEval() used - - jstests/core/js7.js # db.eval() used - - jstests/core/js9.js # db.eval() used - - jstests/core/mr_merge.js # mr temp tables aren't replicated - - jstests/core/mr_merge2.js # mr temp tables aren't replicated - - jstests/core/mr_outreduce.js # mr temp tables aren't replicated - - jstests/core/mr_outreduce2.js # mr temp tables aren't replicated - - jstests/core/opcounters_active.js # off by n problem with opcounters - - jstests/core/opcounters_write_cmd.js # off by n problem with opcounters - - jstests/core/profile_agg.js # system.profile not replicated - - jstests/core/profile_count.js # system.profile not replicated - - jstests/core/profile_delete.js # system.profile not replicated - - jstests/core/profile_distinct.js # system.profile not replicated - - jstests/core/profile_find.js # system.profile not replicated - - jstests/core/profile_findandmodify.js # system.profile not replicated - - jstests/core/profile_geonear.js # system.profile not replicated - - jstests/core/profile_getmore.js # system.profile not replicated - - jstests/core/profile_group.js # system.profile not replicated - - jstests/core/profile_insert.js # system.profile not replicated - - jstests/core/profile_mapreduce.js # system.profile not replicated - - jstests/core/profile_update.js # system.profile not replicated - - jstests/core/profile1.js # system.profile not replicated - - jstests/core/profile2.js # system.profile not replicated - - jstests/core/profile3.js # system.profile not replicated - - jstests/core/read_after_optime.js # verifies read after optime fails on standalone - - jstests/core/remove8.js # db.eval() used - - jstests/core/rename4.js # db.eval() used - - jstests/core/shell1.js # tests setSlaveOk() variations on standalone mongod - - jstests/core/shellkillop.js # db.eval() used - - jstests/core/shell_writeconcern.js # checks write concern shell helpers - - jstests/core/storefunc.js # db.eval() used - - jstests/core/write_result.js # Tests invalid writeConcern, we shouldn't override. - # Tests that need triaging & remediation | blacklist decision - # Comments list possible problem point under review. - - jstests/core/capped6.js # Uses captrunc test command. - - jstests/core/convert_to_capped_nonexistant.js # Uses convertToCapped and captrunc command. - - jstests/core/stages_delete.js # Uses stageDebug command for deletes. + # These tests are not expected to pass with replica-sets: + - jstests/core/dbadmin.js + - jstests/core/opcounters_write_cmd.js + - jstests/core/read_after_optime.js + - jstests/core/capped_update.js + # These tests use benchRun(), which isn't configured to use the overridden writeConcern. + - jstests/core/bench_test*.js + exclude_with_any_tags: + ## + # The next three tags correspond to the special errors thrown by the + # set_read_and_write_concerns.js override when it refuses to replace the readConcern or + # writeConcern of a particular command. Above each tag are the message(s) that cause the tag to + # be warranted. + ## + # "Cowardly refusing to override read concern of command: ..." + - assumes_read_concern_unchanged + # "Cowardly refusing to override write concern of command: ..." + - assumes_write_concern_unchanged + # "Cowardly refusing to run test with overridden write concern when it uses a command that can + # only perform w=1 writes: ..." + - requires_collmod_command + - requires_eval_command executor: js_test: config: shell_options: - eval: "var testingReplication = true; load('jstests/libs/override_methods/set_majority_read_and_write_concerns.js');" + global_vars: + TestData: + defaultReadConcernLevel: majority + eval: >- + var testingReplication = true; + load('jstests/libs/override_methods/set_read_and_write_concerns.js'); readMode: commands hooks: - - class: ValidateCollections + # The CheckReplDBHash hook waits until all operations have replicated to and have been applied + # on the secondaries, so we run the ValidateCollections hook after it to ensure we're + # validating the entire contents of the collection. - class: CheckReplOplogs - class: CheckReplDBHash + - class: ValidateCollections - class: CleanEveryN n: 20 fixture: @@ -84,7 +52,6 @@ executor: set_parameters: enableTestCommands: 1 numInitialSyncAttempts: 1 + writePeriodicNoops: 1 enableMajorityReadConcern: '' num_nodes: 2 - # Needs to be set for any ephemeral or no-journaling storage engine - write_concern_majority_journal_default: false diff --git a/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml index 832e0ab9bfc..114ebcab372 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml @@ -14,6 +14,7 @@ selector: - jstests/core/commands_that_do_not_write_do_not_accept_wc.js - jstests/core/constructors.js - jstests/core/eval_mr.js + - jstests/core/function_string_representations.js - jstests/core/geo_big_polygon3.js - jstests/core/geo_mapreduce.js - jstests/core/geo_mapreduce2.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_resync_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_resync_jscore_passthrough.yml index 3b48456c92e..1e51713a475 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_resync_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_resync_jscore_passthrough.yml @@ -14,6 +14,7 @@ selector: - jstests/core/commands_that_do_not_write_do_not_accept_wc.js - jstests/core/constructors.js - jstests/core/eval_mr.js + - jstests/core/function_string_representations.js - jstests/core/geo_big_polygon3.js - jstests/core/geo_mapreduce.js - jstests/core/geo_mapreduce2.js diff --git a/buildscripts/resmokeconfig/suites/sharding_continuous_config_stepdown.yml b/buildscripts/resmokeconfig/suites/sharding_continuous_config_stepdown.yml index 0bafa32a1e5..acf8ae53e96 100644 --- a/buildscripts/resmokeconfig/suites/sharding_continuous_config_stepdown.yml +++ b/buildscripts/resmokeconfig/suites/sharding_continuous_config_stepdown.yml @@ -50,6 +50,7 @@ selector: - jstests/sharding/shard2.js - jstests/sharding/shard3.js - jstests/sharding/shard_collection_basic.js + - jstests/sharding/shard_existing_coll_chunk_count.js - jstests/sharding/sharding_balance1.js - jstests/sharding/sharding_balance2.js - jstests/sharding/sharding_balance3.js diff --git a/buildscripts/resmokeconfig/suites/sharding_last_stable_mongos_and_mixed_shards.yml b/buildscripts/resmokeconfig/suites/sharding_last_stable_mongos_and_mixed_shards.yml index f4c741badd4..c7ff1b24395 100644 --- a/buildscripts/resmokeconfig/suites/sharding_last_stable_mongos_and_mixed_shards.yml +++ b/buildscripts/resmokeconfig/suites/sharding_last_stable_mongos_and_mixed_shards.yml @@ -95,6 +95,8 @@ selector: # TODO (SERVER-27379): Cannot create views when featureCompatibilityVersion is 3.2. # Unblacklist when the featureCompatibilityVersion is 3.4 or higher. - jstests/sharding/views.js + # SERVER-20392: Reenable when backported to 3.4 and released as last-stable. + - jstests/sharding/shard_existing_coll_chunk_count.js executor: js_test: diff --git a/buildscripts/resmokeconfig/suites/write_concern_majority_passthrough.yml b/buildscripts/resmokeconfig/suites/write_concern_majority_passthrough.yml new file mode 100644 index 00000000000..388046d2782 --- /dev/null +++ b/buildscripts/resmokeconfig/suites/write_concern_majority_passthrough.yml @@ -0,0 +1,78 @@ +test_kind: js_test + +selector: + js_test: + roots: + - jstests/core/**/*.js + exclude_files: + # These tests are not expected to pass with replica-sets: + - jstests/core/dbadmin.js + - jstests/core/opcounters_write_cmd.js + - jstests/core/read_after_optime.js + - jstests/core/capped_update.js + # The connection_string_validation.js test does not expect the mongo shell to be using a replica + # set connection string. + - jstests/core/connection_string_validation.js + # These tests attempt to read from the "system.profile" collection, which may be missing entries + # if a write was performed on the primary of the replica set instead. + - jstests/core/*profile*.js + # The shellkillop.js test spawns a parallel shell without using startParallelShell() and + # therefore doesn't inherit the w="majority" write concern when performing its writes. + - jstests/core/shellkillop.js + exclude_with_any_tags: + ## + # The next three tags correspond to the special errors thrown by the + # set_read_and_write_concerns.js override when it refuses to replace the readConcern or + # writeConcern of a particular command. Above each tag are the message(s) that cause the tag to + # be warranted. + ## + # "Cowardly refusing to override read concern of command: ..." + - assumes_read_concern_unchanged + # "Cowardly refusing to override write concern of command: ..." + - assumes_write_concern_unchanged + # "Cowardly refusing to run test with overridden write concern when it uses a command that can + # only perform w=1 writes: ..." + - requires_collmod_command + - requires_eval_command + ## + # The next tag corresponds to the special error thrown by the set_read_preference_secondary.js + # override when it refuses to replace the readPreference of a particular command. Above each tag + # are the message(s) that cause the tag to be warranted. + ## + # "Cowardly refusing to override read preference of command: ..." + # "Cowardly refusing to run test with overridden read preference when it reads from a + # non-replicated collection: ..." + - assumes_read_preference_unchanged + +executor: + js_test: + config: + shell_options: + global_vars: + TestData: + defaultReadConcernLevel: local + eval: >- + testingReplication = true; + load('jstests/libs/override_methods/set_read_and_write_concerns.js'); + load('jstests/libs/override_methods/set_read_preference_secondary.js'); + readMode: commands + hooks: + # The CheckReplDBHash hook waits until all operations have replicated to and have been applied + # on the secondaries, so we run the ValidateCollections hook after it to ensure we're + # validating the entire contents of the collection. + - class: CheckReplOplogs + - class: CheckReplDBHash + - class: ValidateCollections + - class: CleanEveryN + n: 20 + fixture: + class: ReplicaSetFixture + mongod_options: + set_parameters: + enableTestCommands: 1 + numInitialSyncAttempts: 1 + # This suite requires w="majority" writes to be applied on all secondaries. By using a 2-node + # replica set and having secondaries vote, the majority of the replica set is all nodes. + num_nodes: 2 + voting_secondaries: true + use_replica_set_connection_string: true diff --git a/buildscripts/resmokelib/core/programs.py b/buildscripts/resmokelib/core/programs.py index e4cadc8e006..d94cd438ee0 100644 --- a/buildscripts/resmokelib/core/programs.py +++ b/buildscripts/resmokelib/core/programs.py @@ -115,8 +115,8 @@ def mongos_program(logger, executable=None, process_kwargs=None, **kwargs): return _process.Process(logger, args, **process_kwargs) -def mongo_shell_program(logger, executable=None, filename=None, process_kwargs=None, - isMainTest=True, **kwargs): +def mongo_shell_program(logger, executable=None, connection_string=None, filename=None, + process_kwargs=None, isMainTest=True, **kwargs): """ Returns a Process instance that starts a mongo shell with arguments constructed from 'kwargs'. @@ -184,9 +184,22 @@ def mongo_shell_program(logger, executable=None, filename=None, process_kwargs=N if config.SHELL_WRITE_MODE is not None: kwargs["writeMode"] = config.SHELL_WRITE_MODE + if connection_string is not None: + # The --host and --port options are ignored by the mongo shell when an explicit connection + # string is specified. We remove these options to avoid any ambiguity with what server the + # logged mongo shell invocation will connect to. + if "port" in kwargs: + kwargs.pop("port") + + if "host" in kwargs: + kwargs.pop("host") + # Apply the rest of the command line arguments. _apply_kwargs(args, kwargs) + if connection_string is not None: + args.append(connection_string) + # Have the mongos shell run the specified file. args.append(filename) diff --git a/buildscripts/resmokelib/logging/buildlogger.py b/buildscripts/resmokelib/logging/buildlogger.py index f058372df8c..e8e37d2c004 100644 --- a/buildscripts/resmokelib/logging/buildlogger.py +++ b/buildscripts/resmokelib/logging/buildlogger.py @@ -5,7 +5,9 @@ Defines handlers for communicating with a buildlogger server. from __future__ import absolute_import import functools +import httplib import urllib2 +import json from . import handlers from . import loggers @@ -131,6 +133,47 @@ def new_test_id(build_id, build_config, test_filename, test_command): return response["id"] +class _LogsSplitter(object): + """Class with static methods used to split list of log lines into smaller batches.""" + + @staticmethod + def split_logs(log_lines, max_size): + """ + Splits the log lines into batches of size less than or equal to max_size. + + Args: + log_lines: A list of log lines. + max_size: The maximum size in bytes a batch of log lines can have in JSON. + Returns: + A list of list of log lines. Each item is a list is a list of log lines + satisfying the size requirement. + """ + if not max_size: + return [log_lines] + + def line_size(line): + """ + Computes the encoded JSON size of a log line as part of an array. + 2 is added to each string size to account for the array representation of the logs, + as each line is preceded by a '[' or a space and followed by a ',' or a ']'. + """ + return len(json.dumps(line, encoding="utf-8")) + 2 + + curr_logs = [] + curr_logs_size = 0 + split_logs = [] + for line in log_lines: + size = line_size(line) + if curr_logs_size + size > max_size: + split_logs.append(curr_logs) + curr_logs = [] + curr_logs_size = 0 + curr_logs.append(line) + curr_logs_size += size + split_logs.append(curr_logs) + return split_logs + + class _BaseBuildloggerHandler(handlers.BufferedHandler): """ Base class of the buildlogger handler for the global logs and the @@ -138,8 +181,8 @@ class _BaseBuildloggerHandler(handlers.BufferedHandler): """ def __init__(self, - build_id, build_config, + endpoint, capacity=_SEND_AFTER_LINES, interval_secs=_SEND_AFTER_SECS): """ @@ -157,8 +200,9 @@ class _BaseBuildloggerHandler(handlers.BufferedHandler): username, password) - self.build_id = build_id + self.endpoint = endpoint self.retry_buffer = [] + self.max_size = None def process_record(self, record): """ @@ -177,12 +221,55 @@ class _BaseBuildloggerHandler(handlers.BufferedHandler): """ Convenience method for subclasses to use when making POST requests. """ - return self.http_handler.post(*args, **kwargs) def _append_logs(self, log_lines): - raise NotImplementedError("_append_logs must be implemented by _BaseBuildloggerHandler" - " subclasses") + """ + Sends a POST request to the handlers endpoint with the logs that have been captured. + + Returns: + The number of log lines that have been successfully sent. + """ + lines_sent = 0 + for chunk in _LogsSplitter.split_logs(log_lines, self.max_size): + chunk_lines_sent = self.__append_logs_chunk(chunk) + lines_sent += chunk_lines_sent + if chunk_lines_sent < len(chunk): + # Not all lines have been sent. We stop here. + break + return lines_sent + + def __append_logs_chunk(self, log_lines_chunk): + """ + Sends a log lines chunk, handles 413 Request Entity Too Large errors and retries + if necessary. + + Returns: + The number of log lines that have been successfully sent. + """ + try: + self.post(self.endpoint, data=log_lines_chunk) + return len(log_lines_chunk) + except urllib2.HTTPError as err: + # Handle the "Request Entity Too Large" error, set the max size and retry. + if err.code == httplib.REQUEST_ENTITY_TOO_LARGE: + response_data = json.load(err, "utf-8") + if isinstance(response_data, dict) and "max_size" in response_data: + new_max_size = response_data["max_size"] + if self.max_size and new_max_size >= self.max_size: + loggers._BUILDLOGGER_FALLBACK.exception( + "Received an HTTP 413 code, but already had max_size set") + return 0 + loggers._BUILDLOGGER_FALLBACK.warning( + "Received an HTTP 413 code, updating the request max_size to %s", + new_max_size) + self.max_size = new_max_size + return self._append_logs(log_lines_chunk) + loggers._BUILDLOGGER_FALLBACK.exception("Encountered an error.") + return 0 + except: + loggers._BUILDLOGGER_FALLBACK.exception("Encountered an error.") + return 0 def _flush_buffer_with_lock(self, buf, close_called): """ @@ -196,9 +283,10 @@ class _BaseBuildloggerHandler(handlers.BufferedHandler): self.retry_buffer.extend(buf) - if self._append_logs(self.retry_buffer): - self.retry_buffer = [] - elif close_called: + nb_sent = self._append_logs(self.retry_buffer) + if nb_sent: + self.retry_buffer = self.retry_buffer[nb_sent:] + if close_called and self.retry_buffer: # Request to the buildlogger server returned an error, so use the fallback logger to # avoid losing the log messages entirely. for (_, message) in self.retry_buffer: @@ -215,29 +303,14 @@ class BuildloggerTestHandler(_BaseBuildloggerHandler): Buildlogger handler for the test logs. """ - def __init__(self, build_id, build_config, test_id, **kwargs): - """ - Initializes the buildlogger handler with the build id, test id, - and credentials. - """ - - _BaseBuildloggerHandler.__init__(self, build_id, build_config, **kwargs) - - self.test_id = test_id - - @_log_on_error - def _append_logs(self, log_lines): - """ - Sends a POST request to the APPEND_TEST_LOGS_ENDPOINT with the - logs that have been captured. - """ + def __init__(self, build_config, build_id, test_id, + capacity=_SEND_AFTER_LINES, interval_secs=_SEND_AFTER_SECS): + """Initializes the buildlogger handler with the credentials, build id, and test id.""" endpoint = APPEND_TEST_LOGS_ENDPOINT % { - "build_id": self.build_id, - "test_id": self.test_id, + "build_id": build_id, + "test_id": test_id, } - - response = self.post(endpoint, data=log_lines) - return response is not None + _BaseBuildloggerHandler.__init__(self, build_config, endpoint, capacity, interval_secs) @_log_on_error def _finish_test(self, failed=False): @@ -245,12 +318,7 @@ class BuildloggerTestHandler(_BaseBuildloggerHandler): Sends a POST request to the APPEND_TEST_LOGS_ENDPOINT with the test status. """ - endpoint = APPEND_TEST_LOGS_ENDPOINT % { - "build_id": self.build_id, - "test_id": self.test_id, - } - - self.post(endpoint, headers={ + self.post(self.endpoint, headers={ "X-Sendlogs-Test-Done": "true", "X-Sendlogs-Test-Failed": "true" if failed else "false", }) @@ -271,12 +339,8 @@ class BuildloggerGlobalHandler(_BaseBuildloggerHandler): Buildlogger handler for the global logs. """ - @_log_on_error - def _append_logs(self, log_lines): - """ - Sends a POST request to the APPEND_GLOBAL_LOGS_ENDPOINT with - the logs that have been captured. - """ - endpoint = APPEND_GLOBAL_LOGS_ENDPOINT % {"build_id": self.build_id} - response = self.post(endpoint, data=log_lines) - return response is not None + def __init__(self, build_config, build_id, + capacity=_SEND_AFTER_LINES, interval_secs=_SEND_AFTER_SECS): + """Initializes the buildlogger handler with the credentials and build id.""" + endpoint = APPEND_GLOBAL_LOGS_ENDPOINT % {"build_id": build_id} + _BaseBuildloggerHandler.__init__(self, build_config, endpoint, capacity, interval_secs) diff --git a/buildscripts/resmokelib/logging/config.py b/buildscripts/resmokelib/logging/config.py index c3960bbafd3..c3216b2d879 100644 --- a/buildscripts/resmokelib/logging/config.py +++ b/buildscripts/resmokelib/logging/config.py @@ -67,8 +67,8 @@ def apply_buildlogger_global_handler(logger, logging_config, build_id=None, buil log_format = logger_info.get("format", _DEFAULT_FORMAT) formatter = formatters.ISO8601Formatter(fmt=log_format) - handler = buildlogger.BuildloggerGlobalHandler(build_id, - build_config, + handler = buildlogger.BuildloggerGlobalHandler(build_config, + build_id, **handler_info) handler.setFormatter(formatter) else: @@ -98,8 +98,8 @@ def apply_buildlogger_test_handler(logger, log_format = logger_info.get("format", _DEFAULT_FORMAT) formatter = formatters.ISO8601Formatter(fmt=log_format) - handler = buildlogger.BuildloggerTestHandler(build_id, - build_config, + handler = buildlogger.BuildloggerTestHandler(build_config, + build_id, test_id, **handler_info) handler.setFormatter(formatter) diff --git a/buildscripts/resmokelib/testing/fixtures/interface.py b/buildscripts/resmokelib/testing/fixtures/interface.py index b4b0066a5aa..83418502ead 100644 --- a/buildscripts/resmokelib/testing/fixtures/interface.py +++ b/buildscripts/resmokelib/testing/fixtures/interface.py @@ -79,13 +79,22 @@ class Fixture(object): """ return True - def get_connection_string(self): + def get_internal_connection_string(self): """ Returns the connection string for this fixture. This is NOT a driver connection string, but a connection string of the format expected by the mongo::ConnectionString class. """ - raise NotImplementedError("get_connection_string must be implemented by Fixture subclasses") + raise NotImplementedError( + "get_internal_connection_string must be implemented by Fixture subclasses") + + def get_driver_connection_url(self): + """ + Return the mongodb connection string as defined here: + https://docs.mongodb.com/manual/reference/connection-string/ + """ + raise NotImplementedError( + "get_driver_connection_url must be implemented by Fixture subclasses") def __str__(self): return "%s (Job #%d)" % (self.__class__.__name__, self.job_num) diff --git a/buildscripts/resmokelib/testing/fixtures/masterslave.py b/buildscripts/resmokelib/testing/fixtures/masterslave.py index 469c7ac0816..fb444cfe097 100644 --- a/buildscripts/resmokelib/testing/fixtures/masterslave.py +++ b/buildscripts/resmokelib/testing/fixtures/masterslave.py @@ -5,7 +5,6 @@ Master/slave fixture for executing JSTests against. from __future__ import absolute_import import os.path -import socket import pymongo @@ -160,6 +159,12 @@ class MasterSlaveFixture(interface.ReplFixture): mongod_options = self.mongod_options.copy() mongod_options.update(self.slave_options) mongod_options["slave"] = "" - mongod_options["source"] = "%s:%d" % (socket.gethostname(), self.port) + mongod_options["source"] = self.master.get_internal_connection_string() mongod_options["dbpath"] = os.path.join(self._dbpath_prefix, "slave") return self._new_mongod(mongod_logger, mongod_options) + + def get_internal_connection_string(self): + return self.master.get_internal_connection_string() + + def get_driver_connection_url(self): + return self.master.get_driver_connection_url() diff --git a/buildscripts/resmokelib/testing/fixtures/replicaset.py b/buildscripts/resmokelib/testing/fixtures/replicaset.py index 141bbd60ea1..0ec3de30280 100644 --- a/buildscripts/resmokelib/testing/fixtures/replicaset.py +++ b/buildscripts/resmokelib/testing/fixtures/replicaset.py @@ -37,7 +37,8 @@ class ReplicaSetFixture(interface.ReplFixture): write_concern_majority_journal_default=None, auth_options=None, replset_config_options=None, - voting_secondaries=True): + voting_secondaries=False, + use_replica_set_connection_string=False): interface.ReplFixture.__init__(self, logger, job_num) @@ -50,6 +51,7 @@ class ReplicaSetFixture(interface.ReplFixture): self.auth_options = auth_options self.replset_config_options = utils.default_if_none(replset_config_options, {}) self.voting_secondaries = voting_secondaries + self.use_replica_set_connection_string = use_replica_set_connection_string # The dbpath in mongod_options is used as the dbpath prefix for replica set members and # takes precedence over other settings. The ShardedClusterFixture uses this parameter to @@ -97,7 +99,7 @@ class ReplicaSetFixture(interface.ReplFixture): # Initiate the replica set. members = [] for (i, node) in enumerate(self.nodes): - member_info = {"_id": i, "host": node.get_connection_string()} + member_info = {"_id": i, "host": node.get_internal_connection_string()} if i > 0: member_info["priority"] = 0 if i >= 7 or not self.voting_secondaries: @@ -107,7 +109,7 @@ class ReplicaSetFixture(interface.ReplFixture): members.append(member_info) if self.initial_sync_node: members.append({"_id": self.initial_sync_node_idx, - "host": self.initial_sync_node.get_connection_string(), + "host": self.initial_sync_node.get_internal_connection_string(), "priority": 0, "hidden": 1, "votes": 0}) @@ -291,11 +293,27 @@ class ReplicaSetFixture(interface.ReplFixture): return logging.loggers.new_logger(logger_name, parent=self.logger) - def get_connection_string(self): + def get_internal_connection_string(self): if self.replset_name is None: - raise ValueError("Must call setup() before calling get_connection_string()") + raise ValueError("Must call setup() before calling get_internal_connection_string()") - conn_strs = [node.get_connection_string() for node in self.nodes] + conn_strs = [node.get_internal_connection_string() for node in self.nodes] if self.initial_sync_node: - conn_strs.append(self.initial_sync_node.get_connection_string()) + conn_strs.append(self.initial_sync_node.get_internal_connection_string()) return self.replset_name + "/" + ",".join(conn_strs) + + def get_driver_connection_url(self): + if self.replset_name is None: + raise ValueError("Must call setup() before calling get_driver_connection_url()") + + if self.use_replica_set_connection_string: + # We use a replica set connection string when all nodes are electable because we + # anticipate the client will want to gracefully handle any failovers. + conn_strs = [node.get_internal_connection_string() for node in self.nodes] + if self.initial_sync_node: + conn_strs.append(self.initial_sync_node.get_internal_connection_string()) + return "mongodb://" + ",".join(conn_strs) + "/?replicaSet=" + self.replset_name + else: + # We return a direct connection to the expected pimary when only the first node is + # electable because we want the client to error out if a stepdown occurs. + return self.nodes[0].get_driver_connection_url() diff --git a/buildscripts/resmokelib/testing/fixtures/shardedcluster.py b/buildscripts/resmokelib/testing/fixtures/shardedcluster.py index ac7e597f24b..2e2db535d6d 100644 --- a/buildscripts/resmokelib/testing/fixtures/shardedcluster.py +++ b/buildscripts/resmokelib/testing/fixtures/shardedcluster.py @@ -170,11 +170,14 @@ class ShardedClusterFixture(interface.Fixture): all(shard.is_running() for shard in self.shards) and self.mongos is not None and self.mongos.is_running()) - def get_connection_string(self): + def get_internal_connection_string(self): if self.mongos is None: - raise ValueError("Must call setup() before calling get_connection_string()") + raise ValueError("Must call setup() before calling get_internal_connection_string()") - return "%s:%d" % (socket.gethostname(), self.mongos.port) + return self.mongos.get_internal_connection_string() + + def get_driver_connection_url(self): + return "mongodb://" + self.get_internal_connection_string() def _new_configsvr(self): """ @@ -229,16 +232,11 @@ class ShardedClusterFixture(interface.Fixture): mongos_logger = logging.loggers.new_logger(logger_name, parent=self.logger) mongos_options = copy.deepcopy(self.mongos_options) - configdb_hostname = socket.gethostname() if self.separate_configsvr: - configdb_replset = ShardedClusterFixture._CONFIGSVR_REPLSET_NAME - configdb_port = self.configsvr.port - mongos_options["configdb"] = "%s/%s:%d" % (configdb_replset, - configdb_hostname, - configdb_port) + mongos_options["configdb"] = self.configsvr.get_internal_connection_string() else: - mongos_options["configdb"] = "%s:%d" % (configdb_hostname, self.shards[0].port) + mongos_options["configdb"] = "localhost:%d" % (self.shards[0].port) return _MongoSFixture(mongos_logger, self.job_num, @@ -254,9 +252,9 @@ class ShardedClusterFixture(interface.Fixture): for more details. """ - hostname = socket.gethostname() - self.logger.info("Adding %s:%d as a shard..." % (hostname, shard.port)) - client.admin.command({"addShard": "%s:%d" % (hostname, shard.port)}) + connection_string = shard.get_internal_connection_string() + self.logger.info("Adding %s as a shard...", connection_string) + client.admin.command({"addShard": connection_string}) class _MongoSFixture(interface.Fixture): @@ -356,3 +354,12 @@ class _MongoSFixture(interface.Fixture): def is_running(self): return self.mongos is not None and self.mongos.poll() is None + + def get_internal_connection_string(self): + if self.mongos is None: + raise ValueError("Must call setup() before calling get_internal_connection_string()") + + return "localhost:%d" % self.port + + def get_driver_connection_url(self): + return "mongodb://" + self.get_internal_connection_string() diff --git a/buildscripts/resmokelib/testing/fixtures/standalone.py b/buildscripts/resmokelib/testing/fixtures/standalone.py index ba62b3d2b8c..bc69775c285 100644 --- a/buildscripts/resmokelib/testing/fixtures/standalone.py +++ b/buildscripts/resmokelib/testing/fixtures/standalone.py @@ -7,7 +7,6 @@ from __future__ import absolute_import import os import os.path import shutil -import socket import time import pymongo @@ -146,8 +145,11 @@ class MongoDFixture(interface.Fixture): def is_running(self): return self.mongod is not None and self.mongod.poll() is None - def get_connection_string(self): + def get_internal_connection_string(self): if self.mongod is None: - raise ValueError("Must call setup() before calling get_connection_string()") + raise ValueError("Must call setup() before calling get_internal_connection_string()") - return "%s:%d" % (socket.gethostname(), self.port) + return "localhost:%d" % self.port + + def get_driver_connection_url(self): + return "mongodb://" + self.get_internal_connection_string() diff --git a/buildscripts/resmokelib/testing/testcases.py b/buildscripts/resmokelib/testing/testcases.py index b4029fc6ea8..21d35215a29 100644 --- a/buildscripts/resmokelib/testing/testcases.py +++ b/buildscripts/resmokelib/testing/testcases.py @@ -187,7 +187,7 @@ class CPPIntegrationTestCase(TestCase): def configure(self, fixture, *args, **kwargs): TestCase.configure(self, fixture, *args, **kwargs) - self.program_options["connectionString"] = self.fixture.get_connection_string() + self.program_options["connectionString"] = self.fixture.get_internal_connection_string() def run_test(self): try: @@ -362,6 +362,19 @@ class JSTestCase(TestCase): # Directory already exists. pass + process_kwargs = self.shell_options.get("process_kwargs", {}).copy() + + if "KRB5_CONFIG" in process_kwargs and "KRB5CCNAME" not in process_kwargs: + # Use a job-specific credential cache for JavaScript tests involving Kerberos. + krb5_dir = os.path.join(data_dir, "krb5") + try: + os.makedirs(krb5_dir) + except os.error: + pass + process_kwargs["KRB5CCNAME"] = "DIR:" + os.path.join(krb5_dir, ".") + + self.shell_options["process_kwargs"] = process_kwargs + def _get_data_dir(self, global_vars): """ Returns the value that the mongo shell should set for the @@ -408,11 +421,13 @@ class JSTestCase(TestCase): is_main_test = True if thread_id > 0: is_main_test = False - return core.programs.mongo_shell_program(logger, - executable=self.shell_executable, - filename=self.js_filename, - isMainTest=is_main_test, - **self.shell_options) + return core.programs.mongo_shell_program( + logger, + executable=self.shell_executable, + filename=self.js_filename, + connection_string=self.fixture.get_driver_connection_url(), + isMainTest=is_main_test, + **self.shell_options) def _run_test_in_thread(self, thread_id): # Make a logger for each thread. diff --git a/buildscripts/scons.py b/buildscripts/scons.py index e7860162eb6..b0b9cfa834e 100755 --- a/buildscripts/scons.py +++ b/buildscripts/scons.py @@ -1,16 +1,26 @@ -#!/usr/bin/python +#!/usr/bin/env python2 + +from __future__ import print_function import os -import subprocess import sys SCONS_VERSION = os.environ.get('SCONS_VERSION', "2.5.0") -mongodb_root = os.path.dirname(os.path.dirname(__file__)) -scons_dir = os.path.join(mongodb_root, 'src', 'third_party', 'scons-' + SCONS_VERSION) +mongodb_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +scons_dir = os.path.join(mongodb_root, 'src', 'third_party','scons-' + SCONS_VERSION, + 'scons-local-' + SCONS_VERSION) + +if not os.path.exists(scons_dir): + print("Could not find SCons in '%s'" % (scons_dir)) + sys.exit(1) + +sys.path = [scons_dir] + sys.path + +try: + import SCons.Script +except ImportError: + print("Could not find SCons in '%s'" % (scons_dir)) + sys.exit(1) -if sys.platform == 'win32': - args = [sys.executable, os.path.join(scons_dir, 'scons.py')] + sys.argv[1:] - sys.exit(subprocess.call(args)) -else: - os.execv(os.path.join(scons_dir, 'scons.py'), sys.argv) +SCons.Script.main() diff --git a/buildscripts/tests/resmokelib/logging/test_buildlogger.py b/buildscripts/tests/resmokelib/logging/test_buildlogger.py new file mode 100644 index 00000000000..7bc705948db --- /dev/null +++ b/buildscripts/tests/resmokelib/logging/test_buildlogger.py @@ -0,0 +1,67 @@ +"""Unit tests for the buildscripts.resmokelib.logging.buildlogger module.""" + +from __future__ import absolute_import + +import json +import unittest + +from buildscripts.resmokelib.logging import buildlogger + + +class TestLogsSplitter(unittest.TestCase): + """Unit tests for the _LogsSplitter class.""" + + def test_split_no_logs(self): + logs = [] + max_size = 10 + self.assertEqual([[]], buildlogger._LogsSplitter.split_logs(logs, max_size)) + + def test_split_no_max_size(self): + logs = self.__generate_logs(size=30) + max_size = None + self.assertEqual([logs], buildlogger._LogsSplitter.split_logs(logs, max_size)) + + def test_split_max_size_smaller(self): + logs = self.__generate_logs(size=20) + max_size = 30 + self.assertEqual([logs], buildlogger._LogsSplitter.split_logs(logs, max_size)) + + def test_split_max_size_equal(self): + logs = self.__generate_logs(size=30) + max_size = 30 + self.assertEqual([logs], buildlogger._LogsSplitter.split_logs(logs, max_size)) + + def test_split_max_size_larger(self): + logs = self.__generate_logs(size=31) + max_size = 30 + self.assertEqual( + [logs[0:-1], logs[-1:]], + buildlogger._LogsSplitter.split_logs(logs, max_size)) + + logs = self.__generate_logs(size=149) + max_size = 19 + self.assertEqual( + [logs[0:3], logs[3:6], logs[6:9], logs[9:12], logs[12:15], + logs[15:18], logs[18:21], logs[21:24], logs[24:27], logs[27:]], + buildlogger._LogsSplitter.split_logs(logs, max_size)) + + def check_split_sizes(self, splits, max_size): + for split in splits: + self.assertTrue(TestLogsSplitter.size(split) <= max_size) + + def __generate_logs(self, size): + # The size of [ "x" ] is 5. This is the minimum size we generate. + self.assertTrue(size >= 5) + # Each new "x" adds 5 to the size. + nb_lines = size / 5 + # Each additional "x" on a line adds 1 to the size. + last_line_extra = size % 5 + logs = ["x"] * nb_lines + logs[-1] += "x" * last_line_extra + self.assertEqual(size, TestLogsSplitter.size(logs)) + return logs + + @staticmethod + def size(logs): + """Returns the size of the log lines when represented in JSON.""" + return len(json.dumps(logs, encoding="utf-8")) diff --git a/distsrc/README b/distsrc/README index 8cc83742981..5a772b66e7d 100644 --- a/distsrc/README +++ b/distsrc/README @@ -1,22 +1,30 @@ MongoDB README
-Welcome to MongoDB! - +Welcome to MongoDB 3.4!
+
COMPONENTS
- bin/mongod - The database process.
- bin/mongos - Sharding controller.
- bin/mongo - The database shell (uses interactive javascript).
- -UTILITIES - - bin/mongodump - MongoDB dump tool - for backups, snapshots, etc.. - bin/mongorestore - MongoDB restore a dump - bin/mongoexport - Export a single collection to test (JSON, CSV) - bin/mongoimport - Import from JSON or CSV - bin/mongofiles - Utility for putting and getting files from MongoDB GridFS - bin/mongostat - Show performance statistics - + mongod - The database server.
+ mongos - Sharding router.
+ mongo - The database shell (uses interactive javascript).
+
+UTILITIES
+
+ mongodump - Create a binary dump of the contents of a database.
+ mongorestore - Restore data from the output created by mongodump.
+ mongoexport - Export the contents of a collection to JSON or CSV.
+ mongoimport - Import data from JSON, CSV or TSV.
+ mongofiles - Put, get and delete files from GridFS.
+ mongostat - Show the status of a running mongod/mongos.
+ bsondump - Convert BSON files into human-readable formats.
+ mongooplog - Poll the oplog and apply to a local server.
+ mongoreplay - Traffic capture and replay tool.
+ mongotop - Track time spent reading and writing data.
+
+BUILDING
+
+ See docs/building.md.
+
RUNNING
For command line options invoke:
@@ -25,28 +33,52 @@ RUNNING To run a single server database:
- $ mkdir /data/db
+ $ sudo mkdir -p /data/db
$ ./mongod
$
$ # The mongo javascript shell connects to localhost and test database by default:
- $ ./mongo
+ $ ./mongo
> help
DRIVERS
- Client drivers for most programming languages are available at mongodb.org. Use the - shell ("mongo") for administrative tasks.
+ Client drivers for most programming languages are available at
+ https://docs.mongodb.com/manual/applications/drivers/. Use the shell
+ ("mongo") for administrative tasks.
+
+BUG REPORTS
+
+ See https://github.com/mongodb/mongo/wiki/Submit-Bug-Reports.
+
+PACKAGING
+
+ Packages are created dynamically by the package.py script located in the
+ buildscripts directory. This will generate RPM and Debian packages.
DOCUMENTATION
- http://www.mongodb.org/ - -MAIL LISTS AND IRC - - http://dochub.mongodb.org/core/community
+ https://docs.mongodb.com/manual/
+
+CLOUD HOSTED MONGODB
+
+ https://www.mongodb.com/cloud/atlas
+
+MAIL LISTS
+
+ https://groups.google.com/forum/#!forum/mongodb-user
+
+ A forum for technical questions about using MongoDB.
+
+ https://groups.google.com/forum/#!forum/mongodb-dev
+
+ A forum for technical questions about building and developing MongoDB.
+
+LEARN MONGODB
+
+ https://university.mongodb.com/
-32 BIT BUILD NOTES
+LICENSE
- MongoDB uses memory mapped files. If built as a 32 bit executable, you will
- not be able to work with large (multi-gigabyte) databases. However, 32 bit
- builds work fine with small development databases.
+ Most MongoDB source files (src/mongo folder and below) are made available
+ under the terms of the GNU Affero General Public License (GNU AGPLv3). See
+ individual files for details.
diff --git a/docs/building.md b/docs/building.md index 6008a9b606f..48f1a188396 100644 --- a/docs/building.md +++ b/docs/building.md @@ -4,7 +4,7 @@ Building MongoDB To build MongoDB, you will need: * A modern C++ compiler. One of the following is required. - * GCC 4.8.2 or newer + * GCC 5.3.0 or newer * Clang 3.4 (or Apple XCode 5.1.1 Clang) or newer * Visual Studio 2013 Update 2 or newer * Python 2.7 diff --git a/etc/drivers_nightly.yml b/etc/drivers_nightly.yml index f039112ea85..464f844ac57 100644 --- a/etc/drivers_nightly.yml +++ b/etc/drivers_nightly.yml @@ -3,7 +3,7 @@ ##################################################### ### Instructions for Adding a Driver ### -# The Major sections are: +## The Major sections are: # VARIABLES # FUNCTIONS (MongoDB and Drivers) @@ -218,8 +218,9 @@ functions: - command: shell.exec type: test params: - working_dir: "src/${DRIVER_WORKING_DIRECTORY}" + working_dir: src script: | + cd ${DRIVER_WORKING_DIRECTORY} ${PREPARE_SHELL} echo $DRIVERS_TOOLS echo $MONGO_ORCHESTRATION_HOME @@ -318,8 +319,9 @@ functions: "setup drivers environment": - command: shell.exec params: - working_dir: "src/${DRIVER_WORKING_DIRECTORY}" + working_dir: src script: | + cd "${DRIVER_WORKING_DIRECTORY}" # Get the current unique version of this checkout if [ "${is_patch}" = "true" ]; then CURRENT_VERSION=$(git describe)-patch-${version_id} @@ -433,10 +435,7 @@ tasks: if [ `which strip` ]; then echo "found strip" find . -maxdepth 1 -type f -iname "mongo*" -exec strip {} \; - strip mongo/mongo fi - # On RHEL, the mongo shell binary is in a mongo directory - mv mongo/mongo ./mongodb-binaries/bin/ || true find . -maxdepth 1 -type f -iname "mongo*" -exec mv {} ./mongodb-binaries/bin/ \; ${compress} mongodb-binaries.${ext|tgz} mongodb-binaries/ @@ -584,7 +583,6 @@ axes: display_name: "Python 2.6" variables: ### There is no python toolchain on RHEL 6.2, using system python - ### PYTHON_BINARY: "/opt/python/2.6/bin/python" PYTHON_BINARY: "/usr/bin/python" DRIVER_WORKING_DIRECTORY: "mongo-python-driver" DRIVER_TEST_UPLOAD_DIRECTORY: "src/mongo-python-driver/xunit-results/TEST-*.xml" @@ -629,7 +627,7 @@ modules: - name: mongo-python-driver repo: git@github.com:mongodb/mongo-python-driver.git - branch: evergreen + branch: master ####################################### # BUILDVARIANTS # diff --git a/etc/evergreen.yml b/etc/evergreen.yml index fae0563fbde..520a49b77f5 100644 --- a/etc/evergreen.yml +++ b/etc/evergreen.yml @@ -113,7 +113,7 @@ variables: - enterprise-debian71-64 - enterprise-debian81-64 - enterprise-linux-64-amazon-ami - - enterprise-osx-107 + - enterprise-osx-108 - enterprise-rhel-62-64-bit - enterprise-rhel-62-64-bit-coverage - enterprise-rhel-62-64-bit-inmem @@ -145,17 +145,15 @@ variables: - linux-64-ephemeralForTest - linux-64-lsm - linux-64-repeated-execution - - osx-107 - - osx-107-debug - - osx-107-ssl + - osx-108 + - osx-108-debug + - osx-108-ssl - rhel62 - rhel70 - - solaris-64-bit - suse11 - suse12 - ubuntu1204 - ubuntu1404 - - ubuntu1404-rocksdb - ubuntu1604 - ubuntu1604-arm64 - ubuntu1604-asan @@ -238,9 +236,6 @@ functions: if [ "Windows_NT" = "$OS" ]; then typeperf -qx PhysicalDisk | grep Disk | grep -v _Total > disk_counters.txt typeperf -cf disk_counters.txt -si 5 -o mongo-diskstats - # Solaris: iostat -T d option for timestamp. - elif iostat -T d -Mx > /dev/null 2>&1; then - iostat -T d -Mx 5 > mongo-diskstats # Linux: iostat -t option for timestamp. elif iostat -tdmx > /dev/null 2>&1; then iostat -tdmx 5 > mongo-diskstats @@ -285,11 +280,8 @@ functions: get_pids() { proc_pids=$(pgrep $1); } get_process_info() { proc_name=$(ps -p $1 -o comm=); - # prstat is available on Solaris - if [ ! -z $(which prstat 2> /dev/null) ]; then - proc_threads=$(prstat -p $1 1 1 | grep $1 | cut -f2 -d "/"); # /proc is available on Linux platforms - elif [ -f /proc/$1/status ]; then + if [ -f /proc/$1/status ]; then ${set_sudo} proc_threads=$($sudo grep Threads /proc/$1/status | sed "s/\s//g" | cut -f2 -d ":"); else @@ -365,19 +357,6 @@ functions: mv mongodb*/mongo.{debug,dSYM,pdb} mongodb*/mongod.{debug,dSYM,pdb} mongodb*/mongos.{debug,dSYM,pdb} . 2>/dev/null || true rm -r mongodb*/*.{debug,dSYM,pdb} mongo-debugsymbols.tgz 2>/dev/null || true - "build rocksdb" : - command: shell.exec - params: - script: | - set -o errexit - set -o verbose - if [ "${build_rocksdb|}" = "true" ]; then - rm -rf rocksdb - git clone https://github.com/facebook/rocksdb.git - cd rocksdb - make CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ static_lib - fi - "build new tools" : command: shell.exec params: @@ -441,6 +420,13 @@ functions: set -o errexit set -o verbose + # Set the TMPDIR environment variable to be a directory in the task's working + # directory so that temporary files created by processes spawned by resmoke.py get + # cleaned up after the task completes. This also ensures the spawned processes + # aren't impacted by limited space in the mount point for the /tmp directory. + export TMPDIR="${workdir}/tmp" + mkdir -p $TMPDIR + # check if virtualenv is set up if [ -d "venv" ]; then if [ "Windows_NT" = "$OS" ]; then @@ -524,7 +510,7 @@ functions: path_value="$path_value:${task_path_suffix}" fi - ${resmoke_wrapper} PATH="$path_value" ${san_symbolizer} ${san_options} ${rlp_environment} ${python|/opt/mongodbtoolchain/v2/bin/python2} buildscripts/resmoke.py ${resmoke_args} $extra_args ${test_flags} --log=buildlogger --reportFile=report.json + ${resmoke_wrapper} PATH="$path_value" ${san_symbolizer} ${lang_environment} ${san_options} ${rlp_environment} ${python|/opt/mongodbtoolchain/v2/bin/python2} buildscripts/resmoke.py ${resmoke_args} $extra_args ${test_flags} --log=buildlogger --reportFile=report.json "do jepsen setup" : @@ -852,10 +838,18 @@ functions: script: | set -o errexit + # Override the aws credentials with the kitchen specific credentials + cat <<EOF > ~/.aws/credentials + [default] + aws_access_key_id = ${kitchen_aws_key} + aws_secret_access_key = ${kitchen_aws_secret} + EOF + export KITCHEN_ARTIFACTS_URL="https://s3.amazonaws.com/mciuploads/${project}/${build_variant}/${revision}/artifacts/${build_id}.tgz" export KITCHEN_SECURITY_GROUP="${kitchen_security_group}" export KITCHEN_SSH_KEY_ID="${kitchen_ssh_key_id}" export KITCHEN_SUBNET="${kitchen_subnet}" + export KITCHEN_VPC="${kitchen_vpc}" for i in {1..3} do @@ -1125,6 +1119,12 @@ post: if [ ! -f $new_binary_file ]; then mv $binary_file_location $new_binary_file fi + # On Windows if a .pdb symbol file exists, include it in the archive. + pdb_file=$(echo $binary_file_location | sed "s/\.exe/.pdb/") + if [ -f $pdb_file ]; then + new_pdb_file=unittest_binaries/$(echo $pdb_file | sed "s/.*\///") + mv $pdb_file $new_pdb_file + fi done done - command: archive.targz_pack @@ -1239,14 +1239,12 @@ tasks: directory: src revisions: # for each module include revision as <module_name> : ${<module_name>_rev} enterprise: ${enterprise_rev} - rocksdb: ${rocksdb_rev} - command: git.apply_patch params: directory: src - func: "get buildnumber" - func: "setup credentials" - func: "build new tools" # noop if ${newtools} is not "true" - - func: "build rocksdb" # noop if ${build_rocksdb} is not "true" - command: shell.exec params: working_dir: src @@ -1295,14 +1293,6 @@ tasks: ${strip_command|/usr/bin/strip} mongobridge fi - # If this is a scheduled build, we check for changes against the last scheduled commit. - if [ "${is_patch}" != "true" ]; then - burn_in_args="--checkEvergreen" - fi - - # Capture a list of new and modified tests. - ${python|/opt/mongodbtoolchain/v2/bin/python2} buildscripts/burn_in_tests.py --branch=${branch_name} --buildVariant=${build_variant} --testListOutfile=jstests/new_tests.json --noExec $burn_in_args - # On windows we need to make sure the paths in unittests.txt are compatible with cygwin tar sed 's|\\|/|g' build/unittests.txt > build/unittests-tarlist.txt ${tar|tar} -czvf mongodb-unittests.tgz -T build/unittests-tarlist.txt build/unittests.txt @@ -1421,7 +1411,6 @@ tasks: directory: src revisions: # for each module include revision as <module_name> : ${<module_name>_rev} enterprise: ${enterprise_rev} - rocksdb: ${rocksdb_rev} - command: git.apply_patch params: directory: src @@ -1440,7 +1429,38 @@ tasks: depends_on: - name: compile commands: + - command: git.get_project + # The repository is cloned in a directory distinct from src for the modified test detection + # because the extraction of the artifacts performed in the 'do setup' causes + # 'git diff --name-only' to see all tests as modified on Windows (git 1.9.5). See SERVER-30634. + params: + directory: burn_in_tests_clonedir + revisions: # for each module include revision as <module_name> : ${<module_name>_rev} + enterprise: ${enterprise_rev} + - command: shell.exec - func: "do setup" + - func: "set up virtualenv" + - command: shell.exec + params: + working_dir: burn_in_tests_clonedir + script: | + set -o errexit + # Create a symbolic link to the venv in the src directory so activate_virtualenv can use it + # if it exists. + ln -s ../src/venv venv + ${activate_virtualenv} + set -o verbose + # If this is a scheduled build, we check for changes against the last scheduled commit. + if [ "${is_patch}" != "true" ]; then + burn_in_args="--checkEvergreen" + fi + # Copy the dbtest executable from the src dir because burn_in_tests.py calls it to get the + # list of dbtest suites. + cp ../src/dbtest${exe} . + # Capture a list of new and modified tests. + ${python|/opt/mongodbtoolchain/v2/bin/python2} buildscripts/burn_in_tests.py --branch=${branch_name} --buildVariant=${build_variant} --testListOutfile=jstests/new_tests.json --noExec $burn_in_args + # Copy the results to the src dir. + cp jstests/new_tests.json ../src/jstests/new_tests.json - func: "do multiversion setup" - func: "run tests" vars: @@ -2717,6 +2737,17 @@ tasks: run_multiple_jobs: true - <<: *task_template + name: write_concern_majority_passthrough_WT + depends_on: + - name: jsCore_WT + commands: + - func: "do setup" + - func: "run tests" + vars: + resmoke_args: --suites=write_concern_majority_passthrough --storageEngine=wiredTiger + run_multiple_jobs: true + +- <<: *task_template name: replica_sets commands: - func: "do setup" @@ -3097,7 +3128,7 @@ tasks: /usr/local/bin/notary-client.py --key-name "server-3.4" --auth-token-file ${workdir}/src/signing_auth_token --comment "Evergreen Automatic Signing ${revision} - ${build_variant} - ${branch_name}" --notary-url http://notary-service.build.10gen.cc:5000 --skip-missing mongodb-${push_name}-${push_arch}-${suffix}.${ext|tgz} mongodb-shell-${push_name}-${push_arch}-${suffix}.${ext|tgz} mongodb-${push_name}-${push_arch}-debugsymbols-${suffix}.${ext|tgz} mongodb-win32-${push_arch}-${suffix}.msi mongodb-src-${src_suffix}.${ext|tar.gz} if [ "${has_packages|}" = "true" ]; then - CURATOR_RELEASE="88f34a9f1c79db7ea9597b6e85eb5995d03714ba" + CURATOR_RELEASE="ea8d75dcc1a587111e7418e2428fb67e267af9fe" curl -L -O http://boxes.10gen.com/build/curator/curator-dist-rhel70-$CURATOR_RELEASE.tar.gz tar -zxvf curator-dist-rhel70-$CURATOR_RELEASE.tar.gz ./curator repo --config ./etc/repo_config.yaml --distro ${packager_distro} --edition ${repo_edition} --version ${version} --arch ${packager_arch} --packages repo @@ -3513,11 +3544,6 @@ modules: prefix: src/mongo/db/modules branch: v3.4 -- name: rocksdb - repo: git@github.com:mongodb-partners/mongo-rocks.git - prefix: src/mongo/db/modules - branch: v3.4 - ####################################### # Buildvariants # ####################################### @@ -3650,6 +3676,7 @@ buildvariants: - name: tool - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: push - name: linux-64-repeated-execution @@ -3767,6 +3794,7 @@ buildvariants: - name: tool - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: linux-64-duroff display_name: Linux (No Journal) @@ -4159,6 +4187,7 @@ buildvariants: push_bucket: downloads.mongodb.org push_name: linux push_arch: x86_64-ubuntu1604 + lang_environment: LANG=C compile_flags: --ssl MONGO_DISTMOD=ubuntu1604 -j$(grep -c ^processor /proc/cpuinfo) --release CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy multiversion_platform_arch: "ubuntu1604" multiversion_edition: "targeted" @@ -4283,6 +4312,7 @@ buildvariants: push_arch: arm64-enterprise-ubuntu1604 compile_flags: --ssl MONGO_DISTMOD=ubuntu1604 -j$(grep -c ^processor /proc/cpuinfo) CCFLAGS="-march=armv8-a+crc -mtune=generic" --release CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy num_jobs_available: $(grep -c ^processor /proc/cpuinfo) + max_jobs: 8 # Avoid starting too many mongod's on ARM test servers variant_excluded_flags: requires_mmapv1 has_packages: true packager_script: packager-enterprise.py @@ -4370,6 +4400,7 @@ buildvariants: push_arch: arm64-ubuntu1604 compile_flags: --ssl MONGO_DISTMOD=ubuntu1604 -j$(grep -c ^processor /proc/cpuinfo) --release CCFLAGS="-march=armv8-a+crc -mtune=generic" CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy num_jobs_available: $(grep -c ^processor /proc/cpuinfo) + max_jobs: 8 # Avoid starting too many mongod's on ARM test servers variant_excluded_flags: requires_mmapv1 has_packages: true packager_script: packager.py @@ -4381,62 +4412,8 @@ buildvariants: - name: compile distros: - ubuntu1604-arm64-large - - name: aggregation_WT - - name: aggregation_auth - - name: auth_WT - name: dbtest_WT - - name: failpoints - - name: failpoints_auth - - name: gle_auth_WT - - name: gle_auth_write_cmd_WT - - name: gle_auth_basics_passthrough_WT - - name: gle_auth_basics_passthrough_write_cmd_WT - - name: sharding_gle_auth_basics_passthrough_WT - - name: sharding_gle_auth_basics_passthrough_write_cmd_WT - name: jsCore_WT - - name: jsCore_compatibility_WT - - name: jsCore_decimal_WT - - name: jstestfuzz_WT - - name: jstestfuzz_concurrent_WT - - name: jstestfuzz_concurrent_replication_WT - - name: jstestfuzz_concurrent_sharded_WT - - name: jstestfuzz_replication_WT - - name: jstestfuzz_sharded_WT - - name: mongosTest - - name: noPassthrough_WT - - name: noPassthroughWithMongod_WT - - name: bulk_gle_passthrough_WT - - name: parallel_WT - - name: parallel_compatibility_WT - - name: concurrency_WT - distros: - # There is a known performance issue with the ubuntu1604-arm64-1.linaro.build.10gen.cc host - # that's part of the ubuntu1604-arm64-large pool. We run the concurrency tests only on the - # hosts from the ubuntu1604-arm64-small pool to work around this issue. - - ubuntu1604-arm64-small - - name: concurrency_replication_WT - distros: - - ubuntu1604-arm64-small - - name: concurrency_sharded_WT - distros: - - ubuntu1604-arm64-small - - name: concurrency_simultaneous_WT - distros: - - ubuntu1604-arm64-small - - name: replica_sets_WT - - name: replica_sets_auth - - name: replica_sets_jscore_passthrough_WT - - name: master_slave_WT - - name: master_slave_auth - - name: master_slave_jscore_passthrough_WT - - name: sharding_WT - - name: sharding_auth - - name: slow1_WT - - name: serial_run_WT - - name: sharding_jscore_passthrough_WT - - name: ssl - - name: sslSpecial - - name: tool_WT - name: unittests - name: push distros: @@ -4529,6 +4506,7 @@ buildvariants: - ubuntu1604-zseries-large - ubuntu1604-zseries-small batchtime: 1440 # 1 day + stepback: false expansions: gorootvars: PATH=/opt/mongodbtoolchain/v2/bin:$PATH tooltags: -gccgoflags "$(pkg-config --libs --cflags libssl libcrypto libsasl2)" -tags 'sasl ssl' @@ -4536,7 +4514,7 @@ buildvariants: push_bucket: downloads.10gen.com push_name: linux push_arch: s390x-enterprise-ubuntu1604 - compile_flags: --ssl MONGO_DISTMOD=ubuntu1604 --release -j$(grep -c ^processor /proc/cpuinfo) CCFLAGS="-march=z196 -mtune=zEC12" CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy + compile_flags: --ssl MONGO_DISTMOD=ubuntu1604 --release -j3 CCFLAGS="-march=z196 -mtune=zEC12" CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy num_jobs_available: 2 variant_excluded_flags: requires_mmapv1 has_packages: true @@ -4621,6 +4599,7 @@ buildvariants: - name: sslSpecial - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: push distros: - ubuntu1604-test @@ -5055,6 +5034,7 @@ buildvariants: - name: tool - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: push distros: - rhel70-small @@ -5194,6 +5174,9 @@ buildvariants: - name: tool - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT + distros: + - windows-64-vs2015-large - name: enterprise-windows-64 display_name: "* Enterprise Windows" @@ -5447,8 +5430,8 @@ buildvariants: # OSX buildvariants # ########################################### -- name: osx-107 - display_name: OS X 10.7 +- name: osx-108 + display_name: OS X 10.8 run_on: - macos-1012 batchtime: 1440 # 1 day @@ -5457,8 +5440,8 @@ buildvariants: push_bucket: downloads.mongodb.org push_name: osx push_arch: x86_64 - gorootvars: CGO_CFLAGS=-mmacosx-version-min=10.7 CGO_LDFLAGS=-mmacosx-version-min=10.7 - compile_flags: --allocator=system -j$(sysctl -n hw.logicalcpu) --release --osx-version-min=10.7 --libc++ + gorootvars: CGO_CFLAGS=-mmacosx-version-min=10.8 CGO_LDFLAGS=-mmacosx-version-min=10.8 + compile_flags: --allocator=system -j$(sysctl -n hw.logicalcpu) --release --osx-version-min=10.8 --libc++ python: python2.7 num_jobs_available: 1 build_mongoreplay: true @@ -5547,8 +5530,8 @@ buildvariants: distros: - rhel70-small -- name: osx-107-ssl - display_name: SSL OS X 10.7 +- name: osx-108-ssl + display_name: SSL OS X 10.8 run_on: - macos-1012 batchtime: 1440 # 1 day @@ -5558,8 +5541,8 @@ buildvariants: push_name: osx-ssl push_arch: x86_64 tooltags: "-tags ssl" - gorootvars: CGO_CPPFLAGS=-I/opt/mongodbtoolchain/v2/include CGO_CFLAGS=-mmacosx-version-min=10.7 CGO_LDFLAGS=-mmacosx-version-min=10.7 - compile_flags: --ssl --allocator=system -j$(sysctl -n hw.logicalcpu) --release --osx-version-min=10.7 --libc++ CPPPATH=/opt/mongodbtoolchain/v2/include + gorootvars: CGO_CPPFLAGS=-I/opt/mongodbtoolchain/v2/include CGO_CFLAGS=-mmacosx-version-min=10.8 CGO_LDFLAGS=-mmacosx-version-min=10.8 + compile_flags: --ssl --allocator=system -j$(sysctl -n hw.logicalcpu) --release --osx-version-min=10.8 --libc++ CPPPATH=/opt/mongodbtoolchain/v2/include python: python2.7 num_jobs_available: 1 build_mongoreplay: true @@ -5644,12 +5627,13 @@ buildvariants: - name: tool - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: push distros: - rhel70-small -- name: osx-107-debug - display_name: "* OS X 10.7 DEBUG" +- name: osx-108-debug + display_name: "* OS X 10.8 DEBUG" run_on: - macos-1012 expansions: @@ -5658,8 +5642,8 @@ buildvariants: push_name: osx-debug push_arch: x86_64 num_jobs_available: 1 - gorootvars: CGO_CFLAGS=-mmacosx-version-min=10.7 CGO_LDFLAGS=-mmacosx-version-min=10.7 - compile_flags: --dbg=on --opt=on --allocator=system -j$(sysctl -n hw.logicalcpu) --osx-version-min=10.7 --libc++ + gorootvars: CGO_CFLAGS=-mmacosx-version-min=10.8 CGO_LDFLAGS=-mmacosx-version-min=10.8 + compile_flags: --dbg=on --opt=on --allocator=system -j$(sysctl -n hw.logicalcpu) --osx-version-min=10.8 --libc++ python: python2.7 build_mongoreplay: true tasks: @@ -5689,8 +5673,8 @@ buildvariants: - name: tool_WT - name: unittests -- name: enterprise-osx-107 - display_name: Enterprise OS X 10.7 +- name: enterprise-osx-108 + display_name: Enterprise OS X 10.8 modules: - enterprise run_on: @@ -5702,8 +5686,8 @@ buildvariants: push_name: osx push_arch: x86_64-enterprise tooltags: "-tags 'ssl sasl'" - gorootvars: CGO_CPPFLAGS=-I/opt/mongodbtoolchain/v2/include CGO_CFLAGS=-mmacosx-version-min=10.7 CGO_LDFLAGS=-mmacosx-version-min=10.7 - compile_flags: --ssl --allocator=system -j$(sysctl -n hw.logicalcpu) --release --osx-version-min=10.7 --libc++ CPPPATH=/opt/mongodbtoolchain/v2/include + gorootvars: CGO_CPPFLAGS=-I/opt/mongodbtoolchain/v2/include CGO_CFLAGS=-mmacosx-version-min=10.8 CGO_LDFLAGS=-mmacosx-version-min=10.8 + compile_flags: --ssl --allocator=system -j$(sysctl -n hw.logicalcpu) --release --osx-version-min=10.8 --libc++ CPPPATH=/opt/mongodbtoolchain/v2/include python: python2.7 num_jobs_available: 1 build_mongoreplay: true @@ -5961,6 +5945,9 @@ buildvariants: - name: tool - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT + distros: + - rhel62-large - name: package distros: - ubuntu1604-packer @@ -6113,6 +6100,7 @@ buildvariants: - name: ssl - name: sslSpecial - name: unittests + - name: write_concern_majority_passthrough_WT - name: enterprise-rhel-70-64-bit display_name: Enterprise RHEL 7.0 @@ -6526,6 +6514,7 @@ buildvariants: - name: sslSpecial - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: push distros: - rhel70-small @@ -6628,6 +6617,7 @@ buildvariants: - name: sslSpecial - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: push distros: - rhel70-small @@ -6763,6 +6753,7 @@ buildvariants: # - name: tool - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: push distros: - rhel62-small @@ -6921,6 +6912,7 @@ buildvariants: push_path: linux push_bucket: downloads.10gen.com push_name: linux + lang_environment: LANG=C push_arch: x86_64-enterprise-ubuntu1604 compile_flags: --ssl MONGO_DISTMOD=ubuntu1604 --release -j$(grep -c ^processor /proc/cpuinfo) CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy num_jobs_available: $(grep -c ^processor /proc/cpuinfo) @@ -7053,6 +7045,7 @@ buildvariants: - suse12-zseries-build - suse12-zseries-test batchtime: 1440 # 1 day + stepback: false expansions: gorootvars: PATH=/opt/mongodbtoolchain/v2/bin:$PATH tooltags: -gccgoflags "$(pkg-config --libs --cflags libssl libsasl2)" -tags 'sasl ssl' @@ -7060,7 +7053,7 @@ buildvariants: push_bucket: downloads.10gen.com push_name: linux push_arch: s390x-enterprise-suse12 - compile_flags: --ssl MONGO_DISTMOD=suse12 --release -j$(( $(grep -c ^processor /proc/cpuinfo) / 2 )) CCFLAGS="-march=z196 -mtune=zEC12" CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy + compile_flags: --ssl MONGO_DISTMOD=suse12 --release -j3 CCFLAGS="-march=z196 -mtune=zEC12" CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy num_jobs_available: $(grep -c ^processor /proc/cpuinfo) variant_excluded_flags: requires_mmapv1 has_packages: true @@ -7142,6 +7135,7 @@ buildvariants: - name: sslSpecial - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: push distros: - suse12-test @@ -7327,6 +7321,7 @@ buildvariants: run_on: - suse11-zseries-build batchtime: 1440 # 1 day + stepback: false expansions: gorootvars: PATH=/opt/mongodbtoolchain/v2/bin:$PATH tooltags: -gccgoflags "$(pkg-config --libs --cflags libssl libsasl2)" -tags 'sasl ssl' @@ -7334,7 +7329,7 @@ buildvariants: push_bucket: downloads.10gen.com push_name: linux push_arch: s390x-enterprise-suse11 - compile_flags: --ssl MONGO_DISTMOD=suse11 --release -j$(grep -c ^processor /proc/cpuinfo) CCFLAGS="-march=z9-109 -mtune=z10" CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy --use-s390x-crc32=off + compile_flags: --ssl MONGO_DISTMOD=suse11 --release -j3 CCFLAGS="-march=z9-109 -mtune=z10" CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy --use-s390x-crc32=off num_jobs_available: $(grep -c ^processor /proc/cpuinfo) variant_excluded_flags: requires_mmapv1 has_packages: true @@ -7413,6 +7408,7 @@ buildvariants: - name: sslSpecial - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: push distros: - suse11-test @@ -7535,94 +7531,6 @@ buildvariants: ########################################### -# Solaris buildvariants # -########################################### - -- name: solaris-64-bit - display_name: "* Solaris" - run_on: - - solaris - expansions: - push_path: sunos5 - push_bucket: downloads.mongodb.org - push_name: sunos5 - push_arch: x86_64 - gorootvars: PATH=/opt/mongodbtoolchain/v2/bin:$PATH - tooltags: -gccgoflags "-lsocket -lnsl" - compile_flags: CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy -j$(kstat cpu | sort -u | grep -c "^module") --release CCFLAGS="-m64" LINKFLAGS="-m64 -static-libstdc++ -static-libgcc" OBJCOPY=/opt/mongodbtoolchain/bin/objcopy - num_jobs_available: $(( $(kstat cpu | sort -u | grep -c "^module") / 2 )) - use_scons_cache: true - tasks: - - name: compile - - name: aggregation - - name: aggregation_WT - - name: auth - - name: auth_WT - - name: bulk_gle_passthrough - - name: bulk_gle_passthrough_WT - - name: concurrency - - name: concurrency_WT - - name: concurrency_replication - - name: concurrency_replication_WT - - name: concurrency_sharded - - name: concurrency_sharded_WT - - name: concurrency_simultaneous - - name: concurrency_simultaneous_WT - - name: dbtest - - name: dbtest_WT - - name: disk - - name: durability - - name: failpoints - - name: httpinterface - - name: jsCore - - name: jsCore_compatibility - - name: jsCore_compatibility_WT - - name: jsCore_decimal - - name: jsCore_decimal_WT - - name: jsCore_WT - - name: jstestfuzz - - name: jstestfuzz_WT - - name: jstestfuzz_replication - - name: jstestfuzz_replication_WT - - name: jstestfuzz_sharded - - name: jstestfuzz_sharded_WT - - name: mmap - - name: mongosTest - - name: noPassthrough - - name: noPassthroughWithMongod - - name: noPassthroughWithMongod_WT - - name: noPassthrough_WT - - name: parallel - - name: parallel_compatibility - - name: parallel_compatibility_WT - - name: parallel_WT - - name: replica_sets - - name: replica_sets_WT - - name: replica_sets_jscore_passthrough - - name: replica_sets_jscore_passthrough_WT - - name: master_slave - - name: master_slave_WT - - name: master_slave_jscore_passthrough - - name: master_slave_jscore_passthrough_WT - - name: sharding - - name: sharded_collections_jscore_passthrough - - name: sharded_collections_jscore_passthrough_WT - - name: sharding_jscore_passthrough - - name: sharding_jscore_passthrough_WT - - name: sharding_jscore_passthrough_wire_ops_WT - - name: sharding_WT - - name: slow1 - - name: slow1_WT - - name: serial_run - - name: serial_run_WT - - name: tool - - name: tool_WT - - name: unittests - - name: push - distros: - - rhel70-small - -########################################### # Debian buildvariants # ########################################### @@ -8075,6 +7983,7 @@ buildvariants: - name: sslSpecial - name: tool - name: unittests + - name: write_concern_majority_passthrough_WT - name: linux-64-ephemeralForTest display_name: Linux (ephemeralForTest) @@ -8232,6 +8141,7 @@ buildvariants: - name: sslSpecial - name: tool - name: unittests + - name: write_concern_majority_passthrough_WT - name: enterprise-rhel-72-s390x-inmem display_name: Enterprise RHEL 7.2 s390x (inMemory) DEBUG @@ -8319,75 +8229,7 @@ buildvariants: - name: sslSpecial - name: tool - name: unittests - -- name: ubuntu1404-rocksdb - display_name: Ubuntu 14.04 (RocksDB) - modules: - - rocksdb - run_on: - - ubuntu1404-test - batchtime: 1440 # 1 day - expansions: - build_rocksdb: true - compile_flags: -j$(grep -c ^processor /proc/cpuinfo) --dbg=off --opt=on CPPPATH=$(readlink -f ../rocksdb/include/) LIBPATH=$(readlink -f ../rocksdb/) LIBS=rocksdb CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy VARIANT_DIR=release --runtime-hardening=off - num_jobs_available: $(grep -c ^processor /proc/cpuinfo) - test_flags: --storageEngine=rocksdb - use_scons_cache: true - build_mongoreplay: true - tasks: - - name: compile - distros: - - ubuntu1404-build - - name: aggregation - - name: aggregation_WT - - name: aggregation_auth - - name: auth - - name: dbtest - # - name: disk - # - name: durability - # - name: dur_jscore_passthrough - - name: failpoints - - name: failpoints_auth - - name: gle_auth - - name: gle_auth_write_cmd - - name: gle_auth_basics_passthrough - - name: gle_auth_basics_passthrough_write_cmd - - name: sharding_gle_auth_basics_passthrough - - name: sharding_gle_auth_basics_passthrough_write_cmd - - name: jsCore - - name: jsCore_auth - - name: jsCore_compatibility - - name: jstestfuzz - - name: jstestfuzz_concurrent_WT - - name: jstestfuzz_concurrent_replication_WT - - name: jstestfuzz_concurrent_sharded_WT - - name: jstestfuzz_replication - - name: jstestfuzz_sharded - - name: noPassthrough - - name: noPassthroughWithMongod - - name: bulk_gle_passthrough - - name: parallel - - name: parallel_compatibility - - name: concurrency - - name: concurrency_replication - - name: concurrency_sharded - # We use machines with more RAM to avoid OOM issues when validating collections. - distros: - - ubuntu1404-build - - name: concurrency_simultaneous - - name: replica_sets - - name: replica_sets_auth - - name: replica_sets_jscore_passthrough - - name: master_slave - - name: master_slave_WT - - name: master_slave_auth - - name: master_slave_jscore_passthrough - - name: sharding - - name: sharding_auth - - name: slow1 - - name: serial_run - - name: sharding_jscore_passthrough - - name: unittests + - name: write_concern_majority_passthrough_WT ########################################### # Experimental buildvariants # @@ -8405,6 +8247,7 @@ buildvariants: tooltags: "-tags 'ssl'" # We need llvm-symbolizer in the PATH for ASAN for clang-3.7 or later. variant_path_suffix: /usr/lib/llvm-3.8/bin + lang_environment: LANG=C san_options: LSAN_OPTIONS="suppressions=etc/lsan.suppressions" ASAN_OPTIONS=detect_leaks=1 compile_flags: CC=/usr/bin/clang-3.8 CXX=/usr/bin/clang++-3.8 CPPDEFINES="_GLIBCXX_USE_CXX11_ABI=0" --dbg=on --opt=on --allocator=system --sanitize=address --ssl -j$(grep -c ^processor /proc/cpuinfo) --nostrip VARIANT_DIR=build multiversion_platform_arch: "ubuntu1604" @@ -8538,6 +8381,7 @@ buildvariants: - name: tool - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: ubuntu1604-asan display_name: ~ ASAN SSL Ubuntu 16.04 @@ -8549,6 +8393,7 @@ buildvariants: tooltags: "-tags 'ssl'" # We need llvm-symbolizer in the PATH for ASAN for clang-3.7 or later. variant_path_suffix: /usr/lib/llvm-3.8/bin + lang_environment: LANG=C san_options: LSAN_OPTIONS="suppressions=etc/lsan.suppressions" ASAN_OPTIONS=detect_leaks=1 compile_flags: CC=/usr/bin/clang-3.8 CXX=/usr/bin/clang++-3.8 CPPDEFINES="_GLIBCXX_USE_CXX11_ABI=0" --opt=on --allocator=system --sanitize=address --ssl -j$(grep -c ^processor /proc/cpuinfo) --nostrip VARIANT_DIR=build num_jobs_available: $(($(grep -c ^processor /proc/cpuinfo) / 3)) # Avoid starting too many mongod's under ASAN build. @@ -8580,6 +8425,7 @@ buildvariants: variant_path_suffix: /usr/lib/llvm-3.8/bin gorootvars: GOROOT=/opt/go PATH="/opt/go/bin:$PATH" tooltags: "-tags 'ssl sasl'" + lang_environment: LANG=C san_options: UBSAN_OPTIONS="print_stacktrace=1" compile_flags: CC=/usr/bin/clang-3.8 CXX=/usr/bin/clang++-3.8 --dbg=on --opt=on --allocator=system --sanitize=undefined --ssl -j$(grep -c ^processor /proc/cpuinfo) --nostrip CXXFLAGS="-nostdlib -nostdinc++" LIBPATH=/opt/mongodbtoolchain/v2/lib/gcc/x86_64-mongodb-linux/5.4.0 CPPPATH="/opt/mongodbtoolchain/v2/include/c++/5.4.0/ /opt/mongodbtoolchain/v2/include/c++/5.4.0/x86_64-mongodb-linux" CPPDEFINES="_GLIBCXX_USE_CXX11_ABI=0" VARIANT_DIR=build multiversion_platform_arch: "ubuntu1604" @@ -8712,6 +8558,7 @@ buildvariants: - name: tool - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: enterprise-ubuntu-dynamic-1604-64-bit display_name: "* Shared Library Enterprise Ubuntu 16.04" @@ -8720,6 +8567,7 @@ buildvariants: expansions: gorootvars: GOROOT=/opt/go PATH="/opt/go/bin:$PATH" tooltags: "-tags 'ssl sasl'" + lang_environment: LANG=C compile_flags: --ssl MONGO_DISTMOD=ubuntu1604 -j$(grep -c ^processor /proc/cpuinfo) CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy --link-model=dynamic num_jobs_available: $(grep -c ^processor /proc/cpuinfo) use_scons_cache: true @@ -8861,6 +8709,7 @@ buildvariants: - name: tool - name: tool_WT - name: unittests + - name: write_concern_majority_passthrough_WT - name: initsync-3dot2-rhel-62 display_name: "~ Initial Sync 3.2 Enterprise RHEL 6.2" @@ -8962,3 +8811,6 @@ buildvariants: - rhel62-large - name: slow1 - name: slow1_WT + - name: write_concern_majority_passthrough_WT + distros: + - rhel62-large diff --git a/etc/longevity.yml b/etc/longevity.yml index f6da942e0c1..9cdf9502fa7 100644 --- a/etc/longevity.yml +++ b/etc/longevity.yml @@ -9,45 +9,22 @@ post: params: working_dir: work script: | - set -v source ./dsienv.sh - $DSI_PATH/bin/make_artifact.sh + make_artifact.sh - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} - local_file: work/reports.tgz - remote_file: ${project}/${build_variant}/${revision}/${task_id}/${version_id}/logs/${task_name}-${build_id}.${ext|tgz} + local_file: work/dsi-artifacts.tgz + remote_file: ${project}/${build_variant}/${revision}/${task_id}/${version_id}/logs/dsi-artifacts-${task_name}-${build_id}-${execution}.${ext|tgz} bucket: mciuploads permissions: public-read content_type: ${content_type|application/x-gzip} - display_name: test-log - - command: s3.put - params: - aws_key: ${aws_key} - aws_secret: ${aws_secret} - local_file: work/reports/graphs/timeseries-p1.html - remote_file: ${project}/${build_variant}/${revision}/${task_id}/${version_id}/logs/timeseries-p1-${task_name}-${build_id}.html - bucket: mciuploads - permissions: public-read - content_type: text/html - display_name: timeseries-p1.html + display_name: Dsi Artifacts - Execution ${execution} - command: attach.results params: file_location: work/report.json - - command: shell.exec - # destroy the cluster - params: - working_dir: work - script: | - set -e - set -o verbose - # call terraform destroy twice to avoid AWS timeout - yes yes | ./terraform destroy --var-file=cluster.json - yes yes | ./terraform destroy --var-file=cluster.json - # clean all file to be safe - rm -rf * - echo "Cluster DESTROYED." + - func: "destroy cluster" - command: shell.exec params: working_dir: src @@ -79,12 +56,17 @@ functions: working_dir: work script: | cat > bootstrap.yml <<EOF - cluster_type: ${cluster} + infrastructure_provisioning: ${cluster} platform: ${platform} - setup: ${setup} + mongodb_setup: ${setup} storageEngine: ${storageEngine} - test: ${test}.longevity + test_control: ${test}.longevity production: true + workloads_dir: ../src/workloads/workloads + ycsb_dir: ../src/YCSB/YCSB + + # compositions of expansions + mongodb_binary_archive: "https://s3.amazonaws.com/mciuploads/${project}/${version_id}/${revision}/${platform}/mongod-${version_id}.tar.gz" EOF cat > runtime.yml <<EOF @@ -106,11 +88,6 @@ functions: ext: ${ext} script_flags : ${script_flags} dsi_rev: ${dsi_rev} - compare_task: ${compare_task} - workloads_rev: ${workloads_rev} - - # compositions of expansions - mongodb_binary_archive: "https://s3.amazonaws.com/mciuploads/${project}/${version_id}/${revision}/${platform}/mongod-${version_id}.tar.gz" EOF - command: shell.exec params: @@ -133,8 +110,6 @@ functions: - command: shell.exec params: working_dir: work - # setup execution environment - # configure environment script: | set -e virtualenv ./venv @@ -147,35 +122,25 @@ functions: set -v set -e source work/dsienv.sh - $DSI_PATH/bin/setup-dsi-env.sh + setup-dsi-env.sh ls -a work - "infrastructure provisioning": + "deploy cluster": - command: shell.exec - # call infrastructure-provisioning.sh. This will either create a cluster, or update tags on existing instances. params: working_dir: work script: | set -e set -v source ./dsienv.sh - export PRODUCTION=true - $DSI_PATH/bin/infrastructure_provisioning.sh ${cluster} - - "configure mongodb cluster": - - command: shell.exec - # bring up the mongod - params: - working_dir: work - script: | - set -e - set -o verbose - source ./dsienv.sh source ./venv/bin/activate - $DSI_PATH/bin/mongodb_setup.py && echo "${setup} MongoDB Cluster STARTED." + infrastructure_provisioning.py + workload_setup.py + mongodb_setup.py "run test": - command: shell.exec + type: test params: working_dir: work script: | @@ -183,9 +148,7 @@ functions: set -v source ./dsienv.sh source ./venv/bin/activate - echo "Run test for ${test}-${storageEngine} with setup ${setup}" - $DSI_PATH/bin/run_test.py ${storageEngine} ${test} ${cluster} - echo "Complete test for ${test} with setup ${setup}!" + test_control.py - command: "json.send" params: name: "perf" @@ -199,27 +162,18 @@ functions: script: | set -e set -o verbose - source ./dsienv.sh # Longevity runs so rarely, we simply teardown the cluster when done. - # Note that nowadays infrastructure_teardown.sh is actually copying the terraform.tfstate into /data/infrastructure_provisioning + # Note that nowadays infrastructure_teardown.py is actually copying the terraform.tfstate into /data/infrastructure_provisioning # but as of this writing the rhel70-perf-longevity distro didn't actually use the teardown hook. source ./dsienv.sh - $DSI_PATH/bin/infrastructure_teardown.sh + source ./venv/bin/activate + infrastructure_teardown.py echo "Cluster DESTROYED." echo echo "All perf results" cd .. cat perf.json | egrep "name|ops_per_sec" - "make test log artifact": - - command: shell.exec - params: - working_dir: work - script: | - set -v - source ./dsienv.sh - $DSI_PATH/bin/make_artifact.sh - "analyze": - command: json.get_history params: @@ -242,8 +196,8 @@ functions: set -o verbose TAG="3.2.1-Baseline" PROJECT="mongo-longevity" - OVERRIDEFILE="../src/dsi/dsi/analysis/v3.4/longevity_override.json" - python -u ../src/dsi/dsi/analysis/post_run_check.py ${script_flags} --reports-analysis reports --perf-file reports/perf.json --rev ${revision} -f history.json -t tags.json --refTag $TAG --overrideFile $OVERRIDEFILE --project_id $PROJECT --task_name ${task_name} --variant ${build_variant} + OVERRIDEFILE="../src/dsi/dsi/analysis/${branch_name}/longevity_override.json" + python -u ../src/dsi/dsi/analysis/post_run_check.py ${script_flags} --reports-analysis reports --perf-file perf.json --rev ${revision} -f history.json -t tags.json --refTag $TAG --overrideFile $OVERRIDEFILE --project_id $PROJECT --task_name ${task_name} --variant ${build_variant} tasks: - name: compile @@ -313,13 +267,8 @@ tasks: vars: storageEngine: "wiredTiger" test: "ycsb" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "wiredTiger" - test: "ycsb" - - func: "make test log artifact" - func: "analyze" vars: script_flags: --ycsb-throughput-analysis reports @@ -334,13 +283,8 @@ tasks: vars: storageEngine: "mmapv1" test: "ycsb" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "mmapv1" - test: "ycsb" - - func: "make test log artifact" - func: "analyze" vars: script_flags: --ycsb-throughput-analysis reports @@ -359,6 +303,11 @@ modules: branch: master + +####################################### +# Linux Buildvariants # +####################################### + buildvariants: - name: linux-wt-shard display_name: Linux WT Shard diff --git a/etc/perf.yml b/etc/perf.yml index 3dbc7baa61d..db116a42887 100644 --- a/etc/perf.yml +++ b/etc/perf.yml @@ -86,7 +86,7 @@ functions: set -v chmod +x mongod chmod +x mongo - git clone https://github.com/mongodb/mongo-perf perf + git clone git@github.com:mongodb/mongo-perf.git perf cd perf git describe --tags - command: shell.exec @@ -158,11 +158,9 @@ functions: script: | set -o errexit source ./venv/bin/activate - REFTAGS="3.4.2-Baseline 3.2.12-Baseline 3.0.14-Baseline" # These are project specific + REFTAGS="3.4.9-Baseline 3.2.17-Baseline 3.0.15-Baseline" # These are project specific # Project Opts are project specific, but don't match expansion from elsewhere. - # We could change dsi/analysis/v3.4 to dsi/analysis/perf-3.4 in DSI to get dsi/analysis/${project}/perf_override.json - # Note project_id performance doesn't match string perf used elsewhere for ${project} - PROJECT_OPTS="--overrideFile ../dsi/analysis/v3.4/perf_override.json --project_id performance" + PROJECT_OPTS="--overrideFile ../dsi/analysis/${branch_name}/perf_override.json --project_id performance" python -u ../dsi/analysis/dashboard_gen.py --rev ${revision} -f history.json -t tags.json --refTag $REFTAGS $PROJECT_OPTS --variant ${build_variant} --task ${task_name} --jira-user ${perf_jira_user} --jira-password ${perf_jira_pw} || true - command: "json.send" params: @@ -182,8 +180,8 @@ functions: # appropriate flags if it's `true`. reports_analysis_flags="--reports-analysis . --perf-file perf/perf.json" cmd_flags=$([ "${reports_analysis}" = "true" ] && echo "$reports_analysis_flags" || echo "") - REFTAG="3.2.12-Baseline" - OVERRIDE="../dsi/analysis/v3.4/perf_override.json" # Note use of v3.4 here cannot use ${project} + REFTAG="3.2.17-Baseline" + OVERRIDE="../dsi/analysis/${branch_name}/perf_override.json" python ../dsi/analysis/perf_regression_check.py $cmd_flags -f history.json --rev ${revision} -t tags.json --refTag $REFTAG --overrideFile $OVERRIDE --variant ${build_variant} --task ${task_name} --threshold 0.10 --threadThreshold 0.15 "run perf tests": - command: shell.exec @@ -196,6 +194,7 @@ functions: source ./venv/bin/activate pip install argparse - command: shell.exec + type : test params: working_dir: src script: | @@ -495,7 +494,7 @@ buildvariants: - name: linux-wt-repl display_name: 1-Node ReplSet Linux inMemory - batchtime: 360 # 6 hours + batchtime: 90 # 1.5 hours expansions: mongod_exec_wrapper: *exec_wrapper perf_exec_wrapper: *perf_wrapper @@ -511,7 +510,7 @@ buildvariants: - name: linux-mmap-repl display_name: 1-Node ReplSet Linux MMAPv1 - batchtime: 360 # 6 hours + batchtime: 90 # 1.5 hours expansions: mongod_exec_wrapper: *exec_wrapper perf_exec_wrapper: *perf_wrapper diff --git a/etc/system_perf.yml b/etc/system_perf.yml index 79f2a3d1a89..137bc2b4a13 100644 --- a/etc/system_perf.yml +++ b/etc/system_perf.yml @@ -10,37 +10,41 @@ post: working_dir: work script: | source ./dsienv.sh - $DSI_PATH/bin/make_artifact.sh + make_artifact.sh - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} - local_file: work/reports.tgz - remote_file: ${project}/${build_variant}/${revision}/${task_id}/${version_id}/logs/${task_name}-${build_id}.${ext|tgz} + local_file: work/dsi-artifacts.tgz + remote_file: ${project}/${build_variant}/${revision}/${task_id}/${version_id}/logs/dsi-artifacts-${task_name}-${build_id}-${execution}.${ext|tgz} bucket: mciuploads permissions: public-read content_type: ${content_type|application/x-gzip} - display_name: test-log + display_name: Dsi Artifacts - Execution ${execution} - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} - local_file: work/reports/graphs/timeseries-p1.html - remote_file: ${project}/${build_variant}/${revision}/${task_id}/${version_id}/logs/timeseries-p1-${task_name}-${build_id}.html + local_file: src/workloads/workloads/jsdoc/jsdocs-redirect.html + remote_file: ${project}/${build_variant}/${revision}/${task_id}/${version_id}/logs/workloads-${task_name}-${build_id}.html bucket: mciuploads permissions: public-read content_type: text/html - display_name: timeseries-p1.html + display_name: workloads documentation - command: attach.results params: file_location: work/report.json + - command: "json.send" + params: + name: "perf" + file: "work/perf.json" - command: shell.exec params: working_dir: work script: | source ./dsienv.sh if [ -e /data/infrastructure_provisioning/terraform/provisioned.${cluster} ]; then - $DSI_PATH/bin/mark_idle.sh + mark_idle.sh fi - command: shell.exec @@ -75,12 +79,15 @@ functions: working_dir: work script: | cat > bootstrap.yml <<EOF - cluster_type: ${cluster} + infrastructure_provisioning: ${cluster} platform: ${platform} - setup: ${setup} + mongodb_setup: ${setup} storageEngine: ${storageEngine} - test: ${test} + test_control: ${test} production: true + mongodb_binary_archive: "https://s3.amazonaws.com/mciuploads/${project}/${version_id}/${revision}/${platform}/mongod-${version_id}.tar.gz" + workloads_dir: ../src/workloads/workloads + ycsb_dir: ../src/YCSB/YCSB EOF cat > runtime.yml <<EOF @@ -102,11 +109,7 @@ functions: ext: ${ext} script_flags : ${script_flags} dsi_rev: ${dsi_rev} - compare_task: ${compare_task} workloads_rev: ${workloads_rev} - - # compositions of expansions - mongodb_binary_archive: "https://s3.amazonaws.com/mciuploads/${project}/${version_id}/${revision}/${platform}/mongod-${version_id}.tar.gz" EOF - command: shell.exec params: @@ -136,44 +139,32 @@ functions: virtualenv ./venv source ./venv/bin/activate pip install -r ../src/dsi/dsi/requirements.txt - python ../src/dsi/dsi/bin/bootstrap.py --production - ls - pwd + python ../src/dsi/dsi/bin/bootstrap.py - command: shell.exec params: script: | set -v set -e source work/dsienv.sh - $DSI_PATH/bin/setup-dsi-env.sh + setup-dsi-env.sh ls -a work - "infrastructure provisioning": + "deploy cluster": - command: shell.exec - # call infrastructure-provisioning.sh. This will either create a cluster, or update tags on existing instances. params: working_dir: work script: | set -e set -v source ./dsienv.sh - export PRODUCTION=true - $DSI_PATH/bin/infrastructure_provisioning.sh ${cluster} - - "configure mongodb cluster": - - command: shell.exec - # bring up the mongod - params: - working_dir: work - script: | - set -e - set -o verbose - source ./dsienv.sh source ./venv/bin/activate - $DSI_PATH/bin/mongodb_setup.py && echo "${setup} MongoDB Cluster STARTED." + infrastructure_provisioning.py + workload_setup.py + mongodb_setup.py "run test": - command: shell.exec + type: test params: working_dir: work script: | @@ -181,21 +172,12 @@ functions: set -v source ./dsienv.sh source ./venv/bin/activate - echo "Run test for ${test}-${storageEngine} with setup ${setup}" - $DSI_PATH/bin/run_test.py ${storageEngine} ${test} ${cluster} + test_control.py - command: "json.send" params: name: "perf" file: "work/perf.json" - "make test log artifact": - - command: shell.exec - params: - working_dir: work - script: | - source ./dsienv.sh - $DSI_PATH/bin/make_artifact.sh - "analyze": - command: json.get_history params: @@ -216,8 +198,8 @@ functions: silent: true script: | set -o errexit - TAGS="3.2.15-Baseline" - OVERRIDEFILE="../src/dsi/dsi/analysis/v3.4/system_perf_override.json" + TAGS="3.2.17-Baseline 3.4.9-Baseline" + OVERRIDEFILE="../src/dsi/dsi/analysis/${branch_name}/system_perf_override.json" python -u ../src/dsi/dsi/analysis/dashboard_gen.py --rev ${revision} -f history.json -t tags.json --refTag $TAGS --overrideFile $OVERRIDEFILE --project_id sys-perf --variant ${build_variant} --task ${task_name} --jira-user ${perf_jira_user} --jira-password ${perf_jira_pw} || true - command: "json.send" params: @@ -231,48 +213,9 @@ functions: script: | set -o errexit set -o verbose - TAG="3.2.15-Baseline" - OVERRIDEFILE="../src/dsi/dsi/analysis/v3.4/system_perf_override.json" - python -u ../src/dsi/dsi/analysis/post_run_check.py ${script_flags} --reports-analysis reports --perf-file reports/perf.json --rev ${revision} -f history.json -t tags.json --refTag $TAG --overrideFile $OVERRIDEFILE --project_id sys-perf --variant ${build_variant} --task ${task_name} - - "compare": - - command: shell.exec - params: - script: | - set -o verbose - rm -rf ./src ./work - mkdir src - mkdir work - - command: manifest.load - - command: git.get_project - params: - directory: src - revisions: # for each module include revision as <module_name> : ${<module_name>_rev} - dsi: ${dsi_rev} - - command: json.get - params: - task: ${compare_task} - variant : ${variant1} - file: "work/standalone.json" - name: "perf" - - command: json.get - params: - task: ${compare_task} - variant : ${variant2} - file: "work/oplog.json" - name: "perf" - - command: shell.exec - type : test - params: - working_dir: work - script: | - set -o errexit - set -o verbose - python -u ../src/dsi/dsi/analysis/compare.py -b standalone.json -c oplog.json - - command: "json.send" - params: - name: "perf" - file: "work/perf.json" + TAG="3.2.17-Baseline" + OVERRIDEFILE="../src/dsi/dsi/analysis/${branch_name}/system_perf_override.json" + python -u ../src/dsi/dsi/analysis/post_run_check.py ${script_flags} --reports-analysis reports --perf-file perf.json --rev ${revision} -f history.json -t tags.json --refTag $TAG --overrideFile $OVERRIDEFILE --project_id sys-perf --variant ${build_variant} --task ${task_name} ####################################### # Tasks # @@ -323,8 +266,17 @@ tasks: then echo "Fetching JS test DB correctness checks from directory jstests/hooks" cp -a jstests/hooks/* mongodb/jstests/hooks + + echo "Now adding our own special run_validate_collections.js wrapper" + mv mongodb/jstests/hooks/run_validate_collections.js mongodb/jstests/hooks/run_validate_collections.actual.js + + cat << EOF > mongodb/jstests/hooks/run_validate_collections.js + print("NOTE: run_validate_collections.js will skip the oplog!"); + TestData = { skipValidationNamespaces: ['local.oplog.rs'] }; + load('jstests/hooks/run_validate_collections.actual.js'); + EOF fi - tar cvf mongodb.tar mongodb + tar cf mongodb.tar mongodb gzip mongodb.tar - command: s3.put params: @@ -341,25 +293,20 @@ tasks: depends_on: - name: compile variant: linux-standalone + priority: 5 commands: - func: "prepare environment" vars: storageEngine: "wiredTiger" test: "ycsb" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "wiredTiger" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "wiredTiger" - test: "ycsb" - - func: "make test log artifact" - func: "analyze" vars: script_flags: --ycsb-throughput-analysis reports - name: industry_benchmarks_MMAPv1 + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -368,20 +315,46 @@ tasks: vars: storageEngine: "mmapv1" test: "ycsb" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" + - func: "deploy cluster" + - func: "run test" + - func: "analyze" vars: - storageEngine: "mmapv1" + script_flags: --ycsb-throughput-analysis reports + +- name: industry_benchmarks_wmajority_WT + priority: 5 + depends_on: + - name: compile + variant: linux-standalone + commands: + - func: "prepare environment" + vars: + storageEngine: "wiredTiger" + test: "ycsb-wmajority" + - func: "deploy cluster" - func: "run test" + - func: "analyze" + vars: + script_flags: --ycsb-throughput-analysis reports + +- name: industry_benchmarks_wmajority_MMAPv1 + priority: 5 + depends_on: + - name: compile + variant: linux-standalone + commands: + - func: "prepare environment" vars: storageEngine: "mmapv1" - test: "ycsb" - - func: "make test log artifact" + test: "ycsb-wmajority" + - func: "deploy cluster" + - func: "run test" - func: "analyze" vars: script_flags: --ycsb-throughput-analysis reports - name: core_workloads_WT + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -390,18 +363,12 @@ tasks: vars: storageEngine: "wiredTiger" test: "core" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "wiredTiger" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "wiredTiger" - test: "core" - - func: "make test log artifact" - func: "analyze" - name: core_workloads_MMAPv1 + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -410,18 +377,12 @@ tasks: vars: storageEngine: "mmapv1" test: "core" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "mmapv1" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "mmapv1" - test: "core" - - func: "make test log artifact" - func: "analyze" - name: non_sharded_workloads_WT + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -430,18 +391,12 @@ tasks: vars: storageEngine: "wiredTiger" test: "non_sharded" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "wiredTiger" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "wiredTiger" - test: "non_sharded" - - func: "make test log artifact" - func: "analyze" - name: non_sharded_workloads_MMAPv1 + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -450,18 +405,12 @@ tasks: vars: storageEngine: "mmapv1" test: "non_sharded" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "mmapv1" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "mmapv1" - test: "non_sharded" - - func: "make test log artifact" - func: "analyze" - name: mongos_workloads_WT + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -470,18 +419,12 @@ tasks: vars: storageEngine: "wiredTiger" test: "mongos" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "wiredTiger" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "wiredTiger" - test: "mongos" - - func: "make test log artifact" - func: "analyze" - name: mongos_workloads_MMAPv1 + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -491,18 +434,12 @@ tasks: vars: storageEngine: "mmapv1" test: "mongos" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "mmapv1" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "mmapv1" - test: "mongos" - - func: "make test log artifact" - func: "analyze" - name: move_chunk_workloads_WT + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -511,18 +448,12 @@ tasks: vars: storageEngine: "wiredTiger" test: "move_chunk" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "wiredTiger" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "wiredTiger" - test: "move_chunk" - - func: "make test log artifact" - func: "analyze" - name: move_chunk_workloads_MMAPv1 + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -531,18 +462,12 @@ tasks: vars: storageEngine: "mmapv1" test: "move_chunk" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "mmapv1" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "mmapv1" - test: "move_chunk" - - func: "make test log artifact" - func: "analyze" - name: secondary_performance_WT + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -550,21 +475,15 @@ tasks: - func: "prepare environment" vars: storageEngine: "wiredTiger" + # Unfortunately the dash/underscore style is different for mongodb_setup and test_control test: "secondary_performance" - setup: "replica-2node" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "wiredTiger" - setup: "replica-2node" + setup: "secondary-performance" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "wiredTiger" - test: "secondary_performance" - - func: "make test log artifact" - func: "analyze" - name: secondary_performance_MMAPv1 + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -572,85 +491,15 @@ tasks: - func: "prepare environment" vars: storageEngine: "mmapv1" + # Unfortunately the dash/underscore style is different for mongodb_setup and test_control test: "secondary_performance" - setup: "replica-2node" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "mmapv1" - setup: "replica-2node" + setup: "secondary-performance" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "mmapv1" - test: "secondary_performance" - - func: "make test log artifact" - - func: "analyze" - -- name: industry_benchmarks_WT_oplog_comp - depends_on: - - name: industry_benchmarks_WT - variant: linux-standalone - status : "*" - - name: industry_benchmarks_WT - variant: linux-1-node-replSet - status: "*" - commands: - - func: "compare" - vars: - compare_task: "industry_benchmarks_WT" - variant1: "linux-standalone" - variant2: "linux-1-node-replSet" - - func: "analyze" - -- name: industry_benchmarks_MMAPv1_oplog_comp - depends_on: - - name: industry_benchmarks_MMAPv1 - variant: linux-standalone - status: "*" - - name: industry_benchmarks_MMAPv1 - variant: linux-1-node-replSet - status: "*" - commands: - - func: "compare" - vars: - compare_task: "industry_benchmarks_MMAPv1" - variant1: "linux-standalone" - variant2: "linux-1-node-replSet" - - func: "analyze" - -- name: core_workloads_WT_oplog_comp - depends_on: - - name: core_workloads_WT - variant: linux-standalone - status: "*" - - name: core_workloads_WT - variant: linux-1-node-replSet - status: "*" - commands: - - func: "compare" - vars: - compare_task: "core_workloads_WT" - variant1: "linux-standalone" - variant2: "linux-1-node-replSet" - - func: "analyze" - -- name: core_workloads_MMAPv1_oplog_comp - depends_on: - - name: core_workloads_MMAPv1 - variant: linux-standalone - status: "*" - - name: core_workloads_MMAPv1 - variant: linux-1-node-replSet - status: "*" - commands: - - func: "compare" - vars: - compare_task: "core_workloads_MMAPv1" - variant1: "linux-standalone" - variant2: "linux-1-node-replSet" - func: "analyze" - name: initialsync_WT + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -658,19 +507,13 @@ tasks: - func: "prepare environment" vars: storageEngine: "wiredTiger" - test: "initialSync" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "wiredTiger" + test: "initialsync" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "wiredTiger" - test: "initialSync" - - func: "make test log artifact" - func: "analyze" - name: initialsync_MMAPv1 + priority: 5 depends_on: - name: compile variant : linux-standalone @@ -678,19 +521,13 @@ tasks: - func: "prepare environment" vars: storageEngine: "mmapv1" - test: "initialSync" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "mmapv1" + test: "initialsync" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "mmapv1" - test: "initialSync" - - func: "make test log artifact" - func: "analyze" - name: initialsync-logkeeper_WT + priority: 5 depends_on: - name: compile variant: linux-standalone @@ -700,18 +537,10 @@ tasks: vars: storageEngine: "wiredTiger" test: "initialsync-logkeeper" - - func: "infrastructure provisioning" - - func: "configure mongodb cluster" - vars: - storageEngine: "wiredTiger" + - func: "deploy cluster" - func: "run test" - vars: - storageEngine: "wiredTiger" - test: "initialsync-logkeeper" - - func: "make test log artifact" - func: "analyze" - ####################################### # Modules # ####################################### @@ -798,6 +627,8 @@ buildvariants: - name: industry_benchmarks_WT - name: core_workloads_WT - name: industry_benchmarks_MMAPv1 + - name: industry_benchmarks_wmajority_WT + - name: industry_benchmarks_wmajority_MMAPv1 - name: core_workloads_MMAPv1 - name: mongos_workloads_WT - name: mongos_workloads_MMAPv1 @@ -820,6 +651,8 @@ buildvariants: - name: industry_benchmarks_WT - name: core_workloads_WT - name: industry_benchmarks_MMAPv1 + - name: industry_benchmarks_wmajority_WT + - name: industry_benchmarks_wmajority_MMAPv1 - name: core_workloads_MMAPv1 - name: secondary_performance_WT - name: secondary_performance_MMAPv1 @@ -858,19 +691,3 @@ buildvariants: - "rhel70-perf-initialsync-logkeeper" tasks: - name: initialsync-logkeeper_WT - -- name: linux-oplog-compare - display_name: Linux Oplog Compare - batchtime: 10080 # 7 days - modules: *modules - expansions: - compile_flags: -j$(grep -c ^processor /proc/cpuinfo) CC=/opt/mongodbtoolchain/v2/bin/gcc CXX=/opt/mongodbtoolchain/v2/bin/g++ OBJCOPY=/opt/mongodbtoolchain/v2/bin/objcopy - use_scons_cache: true - project: *project - run_on: - - "rhel70-perf-single" - tasks: - - name: industry_benchmarks_WT_oplog_comp - - name: core_workloads_WT_oplog_comp - - name: industry_benchmarks_MMAPv1_oplog_comp - - name: core_workloads_MMAPv1_oplog_comp diff --git a/jstests/aggregation/extras/utils.js b/jstests/aggregation/extras/utils.js index 2dd1038388c..68b2597c533 100644 --- a/jstests/aggregation/extras/utils.js +++ b/jstests/aggregation/extras/utils.js @@ -276,7 +276,7 @@ function assertErrorCode(coll, pipe, code, errmsg) { var cursorRes = coll.runCommand("aggregate", cmd); if (cursorRes.ok) { var followupBatchSize = 0; // default - var cursor = new DBCommandCursor(coll.getMongo(), cursorRes, followupBatchSize); + var cursor = new DBCommandCursor(cursorRes._mongo, cursorRes, followupBatchSize); var error = assert.throws(function() { cursor.itcount(); diff --git a/jstests/auth/mongos_cache_invalidation.js b/jstests/auth/mongos_cache_invalidation.js index e36445cac5e..b53656de3a1 100644 --- a/jstests/auth/mongos_cache_invalidation.js +++ b/jstests/auth/mongos_cache_invalidation.js @@ -61,7 +61,7 @@ db3.auth('spencer', 'pwd'); * At this point we have 3 handles to the "test" database, each of which are on connections to * different mongoses. "db1", "db2", and "db3" are all auth'd as spencer@test and will be used * to verify that user and role data changes get propaged to their mongoses. - * "db2" is connected to a mongos with a 10 second user cache invalidation interval, + * "db2" is connected to a mongos with a 5 second user cache invalidation interval, * while "db3" is connected to a mongos with a 10 minute cache invalidation interval. */ @@ -202,12 +202,9 @@ db3.auth('spencer', 'pwd'); assert.commandFailedWithCode(db1.foo.runCommand("collStats"), authzErrorCode); // s1/db2 should update its cache in 10 seconds. - assert.soon( - function() { - return db2.foo.runCommand("collStats").code == authzErrorCode; - }, - "Mongos did not update its user cache after 10 seconds", - 6 * 1000); // Give an extra 1 second to avoid races + assert.soon(function() { + return db2.foo.runCommand("collStats").code == authzErrorCode; + }, "Mongos did not update its user cache after 10 seconds", 10 * 1000); // We manually invalidate the cache on s2/db3. db3.adminCommand("invalidateUserCache"); diff --git a/jstests/auth/scram-credentials-invalid.js b/jstests/auth/scram-credentials-invalid.js new file mode 100644 index 00000000000..16c0c204d12 --- /dev/null +++ b/jstests/auth/scram-credentials-invalid.js @@ -0,0 +1,45 @@ +// Ensure that attempting to use SCRAM-SHA-1 auth on a +// user with invalid SCRAM-SHA-1 credentials fails gracefully. + +(function() { + 'use strict'; + + function runTest(mongod) { + assert(mongod); + const admin = mongod.getDB('admin'); + const test = mongod.getDB('test'); + + admin.createUser({user: 'admin', pwd: 'pass', roles: jsTest.adminUserRoles}); + assert(admin.auth('admin', 'pass')); + + test.createUser({user: 'user', pwd: 'pass', roles: jsTest.basicUserRoles}); + + // Give the test user an invalid set of SCRAM-SHA-1 credentials. + assert.eq(admin.system.users + .update({_id: "test.user"}, { + $set: { + "credentials.SCRAM-SHA-1": { + salt: "AAAA", + storedKey: "AAAA", + serverKey: "AAAA", + iterationCount: 10000 + } + } + }) + .nModified, + 1, + "Should have updated one document for user@test"); + admin.logout(); + + assert(!test.auth({user: 'user', pwd: 'pass'})); + + assert.soon(function() { + const log = cat(mongod.fullOptions.logFile); + return /Unable to perform SCRAM-SHA-1 auth.* invalid SCRAM credentials/.test(log); + }, "No warning issued for invalid SCRAM-SHA-1 credendials doc", 30 * 1000, 5 * 1000); + } + + const mongod = MongoRunner.runMongod({auth: "", useLogFiles: true}); + runTest(mongod); + MongoRunner.stopMongod(mongod); +})(); diff --git a/jstests/auth/system_authorization_indexes.js b/jstests/auth/system_authorization_indexes.js new file mode 100644 index 00000000000..496f78578a4 --- /dev/null +++ b/jstests/auth/system_authorization_indexes.js @@ -0,0 +1,66 @@ +/** Ensure that authorization system collections' indexes are correctly generated. + * + * This test requires users to persist across a restart. + * @tags: [requires_persistence] + */ + +(function() { + let conn = MongoRunner.runMongod({smallfiles: ""}); + let db = conn.getDB("admin"); + + // TEST: User and role collections start off with no indexes + assert.eq(0, db.system.users.getIndexes().length); + assert.eq(0, db.system.roles.getIndexes().length); + + // TEST: User and role creation generates indexes + db.createUser({user: "user", pwd: "pwd", roles: []}); + assert.eq(2, db.system.users.getIndexes().length); + + db.createRole({role: "role", privileges: [], roles: []}); + assert.eq(2, db.system.roles.getIndexes().length); + + // TEST: Destroying admin.system.users index and restarting will recreate it + assert.commandWorked(db.system.users.dropIndexes()); + MongoRunner.stopMongod(conn); + conn = MongoRunner.runMongod({restart: conn, cleanData: false}); + db = conn.getDB("admin"); + assert.eq(2, db.system.users.getIndexes().length); + assert.eq(2, db.system.roles.getIndexes().length); + + // TEST: Destroying admin.system.roles index and restarting will recreate it + assert.commandWorked(db.system.roles.dropIndexes()); + MongoRunner.stopMongod(conn); + conn = MongoRunner.runMongod({restart: conn, cleanData: false}); + db = conn.getDB("admin"); + assert.eq(2, db.system.users.getIndexes().length); + assert.eq(2, db.system.roles.getIndexes().length); + + // TEST: Destroying both authorization indexes and restarting will recreate them + assert.commandWorked(db.system.users.dropIndexes()); + assert.commandWorked(db.system.roles.dropIndexes()); + MongoRunner.stopMongod(conn); + conn = MongoRunner.runMongod({restart: conn, cleanData: false}); + db = conn.getDB("admin"); + assert.eq(2, db.system.users.getIndexes().length); + assert.eq(2, db.system.roles.getIndexes().length); + + // TEST: Destroying the admin.system.users index and restarting will recreate it, even if + // admin.system.roles does not exist + db.dropDatabase(); + db.createUser({user: "user", pwd: "pwd", roles: []}); + assert.commandWorked(db.system.users.dropIndexes()); + MongoRunner.stopMongod(conn); + conn = MongoRunner.runMongod({restart: conn, cleanData: false}); + db = conn.getDB("admin"); + assert.eq(2, db.system.users.getIndexes().length); + + // TEST: Destroying the admin.system.roles index and restarting will recreate it, even if + // admin.system.users does not exist + db.dropDatabase(); + db.createRole({role: "role", privileges: [], roles: []}); + assert.commandWorked(db.system.roles.dropIndexes()); + MongoRunner.stopMongod(conn); + conn = MongoRunner.runMongod({restart: conn, cleanData: false}); + db = conn.getDB("admin"); + assert.eq(2, db.system.roles.getIndexes().length); +})(); diff --git a/jstests/concurrency/fsm_libs/cluster.js b/jstests/concurrency/fsm_libs/cluster.js index a5705a409af..f3e2429f898 100644 --- a/jstests/concurrency/fsm_libs/cluster.js +++ b/jstests/concurrency/fsm_libs/cluster.js @@ -573,6 +573,25 @@ var Cluster = function(options) { return data; }; + + this.isRunningWiredTigerLSM = function isRunningWiredTigerLSM() { + var adminDB = this.getDB('admin'); + + if (this.isSharded()) { + // Get the storage engine the sharded cluster is configured to use from one of the + // shards since mongos won't report it. + adminDB = st.shard0.getDB('admin'); + } + + var res = adminDB.runCommand({getCmdLineOpts: 1}); + assert.commandWorked(res, 'failed to get command line options'); + + var wiredTigerOptions = res.parsed.storage.wiredTiger || {}; + var wiredTigerCollectionConfig = wiredTigerOptions.collectionConfig || {}; + var wiredTigerConfigString = wiredTigerCollectionConfig.configString || ''; + + return wiredTigerConfigString === 'type=lsm'; + }; }; /** diff --git a/jstests/concurrency/fsm_workloads/compact.js b/jstests/concurrency/fsm_workloads/compact.js index 8f91f52bf5e..e86e6ef555f 100644 --- a/jstests/concurrency/fsm_workloads/compact.js +++ b/jstests/concurrency/fsm_workloads/compact.js @@ -87,12 +87,27 @@ var $config = (function() { dropCollections(db, pattern); }; + var skip = function skip(cluster) { + if (cluster.isRunningWiredTigerLSM()) { + // There is a known hang during concurrent FSM workloads with the compact command used + // with wiredTiger LSM variants. Bypass this command for the wiredTiger LSM variant + // until a fix is available for WT-2523. + return { + skip: true, + msg: 'WT-2523: compact command can cause hang using WT LSM index during ' + + 'concurrent workloads' + }; + } + return {skip: false}; + }; + return { threadCount: 15, iterations: 10, states: states, transitions: transitions, teardown: teardown, - data: data + data: data, + skip: skip }; })(); diff --git a/jstests/concurrency/fsm_workloads/remove_multiple_documents.js b/jstests/concurrency/fsm_workloads/remove_multiple_documents.js index bfd64cd6790..1461ca0cc03 100644 --- a/jstests/concurrency/fsm_workloads/remove_multiple_documents.js +++ b/jstests/concurrency/fsm_workloads/remove_multiple_documents.js @@ -34,8 +34,17 @@ var $config = (function() { } }; + var skip = function skip(cluster) { + // When the balancer is enabled, the nRemoved result may be inaccurate as + // a chunk migration may be active, causing the count function to assert. + if (cluster.isBalancerEnabled()) { + return {skip: true, msg: 'does not run when balancer is enabled.'}; + } + return {skip: false}; + }; + var transitions = {init: {count: 1}, count: {remove: 1}, remove: {remove: 0.825, count: 0.125}}; - return {threadCount: 10, iterations: 20, states: states, transitions: transitions}; + return {threadCount: 10, iterations: 20, states: states, transitions: transitions, skip: skip}; })(); diff --git a/jstests/concurrency/fsm_workloads/remove_where.js b/jstests/concurrency/fsm_workloads/remove_where.js index f9c0e6a2c03..3ef214c769b 100644 --- a/jstests/concurrency/fsm_workloads/remove_where.js +++ b/jstests/concurrency/fsm_workloads/remove_where.js @@ -38,5 +38,14 @@ var $config = extendWorkload($config, function($config, $super) { /* no-op to prevent index from being created */ }; + $config.skip = function skip(cluster) { + // When the balancer is enabled, the nRemoved result may be inaccurate as + // a chunk migration may be active, causing the count function to assert. + if (cluster.isBalancerEnabled()) { + return {skip: true, msg: 'does not run when balancer is enabled.'}; + } + return {skip: false}; + }; + return $config; }); diff --git a/jstests/core/apply_ops1.js b/jstests/core/apply_ops1.js index 8ce15b1df22..7088145c718 100644 --- a/jstests/core/apply_ops1.js +++ b/jstests/core/apply_ops1.js @@ -286,9 +286,54 @@ 'applyOps should fail on unknown operation type "x" with valid "ns" value'); assert.eq(0, t.find().count(), "Non-zero amount of documents in collection to start"); + + /** + * Test function for running CRUD operations on non-existent namespaces using various + * combinations of invalid namespaces (collection/database), allowAtomic and alwaysUpsert. + * + * Leave 'expectedErrorCode' undefined if this command is expected to run successfully. + */ + function testCrudOperationOnNonExistentNamespace(optype, o, o2, expectedErrorCode) { + expectedErrorCode = expectedErrorCode || ErrorCodes.OK; + const t2 = db.getSiblingDB('apply_ops1_no_such_db').getCollection('t'); + [t, t2].forEach(coll => { + const op = {op: optype, ns: coll.getFullName(), o: o, o2: o2}; + [false, true].forEach(allowAtomic => { + [false, true].forEach(alwaysUpsert => { + const cmd = { + applyOps: [op], + allowAtomic: allowAtomic, + alwaysUpsert: alwaysUpsert + }; + jsTestLog('Testing applyOps on non-existent namespace: ' + tojson(cmd)); + if (expectedErrorCode === ErrorCodes.OK) { + assert.commandWorked(db.adminCommand(cmd)); + } else { + assert.commandFailedWithCode(db.adminCommand(cmd), expectedErrorCode); + } + }); + }); + }); + } + + // Insert and update operations on non-existent collections/databases should return + // NamespaceNotFound. + testCrudOperationOnNonExistentNamespace('i', {_id: 0}, {}, ErrorCodes.NamespaceNotFound); + testCrudOperationOnNonExistentNamespace('u', {x: 0}, {_id: 0}, ErrorCodes.NamespaceNotFound); + + // Delete operations on non-existent collections/databases should return OK for idempotency + // reasons. + testCrudOperationOnNonExistentNamespace('d', {_id: 0}, {}); + assert.commandFailed( - db.adminCommand({applyOps: [{"op": "i", "ns": t.getFullName(), "o": {_id: 5, x: 17}}]}), - "Applying an insert operation on a non-existent collection should fail"); + db.adminCommand({ + applyOps: [{ + "op": "c", + "ns": "admin.$cmd", + "o": {applyOps: [{"op": "i", "ns": t.getFullName(), "o": {_id: 5, x: 17}}]} + }] + }), + "Applying a nested insert operation on a non-existent collection should fail"); assert.commandWorked(db.createCollection(t.getName())); var a = assert.commandWorked( diff --git a/jstests/core/apply_ops_atomicity.js b/jstests/core/apply_ops_atomicity.js index 911ce32a311..e704815a889 100644 --- a/jstests/core/apply_ops_atomicity.js +++ b/jstests/core/apply_ops_atomicity.js @@ -29,10 +29,12 @@ var newDBName = "apply_ops_atomicity"; var newDB = db.getSiblingDB(newDBName); assert.commandWorked(newDB.dropDatabase()); - // Do an update on a non-existent database, since only 'u' ops can implicitly create - // collections. - assert.commandWorked(newDB.runCommand( - {applyOps: [{op: "u", ns: newDBName + ".foo", o: {_id: 5, x: 17}, o2: {_id: 5, x: 16}}]})); + // Updates on a non-existent database no longer implicitly create collections and will fail with + // a NamespaceNotFound error. + assert.commandFailedWithCode(newDB.runCommand({ + applyOps: [{op: "u", ns: newDBName + ".foo", o: {_id: 5, x: 17}, o2: {_id: 5, x: 16}}] + }), + ErrorCodes.NamespaceNotFound); var sawTooManyLocksError = false; diff --git a/jstests/core/autocomplete.js b/jstests/core/autocomplete.js new file mode 100644 index 00000000000..6eb6e21a7a3 --- /dev/null +++ b/jstests/core/autocomplete.js @@ -0,0 +1,42 @@ +/** + * Validate auto complete works for various javascript types implemented by C++. + */ +(function() { + 'use strict'; + + function testAutoComplete(prefix) { + // This method updates a global object with an array of strings on success. + shellAutocomplete(prefix); + return __autocomplete__; + } + + // Create a collection + db.auto_complete_coll.insert({}); + + // Validate DB auto completion + const db_stuff = testAutoComplete('db.'); + + // Verify we enumerate built-in methods + assert.contains('db.prototype', db_stuff); + assert.contains('db.hasOwnProperty', db_stuff); + assert.contains('db.toString(', db_stuff); + + // Verify we have some methods we added + assert.contains('db.adminCommand(', db_stuff); + assert.contains('db.runCommand(', db_stuff); + + // Verify we enumerate collections + assert.contains('db.auto_complete_coll', db_stuff); + + // Validate Collection autocompletion + const coll_stuff = testAutoComplete('db.auto_complete_coll.'); + + // Verify we enumerate built-in methods + assert.contains('db.auto_complete_coll.prototype', coll_stuff); + assert.contains('db.auto_complete_coll.hasOwnProperty', coll_stuff); + assert.contains('db.auto_complete_coll.toString(', coll_stuff); + + // Verify we have some methods we added + assert.contains('db.auto_complete_coll.aggregate(', coll_stuff); + assert.contains('db.auto_complete_coll.runCommand(', coll_stuff); +})();
\ No newline at end of file diff --git a/jstests/core/batch_write_command_delete.js b/jstests/core/batch_write_command_delete.js index 2aefcea6a7f..99b5f8e3a61 100644 --- a/jstests/core/batch_write_command_delete.js +++ b/jstests/core/batch_write_command_delete.js @@ -1,3 +1,5 @@ +// @tags: [assumes_write_concern_unchanged] + // // Ensures that mongod respects the batch write protocols for delete // diff --git a/jstests/core/batch_write_command_insert.js b/jstests/core/batch_write_command_insert.js index 274f35513e7..3fa6cf98756 100644 --- a/jstests/core/batch_write_command_insert.js +++ b/jstests/core/batch_write_command_insert.js @@ -1,3 +1,5 @@ +// @tags: [assumes_write_concern_unchanged] + // // Ensures that mongod respects the batch write protocol for inserts // diff --git a/jstests/core/batch_write_command_update.js b/jstests/core/batch_write_command_update.js index 2d9d2d699b2..987525e2515 100644 --- a/jstests/core/batch_write_command_update.js +++ b/jstests/core/batch_write_command_update.js @@ -1,3 +1,5 @@ +// @tags: [assumes_write_concern_unchanged] + // // Ensures that mongod respects the batch write protocols for updates // diff --git a/jstests/core/bypass_doc_validation.js b/jstests/core/bypass_doc_validation.js index d9bca81ab6d..c2cdfff0bc9 100644 --- a/jstests/core/bypass_doc_validation.js +++ b/jstests/core/bypass_doc_validation.js @@ -1,5 +1,7 @@ // Test the bypassDocumentValidation flag with some database commands. The test uses relevant shell // helpers when they're available for the respective server commands. +// +// @tags: [requires_collmod_command] (function() { 'use strict'; diff --git a/jstests/core/capped6.js b/jstests/core/capped6.js index e94e7ea44e8..ca216fbe96a 100644 --- a/jstests/core/capped6.js +++ b/jstests/core/capped6.js @@ -1,4 +1,11 @@ // Test NamespaceDetails::cappedTruncateAfter via "captrunc" command +// +// @tags: [ +// # This test attempts to perform read operations on a capped collection after truncating +// # documents using the captrunc command. The writes from the captrunc command aren't guaranteed +// # to become visible until a later w="majority" write occurs. +// assumes_write_concern_unchanged, +// ] (function() { var coll = db.capped6; diff --git a/jstests/core/collation_plan_cache.js b/jstests/core/collation_plan_cache.js index 0eec77388e4..790bbbadaa6 100644 --- a/jstests/core/collation_plan_cache.js +++ b/jstests/core/collation_plan_cache.js @@ -1,4 +1,11 @@ // Integration testing for the plan cache and index filter commands with collation. +// +// @tags: [ +// # This test attempts to perform queries and introspect the server's plan cache entries. The +// # former operation may be routed to a secondary in the replica set, whereas the latter must be +// # routed to the primary. +// assumes_read_preference_unchanged, +// ] (function() { 'use strict'; @@ -237,4 +244,4 @@ assert.eq(0, coll.runCommand('planCacheListFilters').filters.length, 'unexpected number of plan cache filters'); -})();
\ No newline at end of file +})(); diff --git a/jstests/core/collection_info_cache_race.js b/jstests/core/collection_info_cache_race.js index d57fc3340db..8fcde050e99 100644 --- a/jstests/core/collection_info_cache_race.js +++ b/jstests/core/collection_info_cache_race.js @@ -5,9 +5,9 @@ var coll = db.collection_info_cache_race; coll.drop(); assert.commandWorked(db.createCollection(coll.getName(), {autoIndexId: false})); // Fails when SERVER-16502 was not fixed, due to invariant -assert.writeOK(coll.save({_id: false}, {writeConcern: {w: 1}})); +assert.writeOK(coll.save({_id: false})); coll.drop(); assert.commandWorked(db.createCollection(coll.getName(), {autoIndexId: false})); assert.eq(null, coll.findOne()); -assert.writeOK(coll.save({_id: false}, {writeConcern: {w: 1}})); +assert.writeOK(coll.save({_id: false})); diff --git a/jstests/core/collmod.js b/jstests/core/collmod.js index 16f9694560c..e366041bd99 100644 --- a/jstests/core/collmod.js +++ b/jstests/core/collmod.js @@ -1,5 +1,7 @@ // Basic js tests for the collMod command. // Test setting the usePowerOf2Sizes flag, and modifying TTL indexes. +// +// @tags: [requires_collmod_command] function debug(x) { // printjson( x ); diff --git a/jstests/core/collmod_bad_spec.js b/jstests/core/collmod_bad_spec.js index ccce81fd4b1..c3d5e7a148e 100644 --- a/jstests/core/collmod_bad_spec.js +++ b/jstests/core/collmod_bad_spec.js @@ -2,6 +2,8 @@ // // Tests that a collMod with a bad specification does not cause any changes, and does not crash the // server. +// +// @tags: [requires_collmod_command] (function() { "use strict"; diff --git a/jstests/core/commands_that_do_not_write_do_not_accept_wc.js b/jstests/core/commands_that_do_not_write_do_not_accept_wc.js index ef5f8762a42..a9c72e108a1 100644 --- a/jstests/core/commands_that_do_not_write_do_not_accept_wc.js +++ b/jstests/core/commands_that_do_not_write_do_not_accept_wc.js @@ -2,6 +2,8 @@ * This file tests commands that do not support write concern. It passes both valid and invalid * writeConcern fields to commands and expects the commands to fail with a writeConcernNotSupported * error. + * + * @tags: [assumes_write_concern_unchanged] */ (function() { diff --git a/jstests/core/constructors.js b/jstests/core/constructors.js index 9e2cd26bbe8..93842780b42 100644 --- a/jstests/core/constructors.js +++ b/jstests/core/constructors.js @@ -1,4 +1,6 @@ // Tests to see what validity checks are done for 10gen specific object construction +// +// @tags: [requires_eval_command] // Takes a list of constructors and returns a new list with an extra entry for each constructor with // "new" prepended diff --git a/jstests/core/count10.js b/jstests/core/count10.js index 2a1853c399a..453775c97f5 100644 --- a/jstests/core/count10.js +++ b/jstests/core/count10.js @@ -1,4 +1,11 @@ // Test that interrupting a count returns an error code. +// +// @tags: [ +// # This test attempts to perform a count command and find it using the currentOp command. The +// # former operation may be routed to a secondary in the replica set, whereas the latter must be +// # routed to the primary. +// assumes_read_preference_unchanged, +// ] t = db.count10; t.drop(); diff --git a/jstests/core/count_plan_summary.js b/jstests/core/count_plan_summary.js index 48891d21e8e..365f289c457 100644 --- a/jstests/core/count_plan_summary.js +++ b/jstests/core/count_plan_summary.js @@ -1,5 +1,11 @@ -// Test that the plan summary string appears in db.currentOp() for -// count operations. SERVER-14064. +// Test that the plan summary string appears in db.currentOp() for count operations. SERVER-14064. +// +// @tags: [ +// # This test attempts to perform a find command and find it using the currentOp command. The +// # former operation may be routed to a secondary in the replica set, whereas the latter must be +// # routed to the primary. +// assumes_read_preference_unchanged, +// ] var t = db.jstests_count_plan_summary; t.drop(); diff --git a/jstests/core/crud_api.js b/jstests/core/crud_api.js index c9dbfb40c85..d572d4b90a5 100644 --- a/jstests/core/crud_api.js +++ b/jstests/core/crud_api.js @@ -1,3 +1,5 @@ +// @tags: [assumes_write_concern_unchanged] + (function() { "use strict"; diff --git a/jstests/core/diagdata.js b/jstests/core/diagdata.js index 490e4a3eb2b..6938f8c5102 100644 --- a/jstests/core/diagdata.js +++ b/jstests/core/diagdata.js @@ -1,4 +1,5 @@ // Test that verifies getDiagnosticData returns FTDC data +load('jstests/libs/ftdc.js'); (function() { "use strict"; @@ -6,28 +7,5 @@ // Verify we require admin database assert.commandFailed(db.diagdata.runCommand("getDiagnosticData")); - // We need to retry a few times if run this test immediately after mongod is started as FTDC may - // not have run yet. - var foundGoodDocument = false; - - for (var i = 0; i < 60; ++i) { - var result = db.adminCommand("getDiagnosticData"); - assert.commandWorked(result); - - var data = result.data; - - if (!data.hasOwnProperty("start")) { - // Wait a little longer for FTDC to start - sleep(500); - } else { - // Check for a few common properties to ensure we got data - assert(data.hasOwnProperty("serverStatus"), - "does not have 'serverStatus' in '" + tojson(data) + "'"); - assert(data.hasOwnProperty("end"), "does not have 'end' in '" + tojson(data) + "'"); - foundGoodDocument = true; - } - } - assert(foundGoodDocument, - "getDiagnosticData failed to return a non-empty command, is FTDC running?"); - + verifyGetDiagnosticData(db.getSiblingDB('admin')); })(); diff --git a/jstests/core/doc_validation.js b/jstests/core/doc_validation.js index a30763869e7..95dbae1b219 100644 --- a/jstests/core/doc_validation.js +++ b/jstests/core/doc_validation.js @@ -1,4 +1,6 @@ // Test basic inserts and updates with document validation. +// +// @tags: [requires_collmod_command] (function() { "use strict"; diff --git a/jstests/core/doc_validation_invalid_validators.js b/jstests/core/doc_validation_invalid_validators.js index b78b31c0977..70262f5de43 100644 --- a/jstests/core/doc_validation_invalid_validators.js +++ b/jstests/core/doc_validation_invalid_validators.js @@ -1,5 +1,7 @@ // Verify invalid validator statements won't work and that we // can't create validated collections on restricted databases. +// +// @tags: [requires_collmod_command] (function() { "use strict"; diff --git a/jstests/core/doc_validation_options.js b/jstests/core/doc_validation_options.js index 8a96685e48f..d1434af6d45 100644 --- a/jstests/core/doc_validation_options.js +++ b/jstests/core/doc_validation_options.js @@ -1,3 +1,4 @@ +// @tags: [requires_collmod_command] (function() { "use strict"; diff --git a/jstests/core/dropdb_race.js b/jstests/core/dropdb_race.js index bd5e7e5ddba..d8b49d08174 100644 --- a/jstests/core/dropdb_race.js +++ b/jstests/core/dropdb_race.js @@ -1,4 +1,6 @@ // test dropping a db with simultaneous commits +// +// @tags: [assumes_write_concern_unchanged] m = db.getMongo(); baseName = "jstests_dur_droprace"; diff --git a/jstests/core/elemMatchProjection.js b/jstests/core/elemMatchProjection.js index 4e80e8a296e..5060d8a6346 100644 --- a/jstests/core/elemMatchProjection.js +++ b/jstests/core/elemMatchProjection.js @@ -4,35 +4,59 @@ t.drop(); date1 = new Date(); +// Generate monotonically increasing _id values. ObjectIds generated by the shell are not guaranteed +// to be monotically increasing, and we will depend on the _id sort order later in the test. +var currentId = 0; +function nextId() { + return ++currentId; +} + // Insert various styles of arrays for (i = 0; i < 100; i++) { - t.insert({group: 1, x: [1, 2, 3, 4, 5]}); - t.insert({group: 2, x: [{a: 1, b: 2}, {a: 2, c: 3}, {a: 1, d: 5}]}); + t.insert({_id: nextId(), group: 1, x: [1, 2, 3, 4, 5]}); + t.insert({_id: nextId(), group: 2, x: [{a: 1, b: 2}, {a: 2, c: 3}, {a: 1, d: 5}]}); t.insert({ + _id: nextId(), group: 3, x: [{a: 1, b: 2}, {a: 2, c: 3}, {a: 1, d: 5}], y: [{aa: 1, bb: 2}, {aa: 2, cc: 3}, {aa: 1, dd: 5}] }); - t.insert({group: 3, x: [{a: 1, b: 3}, {a: -6, c: 3}]}); - t.insert({group: 4, x: [{a: 1, b: 4}, {a: -6, c: 3}]}); - t.insert({group: 5, x: [new Date(), 5, 10, 'string', new ObjectId(), 123.456]}); + t.insert({_id: nextId(), group: 3, x: [{a: 1, b: 3}, {a: -6, c: 3}]}); + t.insert({_id: nextId(), group: 4, x: [{a: 1, b: 4}, {a: -6, c: 3}]}); + t.insert({_id: nextId(), group: 5, x: [new Date(), 5, 10, 'string', new ObjectId(), 123.456]}); t.insert({ + _id: nextId(), group: 6, x: [{a: 'string', b: date1}, {a: new ObjectId(), b: 1.2345}, {a: 'string2', b: date1}] }); - t.insert({group: 7, x: [{y: [1, 2, 3, 4]}]}); - t.insert({group: 8, x: [{y: [{a: 1, b: 2}, {a: 3, b: 4}]}]}); - t.insert({group: 9, x: [{y: [{a: 1, b: 2}, {a: 3, b: 4}]}, {z: [{a: 1, b: 2}, {a: 3, b: 4}]}]}); - t.insert({group: 10, x: [{a: 1, b: 2}, {a: 3, b: 4}], y: [{c: 1, d: 2}, {c: 3, d: 4}]}); - t.insert({group: 10, x: [{a: 1, b: 2}, {a: 3, b: 4}], y: [{c: 1, d: 2}, {c: 3, d: 4}]}); + t.insert({_id: nextId(), group: 7, x: [{y: [1, 2, 3, 4]}]}); + t.insert({_id: nextId(), group: 8, x: [{y: [{a: 1, b: 2}, {a: 3, b: 4}]}]}); + t.insert({ + _id: nextId(), + group: 9, + x: [{y: [{a: 1, b: 2}, {a: 3, b: 4}]}, {z: [{a: 1, b: 2}, {a: 3, b: 4}]}] + }); + t.insert({ + _id: nextId(), + group: 10, + x: [{a: 1, b: 2}, {a: 3, b: 4}], + y: [{c: 1, d: 2}, {c: 3, d: 4}] + }); + t.insert({ + _id: nextId(), + group: 10, + x: [{a: 1, b: 2}, {a: 3, b: 4}], + y: [{c: 1, d: 2}, {c: 3, d: 4}] + }); t.insert({ + _id: nextId(), group: 11, x: [{a: 1, b: 2}, {a: 2, c: 3}, {a: 1, d: 5}], covered: [{aa: 1, bb: 2}, {aa: 2, cc: 3}, {aa: 1, dd: 5}] }); - t.insert({group: 12, x: {y: [{a: 1, b: 1}, {a: 1, b: 2}]}}); - t.insert({group: 13, x: [{a: 1, b: 1}, {a: 1, b: 2}]}); - t.insert({group: 13, x: [{a: 1, b: 2}, {a: 1, b: 1}]}); + t.insert({_id: nextId(), group: 12, x: {y: [{a: 1, b: 1}, {a: 1, b: 2}]}}); + t.insert({_id: nextId(), group: 13, x: [{a: 1, b: 1}, {a: 1, b: 2}]}); + t.insert({_id: nextId(), group: 13, x: [{a: 1, b: 2}, {a: 1, b: 1}]}); } t.ensureIndex({ group: 1, diff --git a/jstests/core/error2.js b/jstests/core/error2.js index 6f0b95bc17e..fb6a8e6e3b2 100644 --- a/jstests/core/error2.js +++ b/jstests/core/error2.js @@ -1,4 +1,5 @@ // Test that client gets stack trace on failed invoke +// @tags: [requires_eval_command] f = db.jstests_error2; diff --git a/jstests/core/eval0.js b/jstests/core/eval0.js index 5802f2597cb..c21c6be66c6 100644 --- a/jstests/core/eval0.js +++ b/jstests/core/eval0.js @@ -1,3 +1,5 @@ +// @tags: [requires_eval_command] + assert.writeOK(db.evalprep.insert({}), "db must exist for eval to succeed"); db.evalprep.drop(); assert.eq(17, diff --git a/jstests/core/eval1.js b/jstests/core/eval1.js index 8b139cae02a..b5bffac892e 100644 --- a/jstests/core/eval1.js +++ b/jstests/core/eval1.js @@ -1,3 +1,4 @@ +// @tags: [requires_eval_command] t = db.eval1; t.drop(); diff --git a/jstests/core/eval3.js b/jstests/core/eval3.js index c4f8be21056..b95837a6817 100644 --- a/jstests/core/eval3.js +++ b/jstests/core/eval3.js @@ -1,3 +1,4 @@ +// @tags: [requires_eval_command] t = db.eval3; t.drop(); diff --git a/jstests/core/eval4.js b/jstests/core/eval4.js index 0d120b393de..9b0c2a49d82 100644 --- a/jstests/core/eval4.js +++ b/jstests/core/eval4.js @@ -1,3 +1,4 @@ +// @tags: [requires_eval_command] t = db.eval4; t.drop(); diff --git a/jstests/core/eval5.js b/jstests/core/eval5.js index 46bd679dd77..815365ac8b9 100644 --- a/jstests/core/eval5.js +++ b/jstests/core/eval5.js @@ -1,3 +1,4 @@ +// @tags: [requires_eval_command] t = db.eval5; t.drop(); diff --git a/jstests/core/eval6.js b/jstests/core/eval6.js index 31258f6917b..96b43b3516c 100644 --- a/jstests/core/eval6.js +++ b/jstests/core/eval6.js @@ -1,3 +1,4 @@ +// @tags: [requires_eval_command] t = db.eval6; t.drop(); diff --git a/jstests/core/eval7.js b/jstests/core/eval7.js index 80197fcdde6..3bace093db1 100644 --- a/jstests/core/eval7.js +++ b/jstests/core/eval7.js @@ -1,3 +1,5 @@ +// @tags: [requires_eval_command] + assert.writeOK(db.evalprep.insert({}), "db must exist for eval to succeed"); db.evalprep.drop(); diff --git a/jstests/core/eval9.js b/jstests/core/eval9.js index 1480ff6519c..82c230a8a95 100644 --- a/jstests/core/eval9.js +++ b/jstests/core/eval9.js @@ -1,3 +1,5 @@ +// @tags: [requires_eval_command] + assert.writeOK(db.evalprep.insert({}), "db must exist for eval to succeed"); db.evalprep.drop(); diff --git a/jstests/core/eval_mr.js b/jstests/core/eval_mr.js index 4a3dc8dad6c..33ca96eea01 100644 --- a/jstests/core/eval_mr.js +++ b/jstests/core/eval_mr.js @@ -1,4 +1,6 @@ // Test that the eval command can't be used to invoke the mapReduce command. SERVER-17889. +// +// @tags: [requires_eval_command] (function() { "use strict"; db.eval_mr.drop(); diff --git a/jstests/core/eval_nolock.js b/jstests/core/eval_nolock.js index 9511784becb..0fde2666f5d 100644 --- a/jstests/core/eval_nolock.js +++ b/jstests/core/eval_nolock.js @@ -1,3 +1,4 @@ +// @tags: [requires_eval_command] t = db.eval_nolock; t.drop(); diff --git a/jstests/core/evala.js b/jstests/core/evala.js index 7ccf33ac754..09241eeedee 100644 --- a/jstests/core/evala.js +++ b/jstests/core/evala.js @@ -1,3 +1,4 @@ +// @tags: [requires_eval_command] t = db.evala; t.drop(); diff --git a/jstests/core/evalb.js b/jstests/core/evalb.js index 3391c4cc4f2..2de16f4ea83 100644 --- a/jstests/core/evalb.js +++ b/jstests/core/evalb.js @@ -1,5 +1,7 @@ // Check the return value of a db.eval function running a database query, and ensure the function's // contents are logged in the profile log. +// +// @tags: [requires_eval_command] // Use a reserved database name to avoid a conflict in the parallel test suite. var stddb = db; diff --git a/jstests/core/evald.js b/jstests/core/evald.js index 8049d2ba8ae..43e74fc4600 100644 --- a/jstests/core/evald.js +++ b/jstests/core/evald.js @@ -1,3 +1,5 @@ +// @tags: [requires_eval_command] + t = db.jstests_evald; t.drop(); diff --git a/jstests/core/evale.js b/jstests/core/evale.js index 1ddc8519fc6..20384e0d741 100644 --- a/jstests/core/evale.js +++ b/jstests/core/evale.js @@ -1,3 +1,5 @@ +// @tags: [requires_eval_command] + t = db.jstests_evale; t.drop(); diff --git a/jstests/core/evalg.js b/jstests/core/evalg.js index 18503659217..56cb1dedd5f 100644 --- a/jstests/core/evalg.js +++ b/jstests/core/evalg.js @@ -1,4 +1,6 @@ // SERVER-17499: Test behavior of getMore on aggregation cursor under eval command. +// +// @tags: [requires_eval_command] db.evalg.drop(); for (var i = 0; i < 102; ++i) { db.evalg.insert({}); diff --git a/jstests/core/evalh.js b/jstests/core/evalh.js index 11e672f6bf4..b9c0d486d59 100644 --- a/jstests/core/evalh.js +++ b/jstests/core/evalh.js @@ -1,5 +1,7 @@ /** * Test that db.eval does not support auth. + * + * @tags: [requires_eval_command] */ (function() { 'use strict'; diff --git a/jstests/core/evalj.js b/jstests/core/evalj.js index f2326fff365..d6ef46430de 100644 --- a/jstests/core/evalj.js +++ b/jstests/core/evalj.js @@ -1,3 +1,5 @@ +// @tags: [requires_eval_command] + (function() { "use strict"; diff --git a/jstests/core/existsa.js b/jstests/core/existsa.js index e9430b489a3..d98fd3f2d68 100644 --- a/jstests/core/existsa.js +++ b/jstests/core/existsa.js @@ -1,101 +1,111 @@ -// Sparse indexes are disallowed for $exists:false queries. SERVER-3918 - -t = db.jstests_existsa; -t.drop(); - -t.save({}); -t.save({a: 1}); -t.save({a: {x: 1}, b: 1}); - -/** Configure testing of an index { <indexKeyField>:1 }. */ -function setIndex(_indexKeyField) { - indexKeyField = _indexKeyField; - indexKeySpec = {}; - indexKeySpec[indexKeyField] = 1; - t.ensureIndex(indexKeySpec, {sparse: true}); -} -setIndex('a'); - -/** @return count when hinting the index to use. */ -function hintedCount(query) { - return t.find(query).hint(indexKeySpec).itcount(); -} - -/** The query field does not exist and the sparse index is not used without a hint. */ -function assertMissing(query, expectedMissing, expectedIndexedMissing) { - expectedMissing = expectedMissing || 1; - expectedIndexedMissing = expectedIndexedMissing || 0; - assert.eq(expectedMissing, t.count(query)); - // We also shouldn't get a different count depending on whether - // an index is used or not. - assert.eq(expectedIndexedMissing, hintedCount(query)); -} - -/** The query field exists and the sparse index is used without a hint. */ -function assertExists(query, expectedExists) { - expectedExists = expectedExists || 2; - assert.eq(expectedExists, t.count(query)); - // An $exists:true predicate generates no index filters. Add another predicate on the index key - // to trigger use of the index. - andClause = {}; - andClause[indexKeyField] = {$ne: null}; - Object.extend(query, {$and: [andClause]}); - assert.eq(expectedExists, t.count(query)); - assert.eq(expectedExists, hintedCount(query)); -} - -/** The query field exists and the sparse index is not used without a hint. */ -function assertExistsUnindexed(query, expectedExists) { - expectedExists = expectedExists || 2; - assert.eq(expectedExists, t.count(query)); - // Even with another predicate on the index key, the sparse index is disallowed. - andClause = {}; - andClause[indexKeyField] = {$ne: null}; - Object.extend(query, {$and: [andClause]}); - assert.eq(expectedExists, t.count(query)); - assert.eq(expectedExists, hintedCount(query)); -} - -// $exists:false queries match the proper number of documents and disallow the sparse index. -assertMissing({a: {$exists: false}}); -assertMissing({a: {$not: {$exists: true}}}); -assertMissing({$and: [{a: {$exists: false}}]}); -assertMissing({$or: [{a: {$exists: false}}]}); -assertMissing({$nor: [{a: {$exists: true}}]}); -assertMissing({'a.x': {$exists: false}}, 2, 1); - -// Currently a sparse index is disallowed even if the $exists:false query is on a different field. -assertMissing({b: {$exists: false}}, 2, 1); -assertMissing({b: {$exists: false}, a: {$ne: 6}}, 2, 1); -assertMissing({b: {$not: {$exists: true}}}, 2, 1); - -// Top level $exists:true queries match the proper number of documents -// and use the sparse index on { a : 1 }. -assertExists({a: {$exists: true}}); - -// Nested $exists queries match the proper number of documents and disallow the sparse index. -assertExistsUnindexed({$nor: [{a: {$exists: false}}]}); -assertExistsUnindexed({$nor: [{'a.x': {$exists: false}}]}, 1); -assertExistsUnindexed({a: {$not: {$exists: false}}}); - -// Nested $exists queries disallow the sparse index in some cases where it is not strictly -// necessary to do so. (Descriptive tests.) -assertExistsUnindexed({$nor: [{b: {$exists: false}}]}, 1); // Unindexed field. -assertExists({$or: [{a: {$exists: true}}]}); // $exists:true not $exists:false. - -// Behavior is similar with $elemMatch. -t.drop(); -t.save({a: [{}]}); -t.save({a: [{b: 1}]}); -t.save({a: [{b: 1}]}); -setIndex('a.b'); - -assertMissing({a: {$elemMatch: {b: {$exists: false}}}}); -// A $elemMatch predicate is treated as nested, and the index should be used for $exists:true. -assertExists({a: {$elemMatch: {b: {$exists: true}}}}); - -// A non sparse index will not be disallowed. -t.drop(); -t.save({}); -t.ensureIndex({a: 1}); -assert.eq(1, t.find({a: {$exists: false}}).itcount()); +/** + * Tests that sparse indexes are disallowed for $exists:false queries. + */ +(function() { + "use strict"; + + const coll = db.jstests_existsa; + coll.drop(); + + assert.writeOK(coll.insert({})); + assert.writeOK(coll.insert({a: 1})); + assert.writeOK(coll.insert({a: {x: 1}, b: 1})); + + let indexKeySpec = {}; + let indexKeyField = ''; + + /** Configure testing of an index { <indexKeyField>:1 }. */ + function setIndex(_indexKeyField) { + indexKeyField = _indexKeyField; + indexKeySpec = {}; + indexKeySpec[indexKeyField] = 1; + coll.ensureIndex(indexKeySpec, {sparse: true}); + } + setIndex('a'); + + /** @return count when hinting the index to use. */ + function hintedCount(query) { + return coll.find(query).hint(indexKeySpec).itcount(); + } + + /** The query field does not exist and the sparse index is not used without a hint. */ + function assertMissing(query, expectedMissing = 1, expectedIndexedMissing = 0) { + assert.eq(expectedMissing, coll.count(query)); + // We also shouldn't get a different count depending on whether + // an index is used or not. + assert.eq(expectedIndexedMissing, hintedCount(query)); + } + + /** The query field exists and the sparse index is used without a hint. */ + function assertExists(query, expectedExists = 2) { + assert.eq(expectedExists, coll.count(query)); + // An $exists:true predicate generates no index filters. Add another predicate on the index + // key to trigger use of the index. + let andClause = {}; + andClause[indexKeyField] = {$ne: null}; + Object.extend(query, {$and: [andClause]}); + assert.eq(expectedExists, coll.count(query)); + assert.eq(expectedExists, hintedCount(query)); + } + + /** The query field exists and the sparse index is not used without a hint. */ + function assertExistsUnindexed(query, expectedExists = 2) { + assert.eq(expectedExists, coll.count(query)); + // Even with another predicate on the index key, the sparse index is disallowed. + let andClause = {}; + andClause[indexKeyField] = {$ne: null}; + Object.extend(query, {$and: [andClause]}); + assert.eq(expectedExists, coll.count(query)); + assert.eq(expectedExists, hintedCount(query)); + } + + // $exists:false queries match the proper number of documents and disallow the sparse index. + assertMissing({a: {$exists: false}}); + assertMissing({a: {$not: {$exists: true}}}); + assertMissing({$and: [{a: {$exists: false}}]}); + assertMissing({$or: [{a: {$exists: false}}]}); + assertMissing({$nor: [{a: {$exists: true}}]}); + assertMissing({'a.x': {$exists: false}}, 2, 1); + + // Currently a sparse index is disallowed even if the $exists:false query is on a different + // field. + assertMissing({b: {$exists: false}}, 2, 1); + assertMissing({b: {$exists: false}, a: {$ne: 6}}, 2, 1); + assertMissing({b: {$not: {$exists: true}}}, 2, 1); + + // Top level $exists:true queries match the proper number of documents + // and use the sparse index on { a : 1 }. + assertExists({a: {$exists: true}}); + + // Nested $exists queries match the proper number of documents and disallow the sparse index. + assertExistsUnindexed({$nor: [{a: {$exists: false}}]}); + assertExistsUnindexed({$nor: [{'a.x': {$exists: false}}]}, 1); + assertExistsUnindexed({a: {$not: {$exists: false}}}); + + // Nested $exists queries disallow the sparse index in some cases where it is not strictly + // necessary to do so. (Descriptive tests.) + assertExistsUnindexed({$nor: [{b: {$exists: false}}]}, 1); // Unindexed field. + assertExists({$or: [{a: {$exists: true}}]}); // $exists:true not $exists:false. + + // Behavior is similar with $elemMatch. + coll.drop(); + assert.writeOK(coll.insert({a: [{}]})); + assert.writeOK(coll.insert({a: [{b: 1}]})); + assert.writeOK(coll.insert({a: [{b: [1]}]})); + setIndex('a.b'); + + assertMissing({a: {$elemMatch: {b: {$exists: false}}}}); + + // A $elemMatch predicate is treated as nested, and the index should be used for $exists:true. + assertExists({a: {$elemMatch: {b: {$exists: true}}}}); + + // A $not within $elemMatch should not attempt to use a sparse index for $exists:false. + assertExistsUnindexed({'a.b': {$elemMatch: {$not: {$exists: false}}}}, 1); + assertExistsUnindexed({'a.b': {$elemMatch: {$gt: 0, $not: {$exists: false}}}}, 1); + + // A non sparse index will not be disallowed. + coll.drop(); + assert.writeOK(coll.insert({})); + coll.ensureIndex({a: 1}); + assert.eq(1, coll.find({a: {$exists: false}}).itcount()); +})(); diff --git a/jstests/core/fsync.js b/jstests/core/fsync.js index 13ff20e4177..2ad625d88ba 100644 --- a/jstests/core/fsync.js +++ b/jstests/core/fsync.js @@ -6,6 +6,8 @@ * - Confirm that writes can progress after fsyncUnlock * - Confirm that the command can be run repeatedly without breaking things * - Confirm that the pseudo commands and eval can perform fsyncLock/Unlock + * + * @tags: [requires_eval_command] */ (function() { "use strict"; diff --git a/jstests/core/fts_dotted_prefix_fields.js b/jstests/core/fts_dotted_prefix_fields.js new file mode 100644 index 00000000000..f811c4a7203 --- /dev/null +++ b/jstests/core/fts_dotted_prefix_fields.js @@ -0,0 +1,15 @@ +// Test that text search works correct when the text index has dotted paths as the non-text +// prefixes. +(function() { + "use strict"; + + let coll = db.fts_dotted_prefix_fields; + coll.drop(); + assert.commandWorked(coll.createIndex({"a.x": 1, "a.y": 1, "b.x": 1, "b.y": 1, words: "text"})); + assert.writeOK(coll.insert({a: {x: 1, y: 2}, b: {x: 3, y: 4}, words: "lorem ipsum dolor sit"})); + assert.writeOK(coll.insert({a: {x: 1, y: 2}, b: {x: 5, y: 4}, words: "lorem ipsum dolor sit"})); + + assert.eq(1, + coll.find({$text: {$search: "lorem ipsum"}, "a.x": 1, "a.y": 2, "b.x": 3, "b.y": 4}) + .itcount()); +}()); diff --git a/jstests/core/fts_trailing_fields.js b/jstests/core/fts_trailing_fields.js new file mode 100644 index 00000000000..2c7f79b423d --- /dev/null +++ b/jstests/core/fts_trailing_fields.js @@ -0,0 +1,22 @@ +// Tests for predicates which can use the trailing field of a text index. +(function() { + "use strict"; + + const coll = db.fts_trailing_fields; + + coll.drop(); + assert.commandWorked(coll.createIndex({a: 1, b: "text", c: 1})); + assert.writeOK(coll.insert({a: 2, b: "lorem ipsum"})); + + assert.eq(0, coll.find({a: 2, $text: {$search: "lorem"}, c: {$exists: true}}).itcount()); + assert.eq(1, coll.find({a: 2, $text: {$search: "lorem"}, c: null}).itcount()); + assert.eq(1, coll.find({a: 2, $text: {$search: "lorem"}, c: {$exists: false}}).itcount()); + + // An equality predicate on the leading field isn't useful, but it shouldn't cause any problems. + // Same with an $elemMatch predicate on one of the trailing fields. + coll.drop(); + assert.commandWorked(coll.createIndex({a: 1, b: "text", "c.d": 1})); + assert.writeOK(coll.insert({a: 2, b: "lorem ipsum", c: {d: 3}})); + assert.eq(0, coll.find({a: [1, 2], $text: {$search: "lorem"}}).itcount()); + assert.eq(0, coll.find({a: 2, $text: {$search: "lorem"}, c: {$elemMatch: {d: 3}}}).itcount()); +}()); diff --git a/jstests/core/function_string_representations.js b/jstests/core/function_string_representations.js new file mode 100644 index 00000000000..af66e9160a9 --- /dev/null +++ b/jstests/core/function_string_representations.js @@ -0,0 +1,38 @@ +/** Demonstrate that mapReduce can accept functions represented by strings. + * Some drivers do not have a type which represents a Javascript function. These languages represent + * the arguments to mapReduce as strings. + */ + +(function() { + "use strict"; + + var col = db.function_string_representations; + col.drop(); + assert.writeOK(col.insert({ + _id: "abc123", + ord_date: new Date("Oct 04, 2012"), + status: 'A', + price: 25, + items: [{sku: "mmm", qty: 5, price: 2.5}, {sku: "nnn", qty: 5, price: 2.5}] + })); + + var mapFunction = "function() {emit(this._id, this.price);}"; + var reduceFunction = "function(keyCustId, valuesPrices) {return Array.sum(valuesPrices);}"; + assert.commandWorked(col.mapReduce(mapFunction, reduceFunction, {out: "map_reduce_example"})); + + // Provided strings may end with semicolons and/or whitespace + mapFunction += " ; "; + reduceFunction += " ; "; + assert.commandWorked(col.mapReduce(mapFunction, reduceFunction, {out: "map_reduce_example"})); + + // $where exhibits the same behavior + var whereFunction = "function() {return this.price === 25;}"; + assert.eq(1, col.find({$where: whereFunction}).itcount()); + + whereFunction += ";"; + assert.eq(1, col.find({$where: whereFunction}).itcount()); + + // db.eval does not need to be tested, as it accepts code fragments, not functions. + // system.js does not need to be tested, as its contents types' are preserved, and + // strings are not promoted into functions. +})(); diff --git a/jstests/core/geo_2d_trailing_fields.js b/jstests/core/geo_2d_trailing_fields.js new file mode 100644 index 00000000000..8f9c881ae4c --- /dev/null +++ b/jstests/core/geo_2d_trailing_fields.js @@ -0,0 +1,45 @@ +// Tests for predicates which can use the trailing field of a 2d index. +(function() { + "use strict"; + + const coll = db.geo_2d_trailing_fields; + + const isMaster = assert.commandWorked(db.adminCommand({isMaster: 1})); + const isMongos = (isMaster.msg === "isdbgrid"); + + coll.drop(); + assert.commandWorked(coll.createIndex({a: "2d", b: 1})); + assert.writeOK(coll.insert({a: [0, 0]})); + + // Verify that $near queries handle existence predicates over the trailing fields correctly. + if (!isMongos) { + assert.eq(0, coll.find({a: {$near: [0, 0]}, b: {$exists: true}}).itcount()); + assert.eq(1, coll.find({a: {$near: [0, 0]}, b: null}).itcount()); + assert.eq(1, coll.find({a: {$near: [0, 0]}, b: {$exists: false}}).itcount()); + } + + // Verify that non-near 2d queries handle existence predicates over the trailing fields + // correctly. + assert.eq(0, + coll.find({a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$exists: true}}).itcount()); + assert.eq(1, coll.find({a: {$geoWithin: {$center: [[0, 0], 1]}}, b: null}).itcount()); + assert.eq(1, + coll.find({a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$exists: false}}).itcount()); + + coll.drop(); + assert.commandWorked(coll.createIndex({a: "2d", "b.c": 1})); + assert.writeOK(coll.insert({a: [0, 0], b: [{c: 2}, {c: 3}]})); + + // Verify that $near queries correctly handle predicates which cannot be covered due to array + // semantics. + if (!isMongos) { + assert.eq(0, coll.find({a: {$near: [0, 0]}, "b.c": [2, 3]}).itcount()); + assert.eq(0, coll.find({a: {$near: [0, 0]}, "b.c": {$type: "array"}}).itcount()); + } + + // Verify that non-near 2d queries correctly handle predicates which cannot be covered due to + // array semantics. + assert.eq(0, coll.find({a: {$geoWithin: {$center: [[0, 0], 1]}}, "b.c": [2, 3]}).itcount()); + assert.eq( + 0, coll.find({a: {$geoWithin: {$center: [[0, 0], 1]}}, "b.c": {$type: "array"}}).itcount()); +}()); diff --git a/jstests/core/geo_s2cursorlimitskip.js b/jstests/core/geo_s2cursorlimitskip.js index 427fbf8fe29..dc645dc68af 100644 --- a/jstests/core/geo_s2cursorlimitskip.js +++ b/jstests/core/geo_s2cursorlimitskip.js @@ -1,4 +1,11 @@ // Test various cursor behaviors +// +// @tags: [ +// # This test attempts to enable profiling on a server and then get profiling data by reading +// # from the "system.profile" collection. The former operation must be routed to the primary in +// # a replica set, whereas the latter may be routed to a secondary. +// assumes_read_preference_unchanged, +// ] var testDB = db.getSiblingDB("geo_s2cursorlimitskip"); var t = testDB.geo_s2getmmm; diff --git a/jstests/core/geo_update_btree.js b/jstests/core/geo_update_btree.js index a85d4274415..b4fa24df57e 100644 --- a/jstests/core/geo_update_btree.js +++ b/jstests/core/geo_update_btree.js @@ -1,4 +1,6 @@ // Tests whether the geospatial search is stable under btree updates +// +// @tags: [assumes_write_concern_unchanged] var coll = db.getCollection("jstests_geo_update_btree"); coll.drop(); diff --git a/jstests/core/getlog2.js b/jstests/core/getlog2.js index 597a85e20ee..e5287ea8c1b 100644 --- a/jstests/core/getlog2.js +++ b/jstests/core/getlog2.js @@ -1,4 +1,11 @@ // tests getlog as well as slow querying logging +// +// @tags: [ +// # This test attempts to perform a find command and see that it ran using the getLog command. +// # The former operation may be routed to a secondary in the replica set, whereas the latter must +// # be routed to the primary. +// assumes_read_preference_unchanged, +// ] glcol = db.getLogTest2; glcol.drop(); diff --git a/jstests/core/index_elemmatch2.js b/jstests/core/index_elemmatch2.js new file mode 100644 index 00000000000..ecd24035284 --- /dev/null +++ b/jstests/core/index_elemmatch2.js @@ -0,0 +1,63 @@ +/** + * Test that queries containing $elemMatch correctly use an index if each child expression is + * compatible with the index. + */ +(function() { + "use strict"; + + load("jstests/libs/analyze_plan.js"); + + const coll = db.elemMatch_index; + coll.drop(); + + assert.writeOK(coll.insert({a: 1})); + assert.writeOK(coll.insert({a: [{}]})); + assert.writeOK(coll.insert({a: [1, null]})); + assert.writeOK(coll.insert({a: [{type: "Point", coordinates: [0, 0]}]})); + + assert.commandWorked(coll.createIndex({a: 1}, {sparse: true})); + + function assertIndexResults(coll, query, useIndex, nReturned) { + const explainPlan = coll.find(query).explain("executionStats"); + assert.eq(isIxscan(explainPlan.queryPlanner.winningPlan), useIndex); + assert.eq(explainPlan.executionStats.nReturned, nReturned); + } + + assertIndexResults(coll, {a: {$elemMatch: {$exists: false}}}, false, 0); + + // An $elemMatch predicate is treated as nested, and the index should be used for $exists:true. + assertIndexResults(coll, {a: {$elemMatch: {$exists: true}}}, true, 3); + + // $not within $elemMatch should not attempt to use a sparse index for $exists:false. + assertIndexResults(coll, {a: {$elemMatch: {$not: {$exists: false}}}}, false, 3); + assertIndexResults(coll, {a: {$elemMatch: {$gt: 0, $not: {$exists: false}}}}, false, 1); + + // $geo within $elemMatch should not attempt to use a non-geo index. + assertIndexResults( + coll, + { + a: { + $elemMatch: { + $geoWithin: { + $geometry: + {type: "Polygon", coordinates: [[[0, 0], [0, 1], [1, 0], [0, 0]]]} + } + } + } + }, + false, + 1); + + // $in with a null value within $elemMatch should use a sparse index. + assertIndexResults(coll, {a: {$elemMatch: {$in: [null]}}}, true, 1); + + // $eq with a null value within $elemMatch should use a sparse index. + assertIndexResults(coll, {a: {$elemMatch: {$eq: null}}}, true, 1); + + // A negated regex within $elemMatch should not use an index, sparse or not. + assertIndexResults(coll, {a: {$elemMatch: {$not: {$in: [/^a/]}}}}, false, 3); + + coll.dropIndexes(); + assert.commandWorked(coll.createIndex({a: 1})); + assertIndexResults(coll, {a: {$elemMatch: {$not: {$in: [/^a/]}}}}, false, 3); +})(); diff --git a/jstests/core/index_filter_commands.js b/jstests/core/index_filter_commands.js index 8684be3b2b9..58f78d0514e 100644 --- a/jstests/core/index_filter_commands.js +++ b/jstests/core/index_filter_commands.js @@ -6,20 +6,24 @@ * Displays index filters for all query shapes in a collection. * * - planCacheClearFilters - * Clears index filter for a single query shape or, - * if the query shape is omitted, all filters for the collection. + * Clears index filter for a single query shape or, if the query shape is omitted, all filters for + * the collection. * * - planCacheSetFilter * Sets index filter for a query shape. Overrides existing filter. * - * Not a lot of data access in this test suite. Hint commands - * manage a non-persistent mapping in the server of - * query shape to list of index specs. + * Not a lot of data access in this test suite. Hint commands manage a non-persistent mapping in the + * server of query shape to list of index specs. * - * Only time we might need to execute a query is to check the plan - * cache state. We would do this with the planCacheListPlans command - * on the same query shape with the index filters. + * Only time we might need to execute a query is to check the plan cache state. We would do this + * with the planCacheListPlans command on the same query shape with the index filters. * + * @tags: [ + * # This test attempts to perform queries with plan cache filters set up. The former operation + * # may be routed to a secondary in the replica set, whereas the latter must be routed to the + * # primary. + * assumes_read_preference_unchanged, + * ] */ load("jstests/libs/analyze_plan.js"); diff --git a/jstests/core/index_stats.js b/jstests/core/index_stats.js index 60b37fd571e..ee4d13d4d0a 100644 --- a/jstests/core/index_stats.js +++ b/jstests/core/index_stats.js @@ -1,3 +1,10 @@ +// @tags: [ +// # This test attempts to perform write operations and get index usage statistics using the +// # $indexStats stage. The former operation must be routed to the primary in a replica set, +// # whereas the latter may be routed to a secondary. +// assumes_read_preference_unchanged, +// ] + (function() { "use strict"; diff --git a/jstests/core/js3.js b/jstests/core/js3.js index 4d46c25bbf7..c808e7ec75a 100644 --- a/jstests/core/js3.js +++ b/jstests/core/js3.js @@ -1,3 +1,4 @@ +// @tags: [requires_eval_command] t = db.jstests_js3; diff --git a/jstests/core/js7.js b/jstests/core/js7.js index 810f4692d4f..99cddd114f8 100644 --- a/jstests/core/js7.js +++ b/jstests/core/js7.js @@ -1,3 +1,5 @@ +// @tags: [requires_eval_command] + t = db.jstests_js7; t.drop(); diff --git a/jstests/core/js9.js b/jstests/core/js9.js index 515fa883aea..e0703ac39ea 100644 --- a/jstests/core/js9.js +++ b/jstests/core/js9.js @@ -1,3 +1,5 @@ +// @tags: [requires_eval_command] + c = db.jstests_js9; c.drop(); diff --git a/jstests/core/js_jit.js b/jstests/core/js_jit.js new file mode 100644 index 00000000000..4ccdd2917ae --- /dev/null +++ b/jstests/core/js_jit.js @@ -0,0 +1,40 @@ +/** + * Validate various native types continue to work when run in JITed code. + * + * In SERVER-30362, the JIT would not compile natives types which had custom getProperty + * implementations correctly. We force the JIT to kick in by using large loops. + */ +(function() { + 'use strict'; + + function testDBCollection() { + const c = new DBCollection(null, null, "foo", "test.foo"); + for (let i = 0; i < 100000; i++) { + if (c.toString() != "test.foo") { + throw i; + } + } + } + + function testDB() { + const c = new DB(null, "test"); + for (let i = 0; i < 100000; i++) { + if (c.toString() != "test") { + throw i; + } + } + } + + function testDBQuery() { + const c = DBQuery('a', 'b', 'c', 'd'); + for (let i = 0; i < 100000; i++) { + if (c.toString() != "DBQuery: d -> null") { + throw i; + } + } + } + + testDBCollection(); + testDB(); + testDBQuery(); +})();
\ No newline at end of file diff --git a/jstests/core/list_collections1.js b/jstests/core/list_collections1.js index c8c3f92fbc9..ff1aac304ff 100644 --- a/jstests/core/list_collections1.js +++ b/jstests/core/list_collections1.js @@ -72,8 +72,8 @@ // var getListCollectionsCursor = function(options, subsequentBatchSize) { - return new DBCommandCursor( - mydb.getMongo(), mydb.runCommand("listCollections", options), subsequentBatchSize); + var res = mydb.runCommand("listCollections", options); + return new DBCommandCursor(res._mongo, res, subsequentBatchSize); }; var cursorCountMatching = function(cursor, pred) { @@ -282,9 +282,9 @@ assert.commandWorked(mydb.createCollection("quux")); res = mydb.runCommand("listCollections", {cursor: {batchSize: 0}}); - cursor = new DBCommandCursor(mydb.getMongo(), res, 2); + cursor = new DBCommandCursor(res._mongo, res, 2); cursor.close(); - cursor = new DBCommandCursor(mydb.getMongo(), res, 2); + cursor = new DBCommandCursor(res._mongo, res, 2); assert.throws(function() { cursor.hasNext(); }); diff --git a/jstests/core/list_collections_filter.js b/jstests/core/list_collections_filter.js index e0d18f055d0..0516d57a417 100644 --- a/jstests/core/list_collections_filter.js +++ b/jstests/core/list_collections_filter.js @@ -19,8 +19,8 @@ filter = {}; } - var cursor = new DBCommandCursor(mydb.getMongo(), - mydb.runCommand("listCollections", {filter: filter})); + var res = mydb.runCommand("listCollections", {filter: filter}); + var cursor = new DBCommandCursor(res._mongo, res); function stripToName(result) { return result.name; } diff --git a/jstests/core/list_indexes.js b/jstests/core/list_indexes.js index de0f4473980..6ec9a7a13e1 100644 --- a/jstests/core/list_indexes.js +++ b/jstests/core/list_indexes.js @@ -27,8 +27,8 @@ // var getListIndexesCursor = function(coll, options, subsequentBatchSize) { - return new DBCommandCursor( - coll.getDB().getMongo(), coll.runCommand("listIndexes", options), subsequentBatchSize); + var res = coll.runCommand("listIndexes", options); + return new DBCommandCursor(res._mongo, res, subsequentBatchSize); }; var cursorGetIndexSpecs = function(cursor) { @@ -163,9 +163,9 @@ assert.commandWorked(coll.ensureIndex({c: 1}, {unique: true})); res = coll.runCommand("listIndexes", {cursor: {batchSize: 0}}); - cursor = new DBCommandCursor(coll.getDB().getMongo(), res, 2); + cursor = new DBCommandCursor(res._mongo, res, 2); cursor.close(); - cursor = new DBCommandCursor(coll.getDB().getMongo(), res, 2); + cursor = new DBCommandCursor(res._mongo, res, 2); assert.throws(function() { cursor.hasNext(); }); diff --git a/jstests/core/list_indexes_invalidation.js b/jstests/core/list_indexes_invalidation.js index b8cbe5eb134..9fe94de5efc 100644 --- a/jstests/core/list_indexes_invalidation.js +++ b/jstests/core/list_indexes_invalidation.js @@ -19,7 +19,7 @@ printjson(res); // Ensure the cursor has data, rename or drop the collection, and exhaust the cursor. - let cursor = new DBCommandCursor(db.getMongo(), res); + let cursor = new DBCommandCursor(res._mongo, res); let errMsg = 'expected more data from command ' + tojson(cmd) + ', with result ' + tojson(res); assert(cursor.hasNext(), errMsg); diff --git a/jstests/core/list_namespaces_invalidation.js b/jstests/core/list_namespaces_invalidation.js index 6f8033b5fe4..85eb26510bb 100644 --- a/jstests/core/list_namespaces_invalidation.js +++ b/jstests/core/list_namespaces_invalidation.js @@ -1,4 +1,6 @@ // SERVER-27996/SERVER-28022 Missing invalidation for system.namespaces writes +// +// @tags: [requires_collmod_command] (function() { 'use strict'; let dbInvalidName = 'system_namespaces_invalidations'; diff --git a/jstests/core/max_time_ms.js b/jstests/core/max_time_ms.js index 0442ffcba68..efe3dabcc6d 100644 --- a/jstests/core/max_time_ms.js +++ b/jstests/core/max_time_ms.js @@ -1,4 +1,12 @@ // Tests query/command option $maxTimeMS. +// +// @tags: [ +// # This test attempts to perform read operations after having enabled the maxTimeAlwaysTimeOut +// # failpoint. The former operations may be routed to a secondary in the replica set, whereas the +// # latter must be routed to the primary. +// assumes_read_preference_unchanged, +// requires_collmod_command, +// ] var t = db.max_time_ms; var exceededTimeLimit = 50; // ErrorCodes::ExceededTimeLimit diff --git a/jstests/core/mr4.js b/jstests/core/mr4.js index 58ea303f7e8..683583acecd 100644 --- a/jstests/core/mr4.js +++ b/jstests/core/mr4.js @@ -9,7 +9,7 @@ t.save({x: 4, tags: ["b", "c"]}); m = function() { this.tags.forEach(function(z) { - emit(z, {count: xx}); + emit(z, {count: xx.val}); }); }; @@ -21,7 +21,7 @@ r = function(key, values) { return {count: total}; }; -res = t.mapReduce(m, r, {out: "mr4_out", scope: {xx: 1}}); +res = t.mapReduce(m, r, {out: "mr4_out", scope: {xx: {val: 1}}}); z = res.convertToSingleObject(); assert.eq(3, Object.keySet(z).length, "A1"); @@ -31,7 +31,7 @@ assert.eq(3, z.c.count, "A4"); res.drop(); -res = t.mapReduce(m, r, {scope: {xx: 2}, out: "mr4_out"}); +res = t.mapReduce(m, r, {scope: {xx: {val: 2}}, out: "mr4_out"}); z = res.convertToSingleObject(); assert.eq(3, Object.keySet(z).length, "A1"); diff --git a/jstests/core/mr_killop.js b/jstests/core/mr_killop.js index 78e98f0bcaa..e85986e2fc0 100644 --- a/jstests/core/mr_killop.js +++ b/jstests/core/mr_killop.js @@ -4,7 +4,7 @@ t = db.jstests_mr_killop; t.drop(); t2 = db.jstests_mr_killop_out; t2.drop(); - +db.adminCommand({"configureFailPoint": 'mr_killop_test_fp', "mode": 'alwaysOn'}); function debug(x) { // printjson( x ); } @@ -171,3 +171,4 @@ var loop = function() { }; runMRTests(loop, false); runFinalizeTests(loop, false); +db.adminCommand({"configureFailPoint": 'mr_killop_test_fp', "mode": 'off'}); diff --git a/jstests/core/mr_optim.js b/jstests/core/mr_optim.js index 7437753ca67..1c525ae3de3 100644 --- a/jstests/core/mr_optim.js +++ b/jstests/core/mr_optim.js @@ -3,8 +3,17 @@ t = db.mr_optim; t.drop(); +// We drop the output collection to ensure the test can be run multiple times successfully. We +// explicitly avoid using the DBCollection#drop() shell helper to avoid implicitly sharding the +// collection during the sharded_collections_jscore_passthrough.yml test suite when reading the +// results from the output collection in the reformat() function. +var res = db.runCommand({drop: "mr_optim_out"}); +if (res.ok !== 1) { + assert.commandFailedWithCode(res, ErrorCodes.NamespaceNotFound); +} + for (var i = 0; i < 1000; ++i) { - t.save({a: Math.random(1000), b: Math.random(10000)}); + assert.writeOK(t.save({a: Math.random(1000), b: Math.random(10000)})); } function m() { @@ -21,7 +30,7 @@ function reformat(r) { if (r.results) cursor = r.results; else - cursor = r.find(); + cursor = r.find().sort({_id: 1}); cursor.forEach(function(z) { x[z._id] = z.value; }); @@ -43,4 +52,4 @@ res.drop(); assert.eq(x, x2, "object from inline and collection are not equal"); -t.drop();
\ No newline at end of file +t.drop(); diff --git a/jstests/core/no_db_created.js b/jstests/core/no_db_created.js index 3491914d470..b67b193494c 100644 --- a/jstests/core/no_db_created.js +++ b/jstests/core/no_db_created.js @@ -1,4 +1,6 @@ // checks that operations do not create a database +// +// @tags: [requires_collmod_command] (function() { "use strict"; @@ -32,4 +34,4 @@ noDB(mydb); assert.writeOK(coll.insert({})); mydb.dropDatabase(); -}());
\ No newline at end of file +}()); diff --git a/jstests/core/notablescan.js b/jstests/core/notablescan.js index 80306c08cf2..bb4c170a603 100644 --- a/jstests/core/notablescan.js +++ b/jstests/core/notablescan.js @@ -1,4 +1,11 @@ // check notablescan mode +// +// @tags: [ +// # This test attempts to perform read operations after having enabled the notablescan server +// # parameter. The former operations may be routed to a secondary in the replica set, whereas the +// # latter must be routed to the primary. +// assumes_read_preference_unchanged, +// ] t = db.test_notablescan; t.drop(); diff --git a/jstests/core/operation_latency_histogram.js b/jstests/core/operation_latency_histogram.js index 1e3f1a59b95..947a8be6520 100644 --- a/jstests/core/operation_latency_histogram.js +++ b/jstests/core/operation_latency_histogram.js @@ -1,4 +1,10 @@ // Checks that histogram counters for collections are updated as we expect. +// +// This test attempts to perform write operations and get latency statistics using the $collStats +// stage. The former operation must be routed to the primary in a replica set, whereas the latter +// may be routed to a secondary. +// +// @tags: [assumes_read_preference_unchanged] (function() { "use strict"; diff --git a/jstests/core/plan_cache_clear.js b/jstests/core/plan_cache_clear.js index 8f9cf0ea302..778239616b5 100644 --- a/jstests/core/plan_cache_clear.js +++ b/jstests/core/plan_cache_clear.js @@ -1,5 +1,12 @@ // Test clearing of the plan cache, either manually through the planCacheClear command, // or due to system events such as an index build. +// +// @tags: [ +// # This test attempts to perform queries and introspect/manipulate the server's plan cache +// # entries. The former operation may be routed to a secondary in the replica set, whereas the +// # latter must be routed to the primary. +// assumes_read_preference_unchanged, +// ] var t = db.jstests_plan_cache_clear; t.drop(); diff --git a/jstests/core/plan_cache_list_plans.js b/jstests/core/plan_cache_list_plans.js index 7ca599483ff..3359980ab07 100644 --- a/jstests/core/plan_cache_list_plans.js +++ b/jstests/core/plan_cache_list_plans.js @@ -1,4 +1,11 @@ // Test the planCacheListPlans command. +// +// @tags: [ +// # This test attempts to perform queries and introspect the server's plan cache entries. The +// # former operation may be routed to a secondary in the replica set, whereas the latter must be +// # routed to the primary. +// assumes_read_preference_unchanged, +// ] var t = db.jstests_plan_cache_list_plans; t.drop(); diff --git a/jstests/core/plan_cache_list_shapes.js b/jstests/core/plan_cache_list_shapes.js index 1c9ecdf9e1b..61c3111cd8a 100644 --- a/jstests/core/plan_cache_list_shapes.js +++ b/jstests/core/plan_cache_list_shapes.js @@ -1,5 +1,12 @@ // Test the planCacheListQueryShapes command, which returns a list of query shapes // for the queries currently cached in the collection. +// +// @tags: [ +// # This test attempts to perform queries with plan cache filters set up. The former operation +// # may be routed to a secondary in the replica set, whereas the latter must be routed to the +// # primary. +// assumes_read_preference_unchanged, +// ] var t = db.jstests_plan_cache_list_shapes; t.drop(); diff --git a/jstests/core/plan_cache_shell_helpers.js b/jstests/core/plan_cache_shell_helpers.js index dc990b19dcc..6c4ff185014 100644 --- a/jstests/core/plan_cache_shell_helpers.js +++ b/jstests/core/plan_cache_shell_helpers.js @@ -1,4 +1,11 @@ // Test the shell helpers which wrap the plan cache commands. +// +// @tags: [ +// # This test attempts to perform queries and introspect the server's plan cache entries. The +// # former operation may be routed to a secondary in the replica set, whereas the latter must be +// # routed to the primary. +// assumes_read_preference_unchanged, +// ] var t = db.jstests_plan_cache_shell_helpers; t.drop(); diff --git a/jstests/core/profile2.js b/jstests/core/profile2.js index bb1605abd1e..9da6410d2ac 100644 --- a/jstests/core/profile2.js +++ b/jstests/core/profile2.js @@ -24,7 +24,7 @@ assert(result.hasOwnProperty('millis')); assert(result.hasOwnProperty('query')); assert.eq('string', typeof(result.query)); // String value is truncated. -assert(result.query.match(/filter: { a: "a+\.\.\." } }$/)); +assert(result.query.match(/filter: { a: "a+\.\.\." }/)); assert.commandWorked(coll.getDB().runCommand({profile: 0})); coll.getDB().system.profile.drop(); diff --git a/jstests/core/profile_getmore.js b/jstests/core/profile_getmore.js index a9272567b1a..3f6f492597e 100644 --- a/jstests/core/profile_getmore.js +++ b/jstests/core/profile_getmore.js @@ -24,12 +24,13 @@ var cursor = coll.find({a: {$gt: 0}}).sort({a: 1}).batchSize(2); cursor.next(); // Perform initial query and consume first of 2 docs returned. - var cursorId = getLatestProfilerEntry(testDB).cursorid; // Save cursorid from find. + var cursorId = + getLatestProfilerEntry(testDB, {op: "query"}).cursorid; // Save cursorid from find. cursor.next(); // Consume second of 2 docs from initial query. cursor.next(); // getMore performed, leaving open cursor. - var profileObj = getLatestProfilerEntry(testDB); + var profileObj = getLatestProfilerEntry(testDB, {op: "getmore"}); assert.eq(profileObj.ns, coll.getFullName(), tojson(profileObj)); assert.eq(profileObj.op, "getmore", tojson(profileObj)); @@ -67,7 +68,7 @@ cursor.next(); // Consume second of 2 docs from initial query. cursor.next(); // getMore performed, leaving open cursor. - profileObj = getLatestProfilerEntry(testDB); + profileObj = getLatestProfilerEntry(testDB, {op: "getmore"}); assert.eq(profileObj.hasSortStage, true, tojson(profileObj)); @@ -83,7 +84,7 @@ cursor.next(); // Perform initial query and consume first of 3 docs returned. cursor.itcount(); // Exhaust the cursor. - profileObj = getLatestProfilerEntry(testDB); + profileObj = getLatestProfilerEntry(testDB, {op: "getmore"}); assert(profileObj.hasOwnProperty("cursorid"), tojson(profileObj)); // cursorid should always be present on getMore. @@ -101,12 +102,12 @@ assert.commandWorked(coll.createIndex({a: 1})); var cursor = coll.aggregate([{$match: {a: {$gte: 0}}}], {cursor: {batchSize: 0}}); - var cursorId = getLatestProfilerEntry(testDB).cursorid; + var cursorId = getLatestProfilerEntry(testDB, {"command.aggregate": coll.getName()}).cursorid; assert.neq(0, cursorId); cursor.next(); // Consume the result set. - profileObj = getLatestProfilerEntry(testDB); + profileObj = getLatestProfilerEntry(testDB, {op: "getmore"}); assert.eq(profileObj.ns, coll.getFullName(), tojson(profileObj)); assert.eq(profileObj.op, "getmore", tojson(profileObj)); diff --git a/jstests/core/profile_insert.js b/jstests/core/profile_insert.js index 3994c896bda..836ec2ecff2 100644 --- a/jstests/core/profile_insert.js +++ b/jstests/core/profile_insert.js @@ -1,4 +1,6 @@ // Confirms that profiled insert execution contains all expected metrics with proper values. +// +// @tags: [assumes_write_concern_unchanged] (function() { "use strict"; diff --git a/jstests/core/recursion.js b/jstests/core/recursion.js index 926250be20d..1db491c8ae9 100644 --- a/jstests/core/recursion.js +++ b/jstests/core/recursion.js @@ -1,5 +1,7 @@ -// Basic tests for a form of stack recursion that's been shown to cause C++ -// side stack overflows in the past. See SERVER-19614. +// Basic tests for a form of stack recursion that's been shown to cause C++ side stack overflows in +// the past. See SERVER-19614. +// +// @tags: [requires_eval_command] (function() { "use strict"; diff --git a/jstests/core/regex_not_id.js b/jstests/core/regex_not_id.js index 1f15250f240..35b2c858867 100644 --- a/jstests/core/regex_not_id.js +++ b/jstests/core/regex_not_id.js @@ -3,10 +3,10 @@ var testColl = db.regex_not_id; testColl.drop(); -assert.writeOK(testColl.insert({_id: "ABCDEF1"}, {writeConcern: {w: 1}})); +assert.writeOK(testColl.insert({_id: "ABCDEF1"})); // Should be an error. -assert.writeError(testColl.insert({_id: /^A/}, {writeConcern: {w: 1}})); +assert.writeError(testColl.insert({_id: /^A/})); // _id doesn't have to be first; still disallowed -assert.writeError(testColl.insert({xxx: "ABCDEF", _id: /ABCDEF/}, {writeConcern: {w: 1}}));
\ No newline at end of file +assert.writeError(testColl.insert({xxx: "ABCDEF", _id: /ABCDEF/})); diff --git a/jstests/core/remove8.js b/jstests/core/remove8.js index 563e4708cf9..3c9fd6a11a1 100644 --- a/jstests/core/remove8.js +++ b/jstests/core/remove8.js @@ -1,3 +1,4 @@ +// @tags: [requires_eval_command] t = db.remove8; t.drop(); diff --git a/jstests/core/rename4.js b/jstests/core/rename4.js index 185193deaa9..756918db5f6 100644 --- a/jstests/core/rename4.js +++ b/jstests/core/rename4.js @@ -1,3 +1,5 @@ +// @tags: [requires_eval_command] + t = db.jstests_rename4; t.drop(); diff --git a/jstests/core/rename_stayTemp.js b/jstests/core/rename_stayTemp.js index d8451af2d2d..8dee8f89fea 100644 --- a/jstests/core/rename_stayTemp.js +++ b/jstests/core/rename_stayTemp.js @@ -11,7 +11,7 @@ function ns(coll) { function istemp(name) { var result = db.runCommand("listCollections", {filter: {name: name}}); assert(result.ok); - var collections = new DBCommandCursor(db.getMongo(), result).toArray(); + var collections = new DBCommandCursor(result._mongo, result).toArray(); assert.eq(1, collections.length); return collections[0].options.temp ? true : false; } diff --git a/jstests/core/shell_connection_strings.js b/jstests/core/shell_connection_strings.js new file mode 100644 index 00000000000..22861e6ce25 --- /dev/null +++ b/jstests/core/shell_connection_strings.js @@ -0,0 +1,33 @@ +// Test mongo shell connect strings. +(function() { + 'use strict'; + + const mongod = new MongoURI(db.getMongo().host).servers[0]; + const host = mongod.host; + const port = mongod.port; + + function testConnect(ok, ...args) { + const exitCode = runMongoProgram('mongo', '--eval', ';', ...args); + if (ok) { + assert.eq(exitCode, 0, "failed to connect with `" + args.join(' ') + "`"); + } else { + assert.neq( + exitCode, 0, "unexpectedly succeeded connecting with `" + args.join(' ') + "`"); + } + } + + testConnect(true, `${host}:${port}`); + testConnect(true, `${host}:${port}/test`); + testConnect(true, `${host}:${port}/admin`); + testConnect(true, host, '--port', port); + testConnect(true, '--host', host, '--port', port, 'test'); + testConnect(true, '--host', host, '--port', port, 'admin'); + testConnect(true, `mongodb://${host}:${port}/test`); + testConnect(true, `mongodb://${host}:${port}/test?connectTimeoutMS=10000`); + + // if a full URI is provided, you cannot also specify host or port + testConnect(false, `${host}/test`, '--port', port); + testConnect(false, `mongodb://${host}:${port}/test`, '--port', port); + testConnect(false, `mongodb://${host}:${port}/test`, '--host', host); + testConnect(false, `mongodb://${host}:${port}/test`, '--host', host, '--port', port); +})(); diff --git a/jstests/core/shell_writeconcern.js b/jstests/core/shell_writeconcern.js index f3f190061cf..e3e7a23a9aa 100644 --- a/jstests/core/shell_writeconcern.js +++ b/jstests/core/shell_writeconcern.js @@ -1,7 +1,10 @@ "use strict"; + // check that shell writeconcern work correctly // 1.) tests that it can be set on each level and is inherited // 2.) tests that each operation (update/insert/remove/save) take and ensure a write concern +// +// @tags: [assumes_write_concern_unchanged] var collA = db.shell_wc_a; var collB = db.shell_wc_b; diff --git a/jstests/core/stages_delete.js b/jstests/core/stages_delete.js index f8e7380c75a..98a270cedf3 100644 --- a/jstests/core/stages_delete.js +++ b/jstests/core/stages_delete.js @@ -1,3 +1,9 @@ +// @tags: [ +// # This test attempts to remove documents using the stageDebug command, which doesn't support +// # specifying a writeConcern. +// assumes_write_concern_unchanged, +// ] + // Test basic delete stage functionality. var coll = db.stages_delete; var collScanStage = {cscan: {args: {direction: 1}, filter: {deleteMe: true}}}; diff --git a/jstests/core/startup_log.js b/jstests/core/startup_log.js index 3b0cbe3464d..c73013d1744 100644 --- a/jstests/core/startup_log.js +++ b/jstests/core/startup_log.js @@ -1,101 +1,108 @@ -load('jstests/aggregation/extras/utils.js');
-
-(function() {
- 'use strict';
-
- // Check that smallArray is entirely contained by largeArray
- // returns false if a member of smallArray is not in largeArray
- function arrayIsSubset(smallArray, largeArray) {
- for (var i = 0; i < smallArray.length; i++) {
- if (!Array.contains(largeArray, smallArray[i])) {
- print("Could not find " + smallArray[i] + " in largeArray");
- return false;
- }
- }
-
- return true;
- }
-
- // Test startup_log
- var stats = db.getSisterDB("local").startup_log.stats();
- assert(stats.capped);
-
- var latestStartUpLog =
- db.getSisterDB("local").startup_log.find().sort({$natural: -1}).limit(1).next();
- var serverStatus = db._adminCommand("serverStatus");
- var cmdLine = db._adminCommand("getCmdLineOpts").parsed;
-
- // Test that the startup log has the expected keys
- var verbose = false;
- var expectedKeys =
- ["_id", "hostname", "startTime", "startTimeLocal", "cmdLine", "pid", "buildinfo"];
- var keys = Object.keySet(latestStartUpLog);
- assert(arrayEq(expectedKeys, keys, verbose), 'startup_log keys failed');
-
- // Tests _id implicitly - should be comprised of host-timestamp
- // Setup expected startTime and startTimeLocal from the supplied timestamp
- var _id = latestStartUpLog._id.split('-'); // _id should consist of host-timestamp
- var _idUptime = _id.pop();
- var _idHost = _id.join('-');
- var uptimeSinceEpochRounded = Math.floor(_idUptime / 1000) * 1000;
- var startTime = new Date(uptimeSinceEpochRounded); // Expected startTime
-
- assert.eq(_idHost, latestStartUpLog.hostname, "Hostname doesn't match one from _id");
- assert.eq(serverStatus.host.split(':')[0],
- latestStartUpLog.hostname,
- "Hostname doesn't match one in server status");
- assert.closeWithinMS(startTime,
- latestStartUpLog.startTime,
- "StartTime doesn't match one from _id",
- 2000); // Expect less than 2 sec delta
- assert.eq(cmdLine, latestStartUpLog.cmdLine, "cmdLine doesn't match that from getCmdLineOpts");
- assert.eq(serverStatus.pid, latestStartUpLog.pid, "pid doesn't match that from serverStatus");
-
- // Test buildinfo
- var buildinfo = db.runCommand("buildinfo");
- delete buildinfo.ok; // Delete extra meta info not in startup_log
- var isMaster = db._adminCommand("ismaster");
-
- // Test buildinfo has the expected keys
- var expectedKeys = [
- "version",
- "gitVersion",
- "allocator",
- "versionArray",
- "javascriptEngine",
- "openssl",
- "buildEnvironment",
- "debug",
- "maxBsonObjectSize",
- "bits",
- "modules"
- ];
-
- var keys = Object.keySet(latestStartUpLog.buildinfo);
- // Disabled to check
- assert(arrayIsSubset(expectedKeys, keys),
- "buildinfo keys failed! \n expected:\t" + expectedKeys + "\n actual:\t" + keys);
- assert.eq(buildinfo,
- latestStartUpLog.buildinfo,
- "buildinfo doesn't match that from buildinfo command");
-
- // Test version and version Array
- var version = latestStartUpLog.buildinfo.version.split('-')[0];
- var versionArray = latestStartUpLog.buildinfo.versionArray;
- var versionArrayCleaned = versionArray.slice(0, 3);
- if (versionArray[3] == -100) {
- versionArrayCleaned[2] -= 1;
- }
-
- assert.eq(serverStatus.version,
- latestStartUpLog.buildinfo.version,
- "Mongo version doesn't match that from ServerStatus");
- assert.eq(
- version, versionArrayCleaned.join('.'), "version doesn't match that from the versionArray");
- var jsEngine = latestStartUpLog.buildinfo.javascriptEngine;
- assert((jsEngine == "none") || jsEngine.startsWith("mozjs"));
- assert.eq(isMaster.maxBsonObjectSize,
- latestStartUpLog.buildinfo.maxBsonObjectSize,
- "maxBsonObjectSize doesn't match one from ismaster");
-
-})();
+/** + * This test attempts to read from the "local.startup_log" collection and assert that it has an + * entry matching the server's response from the "getCmdLineOpts" command. The former operation may + * be routed to a secondary in the replica set, whereas the latter must be routed to the primary. + * + * @tags: [assumes_read_preference_unchanged] + */ +load('jstests/aggregation/extras/utils.js'); + +(function() { + 'use strict'; + + // Check that smallArray is entirely contained by largeArray + // returns false if a member of smallArray is not in largeArray + function arrayIsSubset(smallArray, largeArray) { + for (var i = 0; i < smallArray.length; i++) { + if (!Array.contains(largeArray, smallArray[i])) { + print("Could not find " + smallArray[i] + " in largeArray"); + return false; + } + } + + return true; + } + + // Test startup_log + var stats = db.getSisterDB("local").startup_log.stats(); + assert(stats.capped); + + var latestStartUpLog = + db.getSisterDB("local").startup_log.find().sort({$natural: -1}).limit(1).next(); + var serverStatus = db._adminCommand("serverStatus"); + var cmdLine = db._adminCommand("getCmdLineOpts").parsed; + + // Test that the startup log has the expected keys + var verbose = false; + var expectedKeys = + ["_id", "hostname", "startTime", "startTimeLocal", "cmdLine", "pid", "buildinfo"]; + var keys = Object.keySet(latestStartUpLog); + assert(arrayEq(expectedKeys, keys, verbose), 'startup_log keys failed'); + + // Tests _id implicitly - should be comprised of host-timestamp + // Setup expected startTime and startTimeLocal from the supplied timestamp + var _id = latestStartUpLog._id.split('-'); // _id should consist of host-timestamp + var _idUptime = _id.pop(); + var _idHost = _id.join('-'); + var uptimeSinceEpochRounded = Math.floor(_idUptime / 1000) * 1000; + var startTime = new Date(uptimeSinceEpochRounded); // Expected startTime + + assert.eq(_idHost, latestStartUpLog.hostname, "Hostname doesn't match one from _id"); + assert.eq(serverStatus.host.split(':')[0], + latestStartUpLog.hostname, + "Hostname doesn't match one in server status"); + assert.closeWithinMS(startTime, + latestStartUpLog.startTime, + "StartTime doesn't match one from _id", + 2000); // Expect less than 2 sec delta + assert.eq(cmdLine, latestStartUpLog.cmdLine, "cmdLine doesn't match that from getCmdLineOpts"); + assert.eq(serverStatus.pid, latestStartUpLog.pid, "pid doesn't match that from serverStatus"); + + // Test buildinfo + var buildinfo = db.runCommand("buildinfo"); + delete buildinfo.ok; // Delete extra meta info not in startup_log + var isMaster = db._adminCommand("ismaster"); + + // Test buildinfo has the expected keys + var expectedKeys = [ + "version", + "gitVersion", + "allocator", + "versionArray", + "javascriptEngine", + "openssl", + "buildEnvironment", + "debug", + "maxBsonObjectSize", + "bits", + "modules" + ]; + + var keys = Object.keySet(latestStartUpLog.buildinfo); + // Disabled to check + assert(arrayIsSubset(expectedKeys, keys), + "buildinfo keys failed! \n expected:\t" + expectedKeys + "\n actual:\t" + keys); + assert.eq(buildinfo, + latestStartUpLog.buildinfo, + "buildinfo doesn't match that from buildinfo command"); + + // Test version and version Array + var version = latestStartUpLog.buildinfo.version.split('-')[0]; + var versionArray = latestStartUpLog.buildinfo.versionArray; + var versionArrayCleaned = versionArray.slice(0, 3); + if (versionArray[3] == -100) { + versionArrayCleaned[2] -= 1; + } + + assert.eq(serverStatus.version, + latestStartUpLog.buildinfo.version, + "Mongo version doesn't match that from ServerStatus"); + assert.eq( + version, versionArrayCleaned.join('.'), "version doesn't match that from the versionArray"); + var jsEngine = latestStartUpLog.buildinfo.javascriptEngine; + assert((jsEngine == "none") || jsEngine.startsWith("mozjs")); + assert.eq(isMaster.maxBsonObjectSize, + latestStartUpLog.buildinfo.maxBsonObjectSize, + "maxBsonObjectSize doesn't match one from ismaster"); + +})(); diff --git a/jstests/core/storefunc.js b/jstests/core/storefunc.js index 8598e9cc62b..15abc56421e 100644 --- a/jstests/core/storefunc.js +++ b/jstests/core/storefunc.js @@ -1,3 +1,5 @@ +// @tags: [requires_eval_command] + // Use a private sister database to avoid conflicts with other tests that use system.js var testdb = db.getSisterDB("storefunc"); var res; diff --git a/jstests/core/top.js b/jstests/core/top.js index 819b41b0981..3d98f5a7b2d 100644 --- a/jstests/core/top.js +++ b/jstests/core/top.js @@ -1,5 +1,11 @@ /** * 1. check top numbers are correct + * + * This test attempts to perform read operations and get statistics using the top command. The + * former operation may be routed to a secondary in the replica set, whereas the latter must be + * routed to the primary. + * + * @tags: [assumes_read_preference_unchanged] */ (function() { load("jstests/libs/stats.js"); diff --git a/jstests/core/update_affects_indexes.js b/jstests/core/update_affects_indexes.js new file mode 100644 index 00000000000..91db3ebe565 --- /dev/null +++ b/jstests/core/update_affects_indexes.js @@ -0,0 +1,100 @@ +// This is a regression test for SERVER-32048. It checks that index keys are correctly updated when +// an update modifier implicitly creates a new array element. +(function() { + "use strict"; + + let coll = db.update_affects_indexes; + coll.drop(); + let indexKeyPattern = {"a.b": 1}; + assert.commandWorked(coll.createIndex(indexKeyPattern)); + + // Tests that the document 'docId' has all the index keys in 'expectedKeys' and none of the + // index keys in 'unexpectedKeys'. + function assertExpectedIndexKeys(docId, expectedKeys, unexpectedKeys) { + for (let key of expectedKeys) { + let res = coll.find(docId).hint(indexKeyPattern).min(key).returnKey().toArray(); + assert.eq(1, res.length, tojson(res)); + assert.eq(key, res[0]); + } + + for (let key of unexpectedKeys) { + let res = coll.find(docId).hint(indexKeyPattern).min(key).returnKey().toArray(); + if (res.length > 0) { + assert.eq(1, res.length, tojson(res)); + assert.neq(key, res[0]); + } + } + } + + // $set implicitly creates array element at end of array. + assert.writeOK(coll.insert({_id: 0, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 0}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 0}, {$set: {"a.1.c": 0}})); + assertExpectedIndexKeys({_id: 0}, [{"a.b": 0}, {"a.b": null}], []); + + // $set implicitly creates array element beyond end of array. + assert.writeOK(coll.insert({_id: 1, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 1}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 1}, {$set: {"a.3.c": 0}})); + assertExpectedIndexKeys({_id: 1}, [{"a.b": 0}, {"a.b": null}], []); + + // $set implicitly creates array element in empty array (no index key changes needed). + assert.writeOK(coll.insert({_id: 2, a: []})); + assertExpectedIndexKeys({_id: 2}, [{"a.b": null}], []); + assert.writeOK(coll.update({_id: 2}, {$set: {"a.0.c": 0}})); + assertExpectedIndexKeys({_id: 2}, [{"a.b": null}], []); + + // $inc implicitly creates array element at end of array. + assert.writeOK(coll.insert({_id: 3, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 3}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 3}, {$inc: {"a.1.c": 0}})); + assertExpectedIndexKeys({_id: 3}, [{"a.b": 0}, {"a.b": null}], []); + + // $mul implicitly creates array element at end of array. + assert.writeOK(coll.insert({_id: 4, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 4}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 4}, {$mul: {"a.1.c": 0}})); + assertExpectedIndexKeys({_id: 4}, [{"a.b": 0}, {"a.b": null}], []); + + // $addToSet implicitly creates array element at end of array. + assert.writeOK(coll.insert({_id: 5, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 5}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 5}, {$addToSet: {"a.1.c": 0}})); + assertExpectedIndexKeys({_id: 5}, [{"a.b": 0}, {"a.b": null}], []); + + // $bit implicitly creates array element at end of array. + assert.writeOK(coll.insert({_id: 6, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 6}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 6}, {$bit: {"a.1.c": {and: NumberInt(1)}}})); + assertExpectedIndexKeys({_id: 6}, [{"a.b": 0}, {"a.b": null}], []); + + // $min implicitly creates array element at end of array. + assert.writeOK(coll.insert({_id: 7, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 7}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 7}, {$min: {"a.1.c": 0}})); + assertExpectedIndexKeys({_id: 7}, [{"a.b": 0}, {"a.b": null}], []); + + // $max implicitly creates array element at end of array. + assert.writeOK(coll.insert({_id: 8, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 8}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 8}, {$max: {"a.1.c": 0}})); + assertExpectedIndexKeys({_id: 8}, [{"a.b": 0}, {"a.b": null}], []); + + // $currentDate implicitly creates array element at end of array. + assert.writeOK(coll.insert({_id: 9, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 9}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 9}, {$currentDate: {"a.1.c": true}})); + assertExpectedIndexKeys({_id: 9}, [{"a.b": 0}, {"a.b": null}], []); + + // $push implicitly creates array element at end of array. + assert.writeOK(coll.insert({_id: 10, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 10}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 10}, {$push: {"a.1.c": 0}})); + assertExpectedIndexKeys({_id: 10}, [{"a.b": 0}, {"a.b": null}], []); + + // $pushAll implicitly creates array element at end of array. + assert.writeOK(coll.insert({_id: 11, a: [{b: 0}]})); + assertExpectedIndexKeys({_id: 11}, [{"a.b": 0}], [{"a.b": null}]); + assert.writeOK(coll.update({_id: 11}, {$pushAll: {"a.1.c": [0]}})); + assertExpectedIndexKeys({_id: 11}, [{"a.b": 0}, {"a.b": null}], []); +}()); diff --git a/jstests/core/update_multi5.js b/jstests/core/update_multi5.js index e610462a620..9e9550bc554 100644 --- a/jstests/core/update_multi5.js +++ b/jstests/core/update_multi5.js @@ -7,8 +7,8 @@ assert.writeOK(t.insert({path: 'r1', subscribers: [1, 2]})); assert.writeOK(t.insert({path: 'r2', subscribers: [3, 4]})); - var res = assert.writeOK(t.update( - {}, {$addToSet: {subscribers: 5}}, {upsert: false, multi: true, writeConcern: {w: 1}})); + var res = + assert.writeOK(t.update({}, {$addToSet: {subscribers: 5}}, {upsert: false, multi: true})); assert.eq(res.nMatched, 2, tojson(res)); diff --git a/jstests/core/views/invalid_system_views.js b/jstests/core/views/invalid_system_views.js index 3ba282d2ca1..49ac600ba8b 100644 --- a/jstests/core/views/invalid_system_views.js +++ b/jstests/core/views/invalid_system_views.js @@ -1,6 +1,8 @@ /** * Tests that invalid view definitions in system.views do not impact valid commands on existing * collections. + * + * @tags: [requires_collmod_command] */ (function() { "use strict"; diff --git a/jstests/core/views/views_all_commands.js b/jstests/core/views/views_all_commands.js index 0115672dcda..e1798803a1b 100644 --- a/jstests/core/views/views_all_commands.js +++ b/jstests/core/views/views_all_commands.js @@ -48,6 +48,8 @@ * * skipStandalone * If true, do not run this command on a standalone mongod. + * + * @tags: [requires_collmod_command] */ (function() { diff --git a/jstests/core/views/views_basic.js b/jstests/core/views/views_basic.js index d6032b105df..b4a2f177982 100644 --- a/jstests/core/views/views_basic.js +++ b/jstests/core/views/views_basic.js @@ -13,7 +13,7 @@ let res = viewsDB.runCommand(cmd); assert.commandWorked(res); - let cursor = new DBCommandCursor(db.getMongo(), res, 5); + let cursor = new DBCommandCursor(res._mongo, res, 5); let actual = cursor.toArray(); assert(arrayEq(actual, expected), "actual: " + tojson(cursor.toArray()) + ", expected:" + tojson(expected)); diff --git a/jstests/core/views/views_change.js b/jstests/core/views/views_change.js index 002284095c5..a62e7e7f04f 100644 --- a/jstests/core/views/views_change.js +++ b/jstests/core/views/views_change.js @@ -1,6 +1,7 @@ /** * Tests the behavior of views when the backing view or collection is changed. - * @tags: [requires_find_command] + * + * @tags: [requires_collmod_command, requires_find_command] */ (function() { "use strict"; diff --git a/jstests/core/views/views_collation.js b/jstests/core/views/views_collation.js index 9e4ed7feb30..38557358ce9 100644 --- a/jstests/core/views/views_collation.js +++ b/jstests/core/views/views_collation.js @@ -1,9 +1,13 @@ /** * Tests the behavior of operations when interacting with a view's default collation. + * + * @tags: [requires_collmod_command] */ (function() { "use strict"; + load("jstests/libs/analyze_plan.js"); + let viewsDB = db.getSiblingDB("views_collation"); assert.commandWorked(viewsDB.dropDatabase()); assert.commandWorked(viewsDB.runCommand({create: "simpleCollection"})); @@ -58,6 +62,15 @@ assert.commandWorked(viewsDB.runCommand({count: "filView"})); assert.commandWorked(viewsDB.runCommand({distinct: "filView", key: "x"})); + // Explain of operations that do not specify a collation succeed. + assert.commandWorked(viewsDB.runCommand({aggregate: "filView", pipeline: [], explain: true})); + assert.commandWorked( + viewsDB.runCommand({explain: {find: "filView"}, verbosity: "allPlansExecution"})); + assert.commandWorked( + viewsDB.runCommand({explain: {count: "filView"}, verbosity: "allPlansExecution"})); + assert.commandWorked(viewsDB.runCommand( + {explain: {distinct: "filView", key: "x"}, verbosity: "allPlansExecution"})); + // Operations with a matching collation succeed. assert.commandWorked( viewsDB.runCommand({aggregate: "filView", pipeline: [], collation: {locale: "fil"}})); @@ -66,6 +79,18 @@ assert.commandWorked( viewsDB.runCommand({distinct: "filView", key: "x", collation: {locale: "fil"}})); + // Explain of operations with a matching collation succeed. + assert.commandWorked(viewsDB.runCommand( + {aggregate: "filView", pipeline: [], explain: true, collation: {locale: "fil"}})); + assert.commandWorked(viewsDB.runCommand( + {explain: {find: "filView", collation: {locale: "fil"}}, verbosity: "allPlansExecution"})); + assert.commandWorked(viewsDB.runCommand( + {explain: {count: "filView", collation: {locale: "fil"}}, verbosity: "allPlansExecution"})); + assert.commandWorked(viewsDB.runCommand({ + explain: {distinct: "filView", key: "x", collation: {locale: "fil"}}, + verbosity: "allPlansExecution" + })); + // Attempting to override the non-simple default collation of a view fails. assert.commandFailedWithCode( viewsDB.runCommand({aggregate: "filView", pipeline: [], collation: {locale: "en"}}), @@ -90,6 +115,46 @@ viewsDB.runCommand({distinct: "filView", key: "x", collation: {locale: "simple"}}), ErrorCodes.OptionNotSupportedOnView); + // Attempting to override the default collation of a view with explain fails. + assert.commandFailedWithCode( + viewsDB.runCommand( + {aggregate: "filView", pipeline: [], explain: true, collation: {locale: "en"}}), + ErrorCodes.OptionNotSupportedOnView); + assert.commandFailedWithCode( + viewsDB.runCommand( + {aggregate: "filView", pipeline: [], explain: true, collation: {locale: "simple"}}), + ErrorCodes.OptionNotSupportedOnView); + assert.commandFailedWithCode(viewsDB.runCommand({ + explain: {find: "filView", collation: {locale: "fr"}}, + verbosity: "allPlansExecution" + }), + ErrorCodes.OptionNotSupportedOnView); + assert.commandFailedWithCode(viewsDB.runCommand({ + explain: {find: "filView", collation: {locale: "simple"}}, + verbosity: "allPlansExecution" + }), + ErrorCodes.OptionNotSupportedOnView); + assert.commandFailedWithCode(viewsDB.runCommand({ + explain: {count: "filView", collation: {locale: "zh"}}, + verbosity: "allPlansExecution" + }), + ErrorCodes.OptionNotSupportedOnView); + assert.commandFailedWithCode(viewsDB.runCommand({ + explain: {count: "filView", collation: {locale: "simple"}}, + verbosity: "allPlansExecution" + }), + ErrorCodes.OptionNotSupportedOnView); + assert.commandFailedWithCode(viewsDB.runCommand({ + explain: {distinct: "filView", key: "x", collation: {locale: "es"}}, + verbosity: "allPlansExecution" + }), + ErrorCodes.OptionNotSupportedOnView); + assert.commandFailedWithCode(viewsDB.runCommand({ + explain: {distinct: "filView", key: "x", collation: {locale: "simple"}}, + verbosity: "allPlansExecution" + }), + ErrorCodes.OptionNotSupportedOnView); + const lookupSimpleView = { $lookup: {from: "simpleView", localField: "x", foreignField: "x", as: "result"} }; @@ -253,4 +318,56 @@ viewsDB.runCommand( {collMod: "esView", viewOn: "simpleCollection", pipeline: [graphLookupFilView]}), ErrorCodes.OptionNotSupportedOnView); + + // Make sure that when an operation does not specify the collation, it correctly uses the + // default collation associated with the view. For this, we set up a new backing collection with + // a case-insensitive view. + assert.commandWorked(viewsDB.runCommand({create: "case_sensitive_coll"})); + assert.commandWorked(viewsDB.runCommand({ + create: "case_insensitive_view", + viewOn: "case_sensitive_coll", + collation: {locale: "en", strength: 1} + })); + + assert.writeOK(viewsDB.case_sensitive_coll.insert({f: "case"})); + assert.writeOK(viewsDB.case_sensitive_coll.insert({f: "Case"})); + assert.writeOK(viewsDB.case_sensitive_coll.insert({f: "CASE"})); + + let explain, cursorStage; + + // Test that aggregate against a view with a default collation correctly uses the collation. + assert.eq(1, viewsDB.case_sensitive_coll.aggregate([{$match: {f: "case"}}]).itcount()); + assert.eq(3, viewsDB.case_insensitive_view.aggregate([{$match: {f: "case"}}]).itcount()); + explain = viewsDB.case_insensitive_view.explain().aggregate([{$match: {f: "case"}}]); + cursorStage = getAggPlanStage(explain, "$cursor"); + assert.neq(null, cursorStage, tojson(explain)); + assert.eq(1, cursorStage.$cursor.queryPlanner.collation.strength, tojson(cursorStage)); + + // Test that count against a view with a default collation correctly uses the collation. + assert.eq(1, viewsDB.case_sensitive_coll.count({f: "case"})); + assert.eq(3, viewsDB.case_insensitive_view.count({f: "case"})); + explain = viewsDB.case_insensitive_view.explain().count({f: "case"}); + cursorStage = getAggPlanStage(explain, "$cursor"); + assert.neq(null, cursorStage, tojson(explain)); + assert.eq(1, cursorStage.$cursor.queryPlanner.collation.strength, tojson(cursorStage)); + + // Test that distinct against a view with a default collation correctly uses the collation. + assert.eq(3, viewsDB.case_sensitive_coll.distinct("f").length); + assert.eq(1, viewsDB.case_insensitive_view.distinct("f").length); + explain = viewsDB.case_insensitive_view.explain().distinct("f"); + cursorStage = getAggPlanStage(explain, "$cursor"); + assert.neq(null, cursorStage, tojson(explain)); + assert.eq(1, cursorStage.$cursor.queryPlanner.collation.strength, tojson(cursorStage)); + + // Test that find against a view with a default collation correctly uses the collation. + let findRes = viewsDB.runCommand({find: "case_sensitive_coll", filter: {f: "case"}}); + assert.commandWorked(findRes); + assert.eq(1, findRes.cursor.firstBatch.length); + findRes = viewsDB.runCommand({find: "case_insensitive_view", filter: {f: "case"}}); + assert.commandWorked(findRes); + assert.eq(3, findRes.cursor.firstBatch.length); + explain = viewsDB.runCommand({explain: {find: "case_insensitive_view", filter: {f: "case"}}}); + cursorStage = getAggPlanStage(explain, "$cursor"); + assert.neq(null, cursorStage, tojson(explain)); + assert.eq(1, cursorStage.$cursor.queryPlanner.collation.strength, tojson(cursorStage)); }()); diff --git a/jstests/core/views/views_find.js b/jstests/core/views/views_find.js index c196e980ce5..53c9d4e8b98 100644 --- a/jstests/core/views/views_find.js +++ b/jstests/core/views/views_find.js @@ -15,7 +15,7 @@ let assertFindResultEq = function(cmd, expected, ordered) { let res = viewsDB.runCommand(cmd); assert.commandWorked(res); - let arr = new DBCommandCursor(db.getMongo(), res, 5).toArray(); + let arr = new DBCommandCursor(res._mongo, res, 5).toArray(); let errmsg = tojson({expected: expected, got: arr}); if (typeof(ordered) === "undefined" || !ordered) diff --git a/jstests/core/views/views_rename.js b/jstests/core/views/views_rename.js new file mode 100644 index 00000000000..ad656671ea1 --- /dev/null +++ b/jstests/core/views/views_rename.js @@ -0,0 +1,17 @@ +(function() { + // SERVER-30406 Test that renaming system.views correctly invalidates the view catalog + 'use strict'; + db.view.drop(); + db.coll.drop(); + assert.commandWorked(db.createView("view", "coll", [])); + assert.writeOK(db.coll.insert({_id: 1})); + assert.eq(db.view.find().count(), 1, "couldn't find document in view"); + assert.commandWorked(db.system.views.renameCollection("views", /*dropTarget*/ true)); + assert.eq(db.view.find().count(), + 0, + "find on view should have returned no results after renaming away system.views"); + assert.commandWorked(db.views.renameCollection("system.views")); + assert.eq(db.view.find().count(), + 1, + "find on view should have worked again after renaming system.views back in place"); +})(); diff --git a/jstests/core/views/views_stats.js b/jstests/core/views/views_stats.js index 75feb857c9a..22261e9fa81 100644 --- a/jstests/core/views/views_stats.js +++ b/jstests/core/views/views_stats.js @@ -1,4 +1,10 @@ // Test that top and latency histogram statistics are recorded for views. +// +// This test attempts to perform write operations and get latency statistics using the $collStats +// stage. The former operation must be routed to the primary in a replica set, whereas the latter +// may be routed to a secondary. +// +// @tags: [assumes_read_preference_unchanged] (function() { "use strict"; diff --git a/jstests/core/views/views_validation.js b/jstests/core/views/views_validation.js index 84c7f1d3510..b32750a082f 100644 --- a/jstests/core/views/views_validation.js +++ b/jstests/core/views/views_validation.js @@ -1,3 +1,4 @@ +// @tags: [requires_collmod_command] (function() { "use strict"; let viewsDb = db.getSiblingDB("views_validation"); diff --git a/jstests/core/write_result.js b/jstests/core/write_result.js index 86486089c68..453be4ca1c9 100644 --- a/jstests/core/write_result.js +++ b/jstests/core/write_result.js @@ -1,6 +1,8 @@ // // Tests the behavior of single writes using write commands // +// @tags: [assumes_write_concern_unchanged] +// var coll = db.write_result; coll.drop(); diff --git a/jstests/hooks/validate_collections.js b/jstests/hooks/validate_collections.js index aeb38a98bf5..e64bb39d305 100644 --- a/jstests/hooks/validate_collections.js +++ b/jstests/hooks/validate_collections.js @@ -71,6 +71,21 @@ function validateCollections(db, obj) { filter = {$or: [filter, {type: {$exists: false}}]}; } + // Optionally skip collections. + if (Array.isArray(jsTest.options().skipValidationNamespaces) && + jsTest.options().skipValidationNamespaces.length > 0) { + let skippedCollections = []; + for (let ns of jsTest.options().skipValidationNamespaces) { + // Strip off the database name from 'ns' to extract the collName. + const collName = ns.replace(new RegExp('^' + db.getName() + '\.'), ''); + // Skip the collection 'collName' if the db name was removed from 'ns'. + if (collName !== ns) { + skippedCollections.push({name: {$ne: collName}}); + } + } + filter = {$and: [filter, ...skippedCollections]}; + } + let collInfo = db.getCollectionInfos(filter); for (var collDocument of collInfo) { var coll = db.getCollection(collDocument["name"]); diff --git a/jstests/libs/analyze_plan.js b/jstests/libs/analyze_plan.js index 62511ae1fac..5e52ad6aba3 100644 --- a/jstests/libs/analyze_plan.js +++ b/jstests/libs/analyze_plan.js @@ -115,3 +115,74 @@ function getChunkSkips(root) { return 0; } + +/** + * Given the root stage of agg explain's JSON representation of a query plan ('root'), returns all + * subdocuments whose stage is 'stage'. This can either be an agg stage name like "$cursor" or + * "$sort", or a query stage name like "IXSCAN" or "SORT". + * + * Returns an empty array if the plan does not have the requested stage. Asserts that agg explain + * structure matches expected format. + */ +function getAggPlanStages(root, stage) { + let results = []; + + function getDocumentSources(docSourceArray) { + let results = []; + for (let i = 0; i < docSourceArray.length; i++) { + let properties = Object.getOwnPropertyNames(docSourceArray[i]); + assert.eq(1, properties.length); + if (properties[0] === stage) { + results.push(docSourceArray[i]); + } + } + return results; + } + + if (root.hasOwnProperty("stages")) { + assert(root.stages.constructor === Array); + + results = results.concat(getDocumentSources(root.stages)); + + assert(root.stages[0].hasOwnProperty("$cursor")); + assert(root.stages[0].$cursor.hasOwnProperty("queryPlanner")); + assert(root.stages[0].$cursor.queryPlanner.hasOwnProperty("winningPlan")); + results = + results.concat(getPlanStages(root.stages[0].$cursor.queryPlanner.winningPlan, stage)); + } + + if (root.hasOwnProperty("shards")) { + for (let elem in root.shards) { + assert(root.shards[elem].stages.constructor === Array); + + results = results.concat(getDocumentSources(root.shards[elem].stages)); + + assert(root.shards[elem].stages[0].hasOwnProperty("$cursor")); + assert(root.shards[elem].stages[0].$cursor.hasOwnProperty("queryPlanner")); + assert(root.shards[elem].stages[0].$cursor.queryPlanner.hasOwnProperty("winningPlan")); + results = results.concat( + getPlanStages(root.shards[elem].stages[0].$cursor.queryPlanner.winningPlan, stage)); + } + } + + return results; +} + +/** + * Given the root stage of agg explain's JSON representation of a query plan ('root'), returns the + * subdocument with its stage as 'stage'. Returns null if the plan does not have such a stage. + * Asserts that no more than one stage is a match. + */ +function getAggPlanStage(root, stage) { + let planStageList = getAggPlanStages(root, stage); + + if (planStageList.length === 0) { + return null; + } else { + assert.eq(1, + planStageList.length, + "getAggPlanStage expects to find 0 or 1 matching stages. planStageList: " + + tojson(planStageList)); + return planStageList[0]; + } +} diff --git a/jstests/libs/ftdc.js b/jstests/libs/ftdc.js new file mode 100644 index 00000000000..7d327d39852 --- /dev/null +++ b/jstests/libs/ftdc.js @@ -0,0 +1,102 @@ +/** + * Utility test functions for FTDC + */ +'use strict'; + +/** + * Verify that getDiagnosticData is working correctly. + */ +function verifyGetDiagnosticData(adminDb) { + // We need to retry a few times if run this test immediately after mongod is started as FTDC may + // not have run yet. + var foundGoodDocument = false; + + for (var i = 0; i < 60 && foundGoodDocument == false; ++i) { + var result = adminDb.runCommand("getDiagnosticData"); + assert.commandWorked(result); + + var data = result.data; + + if (!data.hasOwnProperty("start")) { + // Wait a little longer for FTDC to start + jsTestLog("Running getDiagnosticData: " + tojson(result)); + + sleep(500); + } else { + // Check for a few common properties to ensure we got data + assert(data.hasOwnProperty("serverStatus"), + "does not have 'serverStatus' in '" + tojson(data) + "'"); + assert(data.hasOwnProperty("end"), "does not have 'end' in '" + tojson(data) + "'"); + foundGoodDocument = true; + + jsTestLog("Got good getDiagnosticData: " + tojson(result)); + } + } + + assert(foundGoodDocument, + "getDiagnosticData failed to return a non-empty command, is FTDC running?"); +} + +/** + * Validate all the common FTDC parameters are set correctly and can be manipulated. + */ +function verifyCommonFTDCParameters(adminDb, isEnabled) { + // Are we running against MongoS? + var isMongos = ("isdbgrid" == adminDb.runCommand("ismaster").msg); + + // Check the defaults are correct + // + function getparam(field) { + var q = {getParameter: 1}; + q[field] = 1; + + var ret = adminDb.runCommand(q); + return ret[field]; + } + + // Verify the defaults are as we documented them + assert.eq(getparam("diagnosticDataCollectionEnabled"), isEnabled); + assert.eq(getparam("diagnosticDataCollectionPeriodMillis"), 1000); + assert.eq(getparam("diagnosticDataCollectionDirectorySizeMB"), 200); + assert.eq(getparam("diagnosticDataCollectionFileSizeMB"), 10); + assert.eq(getparam("diagnosticDataCollectionSamplesPerChunk"), 300); + assert.eq(getparam("diagnosticDataCollectionSamplesPerInterimUpdate"), 10); + + function setparam(obj) { + var ret = adminDb.runCommand(Object.extend({setParameter: 1}, obj)); + return ret; + } + + if (!isMongos) { + // The MongoS specific behavior for diagnosticDataCollectionEnabled is tested in + // ftdc_setdirectory.js. + assert.commandWorked(setparam({"diagnosticDataCollectionEnabled": 1})); + } + assert.commandWorked(setparam({"diagnosticDataCollectionPeriodMillis": 100})); + assert.commandWorked(setparam({"diagnosticDataCollectionDirectorySizeMB": 10})); + assert.commandWorked(setparam({"diagnosticDataCollectionFileSizeMB": 1})); + assert.commandWorked(setparam({"diagnosticDataCollectionSamplesPerChunk": 2})); + assert.commandWorked(setparam({"diagnosticDataCollectionSamplesPerInterimUpdate": 2})); + + // Negative tests - set values below minimums + assert.commandFailed(setparam({"diagnosticDataCollectionPeriodMillis": 1})); + assert.commandFailed(setparam({"diagnosticDataCollectionDirectorySizeMB": 1})); + assert.commandFailed(setparam({"diagnosticDataCollectionSamplesPerChunk": 1})); + assert.commandFailed(setparam({"diagnosticDataCollectionSamplesPerInterimUpdate": 1})); + + // Negative test - set file size bigger then directory size + assert.commandWorked(setparam({"diagnosticDataCollectionDirectorySizeMB": 10})); + assert.commandFailed(setparam({"diagnosticDataCollectionFileSizeMB": 100})); + + // Negative test - set directory size less then file size + assert.commandWorked(setparam({"diagnosticDataCollectionDirectorySizeMB": 100})); + assert.commandWorked(setparam({"diagnosticDataCollectionFileSizeMB": 50})); + assert.commandFailed(setparam({"diagnosticDataCollectionDirectorySizeMB": 10})); + + // Reset + assert.commandWorked(setparam({"diagnosticDataCollectionFileSizeMB": 10})); + assert.commandWorked(setparam({"diagnosticDataCollectionDirectorySizeMB": 200})); + assert.commandWorked(setparam({"diagnosticDataCollectionPeriodMillis": 1000})); + assert.commandWorked(setparam({"diagnosticDataCollectionSamplesPerChunk": 300})); + assert.commandWorked(setparam({"diagnosticDataCollectionSamplesPerInterimUpdate": 10})); +}
\ No newline at end of file diff --git a/jstests/libs/override_methods/override_helpers.js b/jstests/libs/override_methods/override_helpers.js new file mode 100644 index 00000000000..12bd4b54739 --- /dev/null +++ b/jstests/libs/override_methods/override_helpers.js @@ -0,0 +1,117 @@ +/** + * The OverrideHelpers object defines convenience methods for overriding commands and functions in + * the mongo shell. + */ +var OverrideHelpers = (function() { + "use strict"; + + function isAggregationWithOutStage(commandName, commandObj) { + if (commandName !== "aggregate" || typeof commandObj !== "object" || commandObj === null) { + return false; + } + + if (!Array.isArray(commandObj.pipeline) || commandObj.pipeline.length === 0) { + return false; + } + + const lastStage = commandObj.pipeline[commandObj.pipeline.length - 1]; + if (typeof lastStage !== "object" || lastStage === null) { + return false; + } + + return Object.keys(lastStage)[0] === "$out"; + } + + function isMapReduceWithInlineOutput(commandName, commandObj) { + if ((commandName !== "mapReduce" && commandName !== "mapreduce") || + typeof commandObj !== "object" || commandObj === null) { + return false; + } + + if (typeof commandObj.out !== "object") { + return false; + } + + return commandObj.out.hasOwnProperty("inline"); + } + + function prependOverrideInParallelShell(overrideFile) { + const startParallelShellOriginal = startParallelShell; + + startParallelShell = function(jsCode, port, noConnect) { + let newCode; + if (typeof jsCode === "function") { + // Load the override file and immediately invoke the supplied function. + newCode = `load("${overrideFile}"); (${jsCode})();`; + } else { + newCode = `load("${overrideFile}"); ${jsCode};`; + } + + return startParallelShellOriginal(newCode, port, noConnect); + }; + } + + function overrideRunCommand(overrideFunc) { + const DBQueryOriginal = DBQuery; + const mongoRunCommandOriginal = Mongo.prototype.runCommand; + const mongoRunCommandWithMetadataOriginal = Mongo.prototype.runCommandWithMetadata; + + DBQuery = function( + mongo, db, collection, ns, query, fields, limit, skip, batchSize, options) { + // If the query isn't being run against the "$cmd" or "$cmd.sys" namespaces, then it + // represents an OP_QUERY find on that collection. We skip calling overrideFunc() in + // this case because the operation doesn't represent a command. + if (!(collection instanceof DBCollection && + (collection.getName() === "$cmd" || collection.getName().startsWith("$cmd.")))) { + return DBQueryOriginal.apply(this, arguments); + } + + // Due to the function signatures of Mongo.prototype.runCommand() and + // Mongo.prototype.runCommandWithMetadata(), the overrideFunc() function expects that + // the Mongo connection object is passed as the first argument and also represents the + // 'this' parameter. As a workaround, we bind the appropriate 'this' value to the + // DBQueryOriginal constructor ahead of time. + const commandName = Object.keys(query)[0]; + return overrideFunc( + mongo, + db.getName(), + commandName, + query, + DBQueryOriginal.bind(this), + (query) => + [mongo, db, collection, ns, query, fields, limit, skip, batchSize, options]); + }; + + // Copy any properties (e.g. DBQuery.Option) that are set on DBQueryOriginal. + Object.keys(DBQueryOriginal).forEach(function(key) { + DBQuery[key] = DBQueryOriginal[key]; + }); + + Mongo.prototype.runCommand = function(dbName, commandObj, options) { + const commandName = Object.keys(commandObj)[0]; + return overrideFunc(this, + dbName, + commandName, + commandObj, + mongoRunCommandOriginal, + (commandObj) => [dbName, commandObj, options]); + }; + + Mongo.prototype.runCommandWithMetadata = function(dbName, metadata, commandArgs) { + const commandName = Object.keys(commandArgs)[0]; + return overrideFunc(this, + dbName, + commandName, + commandArgs, + mongoRunCommandWithMetadataOriginal, + (commandArgs) => [dbName, metadata, commandArgs]); + }; + } + + return { + isAggregationWithOutStage: isAggregationWithOutStage, + isMapReduceWithInlineOutput: isMapReduceWithInlineOutput, + prependOverrideInParallelShell: prependOverrideInParallelShell, + overrideRunCommand: overrideRunCommand, + }; +})(); diff --git a/jstests/libs/override_methods/set_majority_read_and_write_concerns.js b/jstests/libs/override_methods/set_majority_read_and_write_concerns.js deleted file mode 100644 index d3bb4449ed4..00000000000 --- a/jstests/libs/override_methods/set_majority_read_and_write_concerns.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Use prototype overrides to set a read concern of "majority" and a write concern of "majority" - * while running core tests. - */ -(function() { - "use strict"; - var defaultWriteConcern = { - w: "majority", - // Use a "signature" value that won't typically match a value assigned in normal use. - wtimeout: 60321 - }; - var defaultReadConcern = {level: "majority"}; - - var originalDBQuery = DBQuery; - - DBQuery = function(mongo, db, collection, ns, query, fields, limit, skip, batchSize, options) { - if (ns.endsWith("$cmd")) { - if (query.hasOwnProperty("writeConcern") && - bsonWoCompare(query.writeConcern, defaultWriteConcern) !== 0) { - jsTestLog("Warning: DBQuery overriding existing writeConcern of: " + - tojson(query.writeConcern)); - query.writeConcern = defaultWriteConcern; - } - } - - return originalDBQuery.apply(this, arguments); - }; - - DBQuery.Option = originalDBQuery.Option; - - var originalStartParallelShell = startParallelShell; - startParallelShell = function(jsCode, port, noConnect) { - var newCode; - var overridesFile = "jstests/libs/override_methods/set_majority_read_and_write_concerns.js"; - - if (typeof(jsCode) === "function") { - // Load the override file and immediately invoke the supplied function. - newCode = `load("${overridesFile}"); (${jsCode})();`; - } else { - newCode = `load("${overridesFile}"); ${jsCode};`; - } - - return originalStartParallelShell(newCode, port, noConnect); - }; - - DB.prototype._runCommandImpl = function(dbName, obj, options) { - var cmdName = ""; - for (var fieldName in obj) { - cmdName = fieldName; - break; - } - - // These commands directly support a writeConcern argument. - var commandsToForceWriteConcern = [ - "_mergeAuthzCollections", - "appendOplogNote", - "applyOps", - "authSchemaUpgrade", - "captrunc", - "cleanupOrphaned", - "clone", - "cloneCollection", - "cloneCollectionAsCapped", - // "collMod", SERVER-25196 - not supported - "convertToCapped", - "copydb", - "create", - "createIndexes", - "createRole", - "createUser", - "delete", - "drop", - "dropDatabase", - "dropAllRolesFromDatabase", - "dropAllUsersFromDatabase", - "dropDatabase", - "dropIndexes", - "dropRole", - "dropUser", - "emptycapped", - "findAndModify", - "findandmodify", - "godinsert", - "grantPrivilegesToRole", - "grantRolesToRole", - "grantRolesToUser", - "insert", - "mapReduceFinish", - "mergeAuthzCollections", - "moveChunk", - "movePrimary", - "reIndex", - "remove", - "renameCollection", - "resvChunkStart", - "revokePriviligesFromRole", - "revokeRolesFromRole", - "revokeRolesFromUser", - "update", - "updateRole", - "updateUser", - ]; - - // These are reading commands that support majority readConcern. - var commandsToForceReadConcern = [ - "count", - "distinct", - "find", - "geoNear", - "geoSearch", - "group", - ]; - - var forceWriteConcern = Array.contains(commandsToForceWriteConcern, cmdName); - var forceReadConcern = Array.contains(commandsToForceReadConcern, cmdName); - - if (cmdName === "aggregate") { - // Aggregate can be either a read or a write depending on whether it has a $out stage. - // $out is required to be the last stage of the pipeline. - var stages = obj.pipeline; - const lastStage = stages && Array.isArray(stages) && (stages.length !== 0) - ? stages[stages.length - 1] - : undefined; - const hasOut = - lastStage && (typeof lastStage === 'object') && lastStage.hasOwnProperty('$out'); - if (hasOut) { - forceWriteConcern = true; - } else { - forceReadConcern = true; - } - } - - else if (cmdName === "mapReduce") { - var stages = obj.pipeline; - const lastStage = stages && Array.isArray(stages) && (stages.length !== 0) - ? stages[stages.length - 1] - : undefined; - const hasOut = - lastStage && (typeof lastStage === 'object') && lastStage.hasOwnProperty('$out'); - if (hasOut) { - forceWriteConcern = true; - } - } - - if (forceWriteConcern) { - if (obj.hasOwnProperty("writeConcern")) { - if (bsonWoCompare(obj.writeConcern, defaultWriteConcern) !== 0) { - jsTestLog("Warning: _runCommandImpl overriding existing writeConcern of: " + - tojson(obj.writeConcern)); - obj.writeConcern = defaultWriteConcern; - } - } else { - obj.writeConcern = defaultWriteConcern; - } - - } else if (forceReadConcern) { - if (obj.hasOwnProperty("readConcern")) { - if (bsonWoCompare(obj.readConcern, defaultReadConcern) !== 0) { - jsTestLog("Warning: _runCommandImpl overriding existing readConcern of: " + - tojson(obj.readConcern)); - obj.readConcern = defaultReadConcern; - } - } else { - obj.readConcern = defaultReadConcern; - } - } - - var res = this.getMongo().runCommand(dbName, obj, options); - - return res; - }; - - // Use a majority write concern if the operation does not specify one. - DBCollection.prototype.getWriteConcern = function() { - return new WriteConcern(defaultWriteConcern); - }; - -})(); diff --git a/jstests/libs/override_methods/set_read_and_write_concerns.js b/jstests/libs/override_methods/set_read_and_write_concerns.js new file mode 100644 index 00000000000..f371644c8f7 --- /dev/null +++ b/jstests/libs/override_methods/set_read_and_write_concerns.js @@ -0,0 +1,216 @@ +/** + * Use prototype overrides to set read concern and write concern while running tests. + */ +(function() { + "use strict"; + + load("jstests/libs/override_methods/override_helpers.js"); + + if (typeof TestData === "undefined" || !TestData.hasOwnProperty("defaultReadConcernLevel")) { + throw new Error( + "The readConcern level to use must be set as the 'defaultReadConcernLevel'" + + " property on the global TestData object"); + } + + const kDefaultReadConcern = {level: TestData.defaultReadConcernLevel}; + const kDefaultWriteConcern = + (TestData.hasOwnProperty("defaultWriteConcern")) ? TestData.defaultWriteConcern : { + w: "majority", + // Use a "signature" value that won't typically match a value assigned in normal use. + // This way the wtimeout set by this override is distinguishable in the server logs. + wtimeout: 5 * 60 * 1000 + 321, // 300321ms + }; + + const kCommandsSupportingReadConcern = new Set([ + "aggregate", + "count", + "distinct", + "find", + "geoNear", + "geoSearch", + "group", + "parallelCollectionScan", + ]); + + const kCommandsSupportingWriteConcern = new Set([ + "_configsvrAddShard", + "_configsvrAddShardToZone", + "_configsvrCommitChunkMerge", + "_configsvrCommitChunkMigration", + "_configsvrCommitChunkSplit", + "_configsvrCreateDatabase", + "_configsvrEnableSharding", + "_configsvrMoveChunk", + "_configsvrMovePrimary", + "_configsvrRemoveShard", + "_configsvrRemoveShardFromZone", + "_configsvrShardCollection", + "_configsvrUpdateZoneKeyRange", + "_mergeAuthzCollections", + "_recvChunkStart", + "appendOplogNote", + "applyOps", + "authSchemaUpgrade", + "aggregate", + "captrunc", + "cleanupOrphaned", + "clone", + "cloneCollection", + "cloneCollectionAsCapped", + // "collMod", SERVER-25196 - not supported + "convertToCapped", + "copydb", + "create", + "createIndexes", + "createRole", + "createUser", + "delete", + "deleteIndexes", + "drop", + "dropAllRolesFromDatabase", + "dropAllUsersFromDatabase", + "dropDatabase", + "dropIndexes", + "dropRole", + "dropUser", + "emptycapped", + "findAndModify", + "findandmodify", + "godinsert", + "grantPrivilegesToRole", + "grantRolesToRole", + "grantRolesToUser", + "insert", + "mapReduce", + "mapreduce", + "mapreduce.shardedfinish", + "moveChunk", + "renameCollection", + "revokePrivilegesFromRole", + "revokeRolesFromRole", + "revokeRolesFromUser", + "setFeatureCompatibilityVersion", + "update", + "updateRole", + "updateUser", + ]); + + function runCommandWithReadAndWriteConcerns( + conn, dbName, commandName, commandObj, func, makeFuncArgs) { + if (typeof commandObj !== "object" || commandObj === null) { + return func.apply(conn, makeFuncArgs(commandObj)); + } + + // If the command is in a wrapped form, then we look for the actual command object inside + // the query/$query object. + let commandObjUnwrapped = commandObj; + if (commandName === "query" || commandName === "$query") { + commandObjUnwrapped = commandObj[commandName]; + commandName = Object.keys(commandObjUnwrapped)[0]; + } + + if (commandName === "collMod" || commandName === "eval" || commandName === "$eval") { + throw new Error("Cowardly refusing to run test with overridden write concern when it" + + " uses a command that can only perform w=1 writes: " + + tojson(commandObj)); + } + + let shouldForceReadConcern = kCommandsSupportingReadConcern.has(commandName); + let shouldForceWriteConcern = kCommandsSupportingWriteConcern.has(commandName); + + if (commandName === "aggregate") { + if (OverrideHelpers.isAggregationWithOutStage(commandName, commandObjUnwrapped)) { + // The $out stage can only be used with readConcern={level: "local"}. + shouldForceReadConcern = false; + } else { + // A writeConcern can only be used with a $out stage. + shouldForceWriteConcern = false; + } + + if (commandObjUnwrapped.explain) { + // Attempting to specify a readConcern while explaining an aggregation would always + // return an error prior to SERVER-30582 and it otherwise only compatible with + // readConcern={level: "local"}. + shouldForceReadConcern = false; + } + } else if (OverrideHelpers.isMapReduceWithInlineOutput(commandName, commandObjUnwrapped)) { + // A writeConcern can only be used with non-inline output. + shouldForceWriteConcern = false; + } else if (commandObj[commandName] === "system.profile") { + // Writes to the "system.profile" collection aren't guaranteed to be visible in the same + // majority-committed snapshot as the command they originated from. We don't override + // the readConcern for operations on the "system.profile" collection so that tests which + // assert on its contents continue to succeed. + shouldForceReadConcern = false; + } + + const inWrappedForm = commandObj !== commandObjUnwrapped; + + if (shouldForceReadConcern) { + // We create a copy of 'commandObj' to avoid mutating the parameter the caller + // specified. + commandObj = Object.assign({}, commandObj); + if (inWrappedForm) { + commandObjUnwrapped = Object.assign({}, commandObjUnwrapped); + commandObj[Object.keys(commandObj)[0]] = commandObjUnwrapped; + } else { + commandObjUnwrapped = commandObj; + } + + if (commandObjUnwrapped.hasOwnProperty("readConcern")) { + let readConcern = commandObjUnwrapped.readConcern; + + if (typeof readConcern !== "object" || readConcern === null || + (readConcern.hasOwnProperty("level") && + bsonWoCompare({_: readConcern.level}, {_: kDefaultReadConcern.level}) !== 0)) { + throw new Error("Cowardly refusing to override read concern of command: " + + tojson(commandObj)); + } + + // We create a copy of the readConcern object to avoid mutating the parameter the + // caller specified. + readConcern = Object.assign({}, readConcern, kDefaultReadConcern); + commandObjUnwrapped.readConcern = readConcern; + } else { + commandObjUnwrapped.readConcern = kDefaultReadConcern; + } + } + + if (shouldForceWriteConcern) { + // We create a copy of 'commandObj' to avoid mutating the parameter the caller + // specified. + commandObj = Object.assign({}, commandObj); + if (inWrappedForm) { + commandObjUnwrapped = Object.assign({}, commandObjUnwrapped); + commandObj[Object.keys(commandObj)[0]] = commandObjUnwrapped; + } else { + commandObjUnwrapped = commandObj; + } + + if (commandObjUnwrapped.hasOwnProperty("writeConcern")) { + let writeConcern = commandObjUnwrapped.writeConcern; + + if (typeof writeConcern !== "object" || writeConcern === null || + (writeConcern.hasOwnProperty("w") && + bsonWoCompare({_: writeConcern.w}, {_: kDefaultWriteConcern.w}) !== 0)) { + throw new Error("Cowardly refusing to override write concern of command: " + + tojson(commandObj)); + } + + // We create a copy of the writeConcern object to avoid mutating the parameter the + // caller specified. + writeConcern = Object.assign({}, writeConcern, kDefaultWriteConcern); + commandObjUnwrapped.writeConcern = writeConcern; + } else { + commandObjUnwrapped.writeConcern = kDefaultWriteConcern; + } + } + + return func.apply(conn, makeFuncArgs(commandObj)); + } + + OverrideHelpers.prependOverrideInParallelShell( + "jstests/libs/override_methods/set_read_and_write_concerns.js"); + + OverrideHelpers.overrideRunCommand(runCommandWithReadAndWriteConcerns); +})(); diff --git a/jstests/libs/override_methods/set_read_preference_secondary.js b/jstests/libs/override_methods/set_read_preference_secondary.js new file mode 100644 index 00000000000..d1d26433c5c --- /dev/null +++ b/jstests/libs/override_methods/set_read_preference_secondary.js @@ -0,0 +1,162 @@ +/** + * Use prototype overrides to set read preference to "secondary" when running tests. + */ +(function() { + "use strict"; + + load("jstests/libs/override_methods/override_helpers.js"); + + const kReadPreferenceSecondary = {mode: "secondary"}; + const kCommandsSupportingReadPreference = new Set([ + "aggregate", + "collStats", + "count", + "dbStats", + "distinct", + "find", + "geoNear", + "geoSearch", + "group", + "mapReduce", + "mapreduce", + "parallelCollectionScan", + ]); + + // This list of cursor-generating commands is incomplete. For example, "listCollections", + // "listIndexes", "parallelCollectionScan", and "repairCursor" are all missing from this list. + // If we ever add tests that attempt to run getMore or killCursors on cursors generated from + // those commands, then we should update the contents of this list and also handle any + // differences in the server's response format. + const kCursorGeneratingCommands = new Set(["aggregate", "find"]); + + const CursorTracker = (function() { + const kNoCursor = new NumberLong(0); + + const connectionsByCursorId = {}; + + return { + getConnectionUsedForCursor: function getConnectionUsedForCursor(cursorId) { + return (cursorId instanceof NumberLong) ? connectionsByCursorId[cursorId] + : undefined; + }, + + setConnectionUsedForCursor: function setConnectionUsedForCursor(cursorId, cursorConn) { + if (cursorId instanceof NumberLong && + !bsonBinaryEqual({_: cursorId}, {_: kNoCursor})) { + connectionsByCursorId[cursorId] = cursorConn; + } + }, + }; + })(); + + function runCommandWithReadPreferenceSecondary( + conn, dbName, commandName, commandObj, func, makeFuncArgs) { + if (typeof commandObj !== "object" || commandObj === null) { + return func.apply(conn, makeFuncArgs(commandObj)); + } + + // If the command is in a wrapped form, then we look for the actual command object inside + // the query/$query object. + let commandObjUnwrapped = commandObj; + if (commandName === "query" || commandName === "$query") { + commandObjUnwrapped = commandObj[commandName]; + commandName = Object.keys(commandObjUnwrapped)[0]; + } + + if (commandObj[commandName] === "system.profile") { + throw new Error("Cowardly refusing to run test with overridden read preference" + + " when it reads from a non-replicated collection: " + + tojson(commandObj)); + } + + if (conn.isReplicaSetConnection()) { + // When a "getMore" or "killCursors" command is issued on a replica set connection, we + // attempt to automatically route the command to the server the cursor(s) were + // originally established on. This makes it possible to use the + // set_read_preference_secondary.js override without needing to update calls of + // DB#runCommand() to explicitly track the connection that was used. If the connection + // is actually a direct connection to a mongod or mongos process, or if the cursor id + // cannot be found in the CursorTracker, then we'll fall back to using DBClientRS's + // server selection and send the operation to the current primary. It is possible that + // the test is trying to exercise the behavior around when an unknown cursor id is sent + // to the server. + if (commandName === "getMore") { + const cursorId = commandObjUnwrapped[commandName]; + const cursorConn = CursorTracker.getConnectionUsedForCursor(cursorId); + if (cursorConn !== undefined) { + return func.apply(cursorConn, makeFuncArgs(commandObj)); + } + } else if (commandName === "killCursors") { + const cursorIds = commandObjUnwrapped.cursors; + if (Array.isArray(cursorIds)) { + let cursorConn; + + for (let cursorId of cursorIds) { + const otherCursorConn = CursorTracker.getConnectionUsedForCursor(cursorId); + if (cursorConn === undefined) { + cursorConn = otherCursorConn; + } else if (otherCursorConn !== undefined) { + // We set 'cursorConn' back to undefined and break out of the loop so + // that we don't attempt to automatically route the "killCursors" + // command when there are cursors from different servers. + cursorConn = undefined; + break; + } + } + + if (cursorConn !== undefined) { + return func.apply(cursorConn, makeFuncArgs(commandObj)); + } + } + } + } + + let shouldForceReadPreference = kCommandsSupportingReadPreference.has(commandName); + if (OverrideHelpers.isAggregationWithOutStage(commandName, commandObjUnwrapped)) { + // An aggregation with a $out stage must be sent to the primary. + shouldForceReadPreference = false; + } else if ((commandName === "mapReduce" || commandName === "mapreduce") && + !OverrideHelpers.isMapReduceWithInlineOutput(commandName, commandObjUnwrapped)) { + // A map-reduce operation with non-inline output must be sent to the primary. + shouldForceReadPreference = false; + } + + if (shouldForceReadPreference) { + if (commandObj === commandObjUnwrapped) { + // We wrap the command object using a "query" field rather than a "$query" field to + // match the implementation of DB.prototype._attachReadPreferenceToCommand(). + commandObj = {query: commandObj}; + } else { + // We create a copy of 'commandObj' to avoid mutating the parameter the caller + // specified. + commandObj = Object.assign({}, commandObj); + } + + if (commandObj.hasOwnProperty("$readPreference") && + !bsonBinaryEqual({_: commandObj.$readPreference}, {_: kReadPreferenceSecondary})) { + throw new Error("Cowardly refusing to override read preference of command: " + + tojson(commandObj)); + } + + commandObj.$readPreference = kReadPreferenceSecondary; + } + + const serverResponse = func.apply(conn, makeFuncArgs(commandObj)); + + if (conn.isReplicaSetConnection() && kCursorGeneratingCommands.has(commandName) && + serverResponse.ok === 1 && serverResponse.hasOwnProperty("cursor")) { + // We associate the cursor id returned by the server with the connection that was used + // to establish it so that we can attempt to automatically route subsequent "getMore" + // and "killCursors" commands. + CursorTracker.setConnectionUsedForCursor(serverResponse.cursor.id, + serverResponse._mongo); + } + + return serverResponse; + } + + OverrideHelpers.prependOverrideInParallelShell( + "jstests/libs/override_methods/set_read_preference_secondary.js"); + + OverrideHelpers.overrideRunCommand(runCommandWithReadPreferenceSecondary); +})(); diff --git a/jstests/multiVersion/initial_sync_last_stable_from_latest.js b/jstests/multiVersion/initial_sync_last_stable_from_latest.js new file mode 100644 index 00000000000..3f036c17fe9 --- /dev/null +++ b/jstests/multiVersion/initial_sync_last_stable_from_latest.js @@ -0,0 +1,15 @@ +/** + * Multiversion initial sync test. Tests that initial sync succeeds when a 'last-stable' version + * secondary syncs from a 'latest' version replica set. + */ + +'use strict'; + +load("./jstests/multiVersion/libs/initial_sync.js"); + +var testName = "multiversion_initial_sync_last_stable_from_latest"; +let replSetVersion = "latest"; +let newSecondaryVersion = "last-stable"; +let fcv = "3.2"; + +multversionInitialSyncTest(testName, replSetVersion, newSecondaryVersion, {}, fcv); diff --git a/jstests/multiVersion/initial_sync_latest_from_last_stable.js b/jstests/multiVersion/initial_sync_latest_from_last_stable.js new file mode 100644 index 00000000000..fd92b64e87a --- /dev/null +++ b/jstests/multiVersion/initial_sync_latest_from_last_stable.js @@ -0,0 +1,14 @@ +/** + * Multiversion initial sync test. Tests that initial sync succeeds when a 'latest' version + * secondary syncs from a 'last-stable' version replica set. + */ + +'use strict'; + +load("./jstests/multiVersion/libs/initial_sync.js"); + +var testName = "multiversion_initial_sync_latest_from_last_stable"; +let replSetVersion = "last-stable"; +let newSecondaryVersion = "latest"; + +multversionInitialSyncTest(testName, replSetVersion, newSecondaryVersion, {}); diff --git a/jstests/multiVersion/initialsync.js b/jstests/multiVersion/initialsync.js deleted file mode 100644 index e9a424fd05c..00000000000 --- a/jstests/multiVersion/initialsync.js +++ /dev/null @@ -1,58 +0,0 @@ -// Multiversion initial sync test. -load("./jstests/multiVersion/libs/multi_rs.js"); -load("./jstests/replsets/rslib.js"); - -var oldVersion = "last-stable"; -var newVersion = "latest"; - -var name = "multiversioninitsync"; - -var multitest = function(replSetVersion, newNodeVersion) { - var nodes = {n1: {binVersion: replSetVersion}, n2: {binVersion: replSetVersion}}; - - print("Start up a two-node " + replSetVersion + " replica set."); - var rst = new ReplSetTest({name: name, nodes: nodes}); - rst.startSet(); - var config = rst.getReplSetConfig(); - // Set protocol version to 0 for 3.2 replset. - if (replSetVersion == newVersion) { - config.protocolVersion = 0; - } - rst.initiate(config); - - // Wait for a primary node. - var primary = rst.getPrimary(); - - // Insert some data and wait for replication. - for (var i = 0; i < 25; i++) { - primary.getDB("foo").foo.insert({_id: i}); - } - rst.awaitReplication(); - - print("Bring up a new node with version " + newNodeVersion + " and add to set."); - rst.add({binVersion: newNodeVersion}); - rst.reInitiate(); - - // Wait for a primary node. - var primary = rst.getPrimary(); - var secondaries = rst.getSecondaries(); - - print("Wait for new node to be synced."); - rst.awaitReplication(); - - rst.stopSet(); -}; - -// ***************************************** -// Test A: -// "Latest" version secondary is synced from -// an old ReplSet. -// ***************************************** -multitest(oldVersion, newVersion); - -// ***************************************** -// Test B: -// Old Secondary is synced from a "latest" -// version ReplSet. -// ***************************************** -multitest(newVersion, oldVersion); diff --git a/jstests/multiVersion/libs/initial_sync.js b/jstests/multiVersion/libs/initial_sync.js new file mode 100644 index 00000000000..866f825c8b9 --- /dev/null +++ b/jstests/multiVersion/libs/initial_sync.js @@ -0,0 +1,50 @@ + +'use strict'; + +load("./jstests/multiVersion/libs/multi_rs.js"); +load("./jstests/replsets/rslib.js"); + +/** + * Test that starts up a replica set with 2 nodes of version 'replSetVersion', inserts some data, + * then adds a new node to the replica set with version 'newNodeVersion' and waits for initial sync + * to complete. If the 'fcv' argument is given, sets the feature compatibility version of the + * replica set to 'fcv' before adding the third node. + */ +var multversionInitialSyncTest = function( + name, replSetVersion, newNodeVersion, configSettings, fcv) { + + var nodes = {n1: {binVersion: replSetVersion}, n2: {binVersion: replSetVersion}}; + + jsTestLog("Starting up a two-node '" + replSetVersion + "' version replica set."); + var rst = new ReplSetTest({name: name, nodes: nodes}); + rst.startSet(); + + var conf = rst.getReplSetConfig(); + conf.settings = configSettings; + rst.initiate(conf); + + // Wait for a primary node. + var primary = rst.getPrimary(); + + // Set 'featureCompatibilityVersion' if given. + if (fcv) { + jsTestLog("Setting FCV to '" + fcv + "' on the primary."); + assert.commandWorked(primary.adminCommand({setFeatureCompatibilityVersion: fcv})); + } + + // Insert some data and wait for replication. + for (var i = 0; i < 25; i++) { + primary.getDB("foo").foo.insert({_id: i}); + } + rst.awaitReplication(); + + jsTestLog("Bringing up a new node with version '" + newNodeVersion + "' and adding to set."); + rst.add({binVersion: newNodeVersion}); + rst.reInitiate(); + + jsTestLog("Waiting for new node to be synced."); + rst.awaitReplication(); + rst.awaitSecondaryNodes(); + + rst.stopSet(); +};
\ No newline at end of file diff --git a/jstests/multiVersion/libs/multi_rs.js b/jstests/multiVersion/libs/multi_rs.js index da976c7f4dc..87d5995ef48 100644 --- a/jstests/multiVersion/libs/multi_rs.js +++ b/jstests/multiVersion/libs/multi_rs.js @@ -25,6 +25,7 @@ ReplSetTest.prototype.upgradeSet = function(options, user, pwd) { var node = nodesToUpgrade[i]; if (node == primary) { node = this.stepdown(node); + this.waitForState(node, ReplSetTest.State.SECONDARY); primary = this.getPrimary(); } diff --git a/jstests/multiVersion/set_feature_compatibility_version.js b/jstests/multiVersion/set_feature_compatibility_version.js index d5c73f26efb..73a5785aec9 100644 --- a/jstests/multiVersion/set_feature_compatibility_version.js +++ b/jstests/multiVersion/set_feature_compatibility_version.js @@ -54,6 +54,23 @@ // featureCompatibilityVersion cannot be set via setParameter. assert.commandFailed(adminDB.runCommand({setParameter: 1, featureCompatibilityVersion: "3.2"})); + // setFeatureCompatibilityVersion fails to downgrade to FCV=3.2 if the write fails. + assert.commandWorked(adminDB.runCommand({ + configureFailPoint: "failCollectionUpdates", + data: {collectionNS: "admin.system.version"}, + mode: "alwaysOn" + })); + assert.commandFailed(adminDB.runCommand({setFeatureCompatibilityVersion: "3.2"})); + res = adminDB.runCommand({getParameter: 1, featureCompatibilityVersion: 1}); + assert.commandWorked(res); + assert.eq(res.featureCompatibilityVersion, "3.4"); + assert.eq(adminDB.system.version.findOne({_id: "featureCompatibilityVersion"}).version, "3.4"); + assert.commandWorked(adminDB.runCommand({ + configureFailPoint: "failCollectionUpdates", + data: {collectionNS: "admin.system.version"}, + mode: "off" + })); + // featureCompatibilityVersion can be set to 3.2. assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: "3.2"})); res = adminDB.runCommand({getParameter: 1, featureCompatibilityVersion: 1}); @@ -70,6 +87,23 @@ "Expected index with name 'incompatible_with_version_32' to have been removed: " + tojson(allIndexes)); + // setFeatureCompatibilityVersion fails to upgrade to FCV=3.4 if the write fails. + assert.commandWorked(adminDB.runCommand({ + configureFailPoint: "failCollectionUpdates", + data: {collectionNS: "admin.system.version"}, + mode: "alwaysOn" + })); + assert.commandFailed(adminDB.runCommand({setFeatureCompatibilityVersion: "3.4"})); + res = adminDB.runCommand({getParameter: 1, featureCompatibilityVersion: 1}); + assert.commandWorked(res); + assert.eq(res.featureCompatibilityVersion, "3.2"); + assert.eq(adminDB.system.version.findOne({_id: "featureCompatibilityVersion"}).version, "3.2"); + assert.commandWorked(adminDB.runCommand({ + configureFailPoint: "failCollectionUpdates", + data: {collectionNS: "admin.system.version"}, + mode: "off" + })); + // featureCompatibilityVersion can be set to 3.4. assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: "3.4"})); res = adminDB.runCommand({getParameter: 1, featureCompatibilityVersion: 1}); diff --git a/jstests/noPassthrough/ftdc_setdirectory.js b/jstests/noPassthrough/ftdc_setdirectory.js new file mode 100644 index 00000000000..c1173055e2a --- /dev/null +++ b/jstests/noPassthrough/ftdc_setdirectory.js @@ -0,0 +1,118 @@ +/** + * Test that verifies FTDC works in mongos. + */ +load('jstests/libs/ftdc.js'); + +(function() { + 'use strict'; + let testPath1 = MongoRunner.toRealPath('ftdc_setdir1'); + let testPath2 = MongoRunner.toRealPath('ftdc_setdir2'); + let testPath3 = MongoRunner.toRealPath('ftdc_setdir3'); + let testLog3 = testPath3 + "mongos_ftdc.log"; + + // Make the log file directory for mongos. + mkdir(testPath3); + + // Startup 3 mongos: + // 1. Normal MongoS with no log file to verify FTDC can be startup at runtime with a path. + // 2. MongoS with explict diagnosticDataCollectionDirectoryPath setParameter at startup. + // 3. MongoS with log file to verify automatic FTDC path computation works. + let st = new ShardingTest({ + shards: 1, + mongos: { + s0: {verbose: 0}, + s1: {setParameter: {diagnosticDataCollectionDirectoryPath: testPath2}}, + s2: {logpath: testLog3} + } + }); + + let admin1 = st.s0.getDB('admin'); + let admin2 = st.s1.getDB('admin'); + let admin3 = st.s2.getDB('admin'); + + function setParam(admin, obj) { + var ret = admin.runCommand(Object.extend({setParameter: 1}, obj)); + return ret; + } + + function getParam(admin, field) { + var q = {getParameter: 1}; + q[field] = 1; + + var ret = admin.runCommand(q); + assert.commandWorked(ret); + return ret[field]; + } + + // Verify FTDC can be started at runtime. + function verifyFTDCDisabledOnStartup() { + jsTestLog("Running verifyFTDCDisabledOnStartup"); + verifyCommonFTDCParameters(admin1, false); + + // 1. Try to enable and fail + assert.commandFailed(setParam(admin1, {"diagnosticDataCollectionEnabled": 1})); + + // 2. Set path and succeed + assert.commandWorked( + setParam(admin1, {"diagnosticDataCollectionDirectoryPath": testPath2})); + + // 3. Set path again and fail + assert.commandFailed( + setParam(admin1, {"diagnosticDataCollectionDirectoryPath": testPath2})); + + // 4. Enable successfully + assert.commandWorked(setParam(admin1, {"diagnosticDataCollectionEnabled": 1})); + + // 5. Validate getDiagnosticData returns FTDC data now + jsTestLog("Verifying FTDC getDiagnosticData"); + verifyGetDiagnosticData(admin1); + } + + // Verify FTDC is already running if there was a path set at startup. + function verifyFTDCStartsWithPath() { + jsTestLog("Running verifyFTDCStartsWithPath"); + verifyCommonFTDCParameters(admin2, true); + + // 1. Set path fail + assert.commandFailed( + setParam(admin2, {"diagnosticDataCollectionDirectoryPath": testPath2})); + + // 2. Enable successfully + assert.commandWorked(setParam(admin2, {"diagnosticDataCollectionEnabled": 1})); + + // 3. Validate getDiagnosticData returns FTDC data now + jsTestLog("Verifying FTDC getDiagnosticData"); + verifyGetDiagnosticData(admin2); + } + + function normpath(path) { + return path.replace(/\\/g, "/"); + } + + // Verify FTDC is already running if there was a path set at startup. + function verifyFTDCStartsWithLogFile() { + jsTestLog("Running verifyFTDCStartsWithLogFile"); + verifyCommonFTDCParameters(admin3, true); + + // 1. Verify that path is computed correctly. + let computedPath = getParam(admin3, "diagnosticDataCollectionDirectoryPath"); + assert.eq(normpath(computedPath), normpath(testPath3 + "mongos_ftdc.diagnostic.data")); + + // 2. Set path fail + assert.commandFailed( + setParam(admin3, {"diagnosticDataCollectionDirectoryPath": testPath2})); + + // 3. Enable successfully + assert.commandWorked(setParam(admin3, {"diagnosticDataCollectionEnabled": 1})); + + // 4. Validate getDiagnosticData returns FTDC data now + jsTestLog("Verifying FTDC getDiagnosticData"); + verifyGetDiagnosticData(admin3); + } + + verifyFTDCDisabledOnStartup(); + verifyFTDCStartsWithPath(); + verifyFTDCStartsWithLogFile(); + + st.stop(); +})(); diff --git a/jstests/noPassthrough/geo_near_fcv32.js b/jstests/noPassthrough/geo_near_fcv32.js new file mode 100644 index 00000000000..10b331a411d --- /dev/null +++ b/jstests/noPassthrough/geo_near_fcv32.js @@ -0,0 +1,85 @@ +/** + * Confirms that $geoNear aggregation and geoNear command succeed when FCV is 3.2. + */ +(function() { + "use strict"; + + const conn = MongoRunner.runMongod({}); + assert.neq(null, conn, "mongod was unable to start up"); + + const testDB = conn.getDB("geo_near_fcv32"); + testDB.test32.drop(); + assert.commandWorked(testDB.adminCommand({setFeatureCompatibilityVersion: "3.2"})); + + // Create 2dsphere index. + assert.commandWorked(testDB.test32.createIndex({loc: "2dsphere"})); + + // Assert that $geoNear aggregate command does not fail due to collation errors when FCV is 3.2. + assert.eq(0, + testDB.test32 + .aggregate([{ + $geoNear: { + near: {type: "Point", coordinates: [1.23, 1.23]}, + distanceField: "distance", + spherical: true + } + }]) + .itcount()); + + // Assert that specifying the simple collation in $geoNear aggregate fails with FCV 3.2. + assert.throws(function() { + testDB.test32.aggregate([{ + $geoNear: { + near: {type: "Point", coordinates: [1.23, 1.23]}, + distanceField: "distance", + spherical: true, + collation: {locale: "simple"}, + } + }]); + }); + + // Assert that specifying the simple collation in geoNear command fails with FCV 3.2. + assert.commandFailed(testDB.runCommand({ + geoNear: "test32", + near: {type: "Point", coordinates: [1.23, 1.23]}, + spherical: true, + collation: {locale: "simple"} + })); + + assert.commandWorked(testDB.adminCommand({setFeatureCompatibilityVersion: "3.4"})); + + // Create collection with case sensitive collation. + assert.commandWorked( + testDB.createCollection("test34", {collation: {locale: "en_US", strength: 2}})); + assert.commandWorked(testDB.test34.createIndex({loc: "2dsphere"})); + assert.writeOK(testDB.test34.insert({loc: [1.23, 1.23], str: "A"})); + + // Assert that after upgrading FCV to 3.4 $geoNear aggregate inherits the collection's default + // collation. + assert.eq(1, + testDB.test34 + .aggregate([{ + $geoNear: { + near: {type: "Point", coordinates: [1.23, 1.23]}, + distanceField: "distance", + spherical: true, + query: {str: "a"}, + } + }]) + .itcount()); + + // Assert that the $geoNear aggregate accepts a specific collation and overrides the default + // collation in FCV 3.4. + assert.eq(1, + testDB.test34 + .aggregate([{ + $geoNear: { + near: {type: "Point", coordinates: [1.23, 1.23]}, + distanceField: "distance", + spherical: true, + query: {str: "Ã "}, + } + }], + {collation: {locale: "en_US", strength: 1}}) + .itcount()); +})(); diff --git a/jstests/noPassthrough/libs/backup_restore.js b/jstests/noPassthrough/libs/backup_restore.js index b4e02415081..e66f0f8af97 100644 --- a/jstests/noPassthrough/libs/backup_restore.js +++ b/jstests/noPassthrough/libs/backup_restore.js @@ -352,15 +352,31 @@ var BackupRestoreTest = function(options) { stopMongoProgramByPid(fsmPid); // Wait up to 5 minutes until the new hidden node is in state SECONDARY. + jsTestLog('CRUD and FSM clients stopped. Waiting for hidden node ' + hiddenHost + + ' to become SECONDARY'); rst.waitForState(hiddenNode, ReplSetTest.State.SECONDARY); // Wait for secondaries to finish catching up before shutting down. + jsTestLog( + 'Hidden node ' + hiddenHost + + ' is now SECONDARY. Waiting for CRUD and FSM operations to be applied on all nodes.'); + rst.awaitReplication(); + + jsTestLog('CRUD and FSM operations successfully applied on all nodes. ' + + 'Waiting for all nodes to agree on the primary.'); rst.awaitNodesAgreeOnPrimary(); + + jsTestLog('All nodes agree on the primary. Getting current primary.'); primary = rst.getPrimary(); - assert.writeOK(primary.getDB("test").foo.insert( - {}, {writeConcern: {w: rst.nodes.length, wtimeout: 10 * 60 * 1000}})); + + jsTestLog('Inserting single document into primary ' + primary.host + + ' with writeConcern w:' + rst.nodes.length); + var writeResult = assert.writeOK(primary.getDB("test").foo.insert( + {}, {writeConcern: {w: rst.nodes.length, wtimeout: ReplSetTest.kDefaultTimeoutMS}})); // Stop set. + jsTestLog('Insert operation successful: ' + tojson(writeResult) + + '. Stopping replica set.'); rst.stopSet(); // Cleanup the files from the test diff --git a/jstests/noPassthrough/non_atomic_apply_ops_logging.js b/jstests/noPassthrough/non_atomic_apply_ops_logging.js new file mode 100644 index 00000000000..f93e9d38189 --- /dev/null +++ b/jstests/noPassthrough/non_atomic_apply_ops_logging.js @@ -0,0 +1,62 @@ +// SERVER-28594 Ensure non-atomic ops are individually logged in applyOps +// and atomic ops are collectively logged in applyOps. +(function() { + "use strict"; + + let rst = new ReplSetTest({nodes: 1}); + rst.startSet(); + rst.initiate(); + + let primary = rst.getPrimary(); + let testDB = primary.getDB("test"); + let oplogColl = primary.getDB("local").oplog.rs; + let testCollName = "testColl"; + let rerenamedCollName = "rerenamedColl"; + + testDB.runCommand({drop: testCollName}); + testDB.runCommand({drop: rerenamedCollName}); + assert.commandWorked(testDB.runCommand({create: testCollName})); + let testColl = testDB[testCollName]; + + // Ensure atomic apply ops logging only produces one oplog entry + // per call to apply ops and does not log individual operations + // separately. + assert.commandWorked(testDB.runCommand({ + applyOps: [ + {op: "i", ns: testColl.getFullName(), o: {_id: 1, a: "foo"}}, + {op: "i", ns: testColl.getFullName(), o: {_id: 2, a: "bar"}} + ] + })); + assert.eq(oplogColl.find({"o.applyOps": {"$exists": true}}).count(), 1); + assert.eq(oplogColl.find({"op": "i"}).count(), 0); + // Ensure non-atomic apply ops logging produces an oplog entry for + // each operation in the apply ops call and no record of applyOps + // appears for these operations. + assert.commandWorked(testDB.runCommand({ + applyOps: [ + { + op: "c", + ns: "test.$cmd", + o: { + renameCollection: "test.testColl", + to: "test.renamedColl", + stayTemp: false, + dropTarget: false + } + }, + { + op: "c", + ns: "test.$cmd", + o: { + renameCollection: "test.renamedColl", + to: "test." + rerenamedCollName, + stayTemp: false, + dropTarget: false + } + } + ] + })); + assert.eq(oplogColl.find({"o.renameCollection": {"$exists": true}}).count(), 2); + assert.eq(oplogColl.find({"o.applyOps": {"$exists": true}}).count(), 1); + rst.stopSet(); +})(); diff --git a/jstests/noPassthrough/partial_unique_indexes.js b/jstests/noPassthrough/partial_unique_indexes.js new file mode 100644 index 00000000000..7c376872ade --- /dev/null +++ b/jstests/noPassthrough/partial_unique_indexes.js @@ -0,0 +1,47 @@ +/** + * SERVER-32001: Test that indexing paths for non-unique, partial, unique, partial&unique + * crud operations correctly handle WriteConflictExceptions. + */ +(function() { + "strict"; + + let conn = MongoRunner.runMongod(); + let testDB = conn.getDB("test"); + + let t = testDB.jstests_parallel_allops; + t.drop(); + + t.createIndex({x: 1, _id: 1}, {partialFilterExpression: {_id: {$lt: 500}}, unique: true}); + t.createIndex({y: -1, _id: 1}, {unique: true}); + t.createIndex({x: -1}, {partialFilterExpression: {_id: {$gte: 500}}, unique: false}); + t.createIndex({y: 1}, {unique: false}); + + let _id = {"#RAND_INT": [0, 1000]}; + let ops = [ + {op: "remove", ns: t.getFullName(), query: {_id}}, + {op: "update", ns: t.getFullName(), query: {_id}, update: {$inc: {x: 1}}, upsert: true}, + {op: "update", ns: t.getFullName(), query: {_id}, update: {$inc: {y: 1}}, upsert: true}, + ]; + + let seconds = 5; + let parallel = 5; + let host = testDB.getMongo().host; + + let benchArgs = {ops, seconds, parallel, host}; + + assert.commandWorked(testDB.adminCommand({ + configureFailPoint: 'WTWriteConflictExceptionForReads', + mode: {activationProbability: 0.01} + })); + assert.commandWorked(testDB.adminCommand( + {configureFailPoint: 'WTWriteConflictException', mode: {activationProbability: 0.01}})); + res = benchRun(benchArgs); + printjson({res}); + + assert.commandWorked( + testDB.adminCommand({configureFailPoint: 'WTWriteConflictException', mode: "off"})); + assert.commandWorked( + testDB.adminCommand({configureFailPoint: 'WTWriteConflictExceptionForReads', mode: "off"})); + res = t.validate(); + assert(res.valid, tojson(res)); +})(); diff --git a/jstests/noPassthrough/skip_sharding_configuration_checks.js b/jstests/noPassthrough/skip_sharding_configuration_checks.js new file mode 100644 index 00000000000..ff3e95440eb --- /dev/null +++ b/jstests/noPassthrough/skip_sharding_configuration_checks.js @@ -0,0 +1,53 @@ +/** + * Starts standalone RS with skipShardingConfigurationChecks. + * @tags: [requires_persistence] + */ +(function() { + 'use strict'; + + function expectState(rst, state) { + assert.soon(function() { + var status = rst.status(); + if (status.myState != state) { + print("Waiting for state " + state + " in replSetGetStatus output: " + + tojson(status)); + } + return status.myState == state; + }); + } + + let configSvr = MongoRunner.runMongod( + {configsvr: "", setParameter: 'skipShardingConfigurationChecks=true'}); + assert.eq(configSvr, null); + + let shardSvr = + MongoRunner.runMongod({shardsvr: "", setParameter: 'skipShardingConfigurationChecks=true'}); + assert.eq(shardSvr, null); + + var st = new ShardingTest({name: "skipConfig", shards: {rs0: {nodes: 1}}}); + var configRS = st.configRS; + var shardRS = st.rs0; + + st.stopAllMongos(); + shardRS.stopSet(15, true); + configRS.stopSet(undefined, true); + + jsTestLog("Restarting configRS as a standalone ReplicaSet"); + + for (let i = 0; i < configRS.nodes.length; i++) { + delete configRS.nodes[i].fullOptions.configsvr; + configRS.nodes[i].fullOptions.setParameter = 'skipShardingConfigurationChecks=true'; + } + configRS.startSet({}, true); + expectState(configRS, ReplSetTest.State.PRIMARY); + configRS.stopSet(); + + jsTestLog("Restarting shardRS as a standalone ReplicaSet"); + for (let i = 0; i < shardRS.nodes.length; i++) { + delete shardRS.nodes[i].fullOptions.shardsvr; + shardRS.nodes[i].fullOptions.setParameter = 'skipShardingConfigurationChecks=true'; + } + shardRS.startSet({}, true); + expectState(shardRS, ReplSetTest.State.PRIMARY); + shardRS.stopSet(); +})(); diff --git a/jstests/noPassthroughWithMongod/apply_ops_index_collation.js b/jstests/noPassthroughWithMongod/apply_ops_index_collation.js new file mode 100644 index 00000000000..6248bc63b0f --- /dev/null +++ b/jstests/noPassthroughWithMongod/apply_ops_index_collation.js @@ -0,0 +1,80 @@ +// Cannot implicitly shard accessed collections because of collection existing when none +// expected. +// @tags: [assumes_no_implicit_collection_creation_after_drop] + +// Tests creation of indexes using applyOps for collections with a non-simple default collation. +// Indexes created through applyOps should be built exactly according to their index spec, without +// inheriting the collection default collation, since this is how the oplog entries are replicated. +// TODO SERVER-31435: Move this test into core once applyOps with createIndexes replicates +// correctly. +(function() { + "use strict"; + + load("jstests/libs/get_index_helpers.js"); + + const coll = db.apply_ops_index_collation; + coll.drop(); + assert.commandWorked(db.createCollection(coll.getName(), {collation: {locale: "fr_CA"}})); + + // An index created using an insert-style oplog entry with a non-simple collation does not + // inherit the collection default collation. + let res = assert.commandWorked(db.adminCommand({ + applyOps: [{ + op: "i", + ns: db.system.indexes.getFullName(), + o: { + v: 2, + key: {c: 1}, + name: "c_1_en", + ns: coll.getFullName(), + collation: { + locale: "en_US", + caseLevel: false, + caseFirst: "off", + strength: 3, + numericOrdering: false, + alternate: "non-ignorable", + maxVariable: "punct", + normalization: false, + backwards: false, + version: "57.1" + } + } + }] + })); + let allIndexes = coll.getIndexes(); + let spec = GetIndexHelpers.findByName(allIndexes, "c_1_en"); + assert.neq(null, spec, "Index 'c_1_en' not found: " + tojson(allIndexes)); + assert.eq(2, spec.v, tojson(spec)); + assert.eq("en_US", spec.collation.locale, tojson(spec)); + + // An index created using an insert-style oplog entry with a simple collation does not inherit + // the collection default collation. + res = assert.commandWorked(db.adminCommand({ + applyOps: [{ + op: "i", + ns: db.system.indexes.getFullName(), + o: {v: 2, key: {c: 1}, name: "c_1", ns: coll.getFullName()} + }] + })); + allIndexes = coll.getIndexes(); + spec = GetIndexHelpers.findByName(allIndexes, "c_1"); + assert.neq(null, spec, "Index 'c_1' not found: " + tojson(allIndexes)); + assert.eq(2, spec.v, tojson(spec)); + assert(!spec.hasOwnProperty("collation"), tojson(spec)); + + // A v=1 index created using an insert-style oplog entry does not inherit the collection default + // collation. + res = assert.commandWorked(db.adminCommand({ + applyOps: [{ + op: "i", + ns: db.system.indexes.getFullName(), + o: {v: 1, key: {d: 1}, name: "d_1", ns: coll.getFullName()} + }] + })); + allIndexes = coll.getIndexes(); + spec = GetIndexHelpers.findByName(allIndexes, "d_1"); + assert.neq(null, spec, "Index 'd_1' not found: " + tojson(allIndexes)); + assert.eq(1, spec.v, tojson(spec)); + assert(!spec.hasOwnProperty("collation"), tojson(spec)); +})(); diff --git a/jstests/noPassthroughWithMongod/ftdc_params.js b/jstests/noPassthroughWithMongod/ftdc_params.js index ced6c1b5675..08714040fcb 100644 --- a/jstests/noPassthroughWithMongod/ftdc_params.js +++ b/jstests/noPassthroughWithMongod/ftdc_params.js @@ -1,58 +1,10 @@ // FTDC test cases // +load('jstests/libs/ftdc.js'); + (function() { 'use strict'; var admin = db.getSiblingDB("admin"); - // Check the defaults are correct - // - function getparam(field) { - var q = {getParameter: 1}; - q[field] = 1; - - var ret = admin.runCommand(q); - return ret[field]; - } - - // Verify the defaults are as we documented them - assert.eq(getparam("diagnosticDataCollectionEnabled"), true); - assert.eq(getparam("diagnosticDataCollectionPeriodMillis"), 1000); - assert.eq(getparam("diagnosticDataCollectionDirectorySizeMB"), 200); - assert.eq(getparam("diagnosticDataCollectionFileSizeMB"), 10); - assert.eq(getparam("diagnosticDataCollectionSamplesPerChunk"), 300); - assert.eq(getparam("diagnosticDataCollectionSamplesPerInterimUpdate"), 10); - - function setparam(obj) { - var ret = admin.runCommand(Object.extend({setParameter: 1}, obj)); - return ret; - } - - assert.commandWorked(setparam({"diagnosticDataCollectionEnabled": 1})); - assert.commandWorked(setparam({"diagnosticDataCollectionPeriodMillis": 100})); - assert.commandWorked(setparam({"diagnosticDataCollectionDirectorySizeMB": 10})); - assert.commandWorked(setparam({"diagnosticDataCollectionFileSizeMB": 1})); - assert.commandWorked(setparam({"diagnosticDataCollectionSamplesPerChunk": 2})); - assert.commandWorked(setparam({"diagnosticDataCollectionSamplesPerInterimUpdate": 2})); - - // Negative tests - set values below minimums - assert.commandFailed(setparam({"diagnosticDataCollectionPeriodMillis": 1})); - assert.commandFailed(setparam({"diagnosticDataCollectionDirectorySizeMB": 1})); - assert.commandFailed(setparam({"diagnosticDataCollectionSamplesPerChunk": 1})); - assert.commandFailed(setparam({"diagnosticDataCollectionSamplesPerInterimUpdate": 1})); - - // Negative test - set file size bigger then directory size - assert.commandWorked(setparam({"diagnosticDataCollectionDirectorySizeMB": 10})); - assert.commandFailed(setparam({"diagnosticDataCollectionFileSizeMB": 100})); - - // Negative test - set directory size less then file size - assert.commandWorked(setparam({"diagnosticDataCollectionDirectorySizeMB": 100})); - assert.commandWorked(setparam({"diagnosticDataCollectionFileSizeMB": 50})); - assert.commandFailed(setparam({"diagnosticDataCollectionDirectorySizeMB": 10})); - - // Reset - assert.commandWorked(setparam({"diagnosticDataCollectionFileSizeMB": 10})); - assert.commandWorked(setparam({"diagnosticDataCollectionDirectorySizeMB": 200})); - assert.commandWorked(setparam({"diagnosticDataCollectionPeriodMillis": 1000})); - assert.commandWorked(setparam({"diagnosticDataCollectionSamplesPerChunk": 300})); - assert.commandWorked(setparam({"diagnosticDataCollectionSamplesPerInterimUpdate": 10})); + verifyCommonFTDCParameters(admin, true); })(); diff --git a/jstests/noPassthroughWithMongod/host_connection_string_validation.js b/jstests/noPassthroughWithMongod/host_connection_string_validation.js index a252ef39230..f7bac51db88 100644 --- a/jstests/noPassthroughWithMongod/host_connection_string_validation.js +++ b/jstests/noPassthroughWithMongod/host_connection_string_validation.js @@ -89,8 +89,8 @@ print("Testing " + (isGood ? "good" : "bad") + " connection string " + i + "..."); print(" * testing " + connectionString); testHost(connectionString, isGood); - print(" * testing mongodb://" + connectionString); - testHost("mongodb://" + connectionString, isGood); + print(" * testing mongodb://" + encodeURIComponent(connectionString)); + testHost("mongodb://" + encodeURIComponent(connectionString), isGood); } var i; diff --git a/jstests/noPassthroughWithMongod/initial_sync_oplog_rollover.js b/jstests/noPassthroughWithMongod/initial_sync_oplog_rollover.js index 3a47e14447c..d186a8f59ec 100644 --- a/jstests/noPassthroughWithMongod/initial_sync_oplog_rollover.js +++ b/jstests/noPassthroughWithMongod/initial_sync_oplog_rollover.js @@ -34,7 +34,7 @@ assert.writeOK(coll.insert({a: 1})); function getFirstOplogEntry(conn) { - return conn.getDB('local').oplog.rs.find().sort({ts: 1}).limit(1)[0]; + return conn.getDB('local').oplog.rs.find().sort({$natural: 1}).limit(1)[0]; } var firstOplogEntry = getFirstOplogEntry(primary); diff --git a/jstests/noPassthroughWithMongod/replset_host_connection_validation.js b/jstests/noPassthroughWithMongod/replset_host_connection_validation.js index 7e48de363e2..e65387dd705 100644 --- a/jstests/noPassthroughWithMongod/replset_host_connection_validation.js +++ b/jstests/noPassthroughWithMongod/replset_host_connection_validation.js @@ -1,5 +1,6 @@ // Test --host with a replica set. (function() { + 'use strict'; const replSetName = 'hostTestReplSetName'; @@ -26,35 +27,56 @@ // Pass the inner test's exit code back as the outer test's exit code if (exitCode != 0) { - doassert("inner test failed with exit code " + exitcode); + doassert("inner test failed with exit code " + exitCode); } return; } - const testHost = function(host) { - const exitCode = runMongoProgram('mongo', '--eval', ';', '--host', host); - if (exitCode !== 0) { - doassert("failed to connect with `--host " + host + - "`, but expected success. Exit code: " + exitCode); + function testHost(host, uri, ok) { + const exitCode = runMongoProgram('mongo', '--eval', ';', '--host', host, uri); + if (ok) { + assert.eq(exitCode, 0, "failed to connect with `--host " + host + "`"); + } else { + assert.neq(exitCode, 0, "unexpectedly succeeded to connect with `--host " + host + "`"); } - }; - - const connStrings = [ - `localhost:${port}`, - `${replSetName}/localhost:${port}`, - `mongodb://localhost:${port}/admin?replicaSet=${replSetName}`, - `mongodb://localhost:${port}`, - ]; - - function runConnectionStringTestFor(i, connectionString) { - print("Testing connection string " + i + "..."); - print(" * testing " + connectionString); - testHost(connectionString); } - for (let i = 0; i < connStrings.length; ++i) { - runConnectionStringTestFor(i, connStrings[i]); + function runConnectionStringTestFor(connectionString, uri, ok) { + print("* Testing: --host " + connectionString + " " + uri); + if (!ok) { + print(" This should fail"); + } + testHost(connectionString, uri, ok); + } + + function expSuccess(str) { + runConnectionStringTestFor(str, '', true); + if (!str.startsWith('mongodb://')) { + runConnectionStringTestFor(str, 'dbname', true); + } } + function expFailure(str) { + runConnectionStringTestFor(str, '', false); + } + + expSuccess(`localhost:${port}`); + expSuccess(`${replSetName}/localhost:${port}`); + expSuccess(`${replSetName}/localhost:${port},[::1]:${port}`); + expSuccess(`${replSetName}/localhost:${port},`); + expSuccess(`${replSetName}/localhost:${port},,`); + expSuccess(`mongodb://localhost:${port}/admin?replicaSet=${replSetName}`); + expSuccess(`mongodb://localhost:${port}`); + + expFailure(','); + expFailure(',,'); + expFailure(`${replSetName}/`); + expFailure(`${replSetName}/,`); + expFailure(`${replSetName}/,,`); + expFailure(`${replSetName}//not/a/socket`); + expFailure(`mongodb://localhost:${port}/admin?replicaSet=`); + expFailure('mongodb://localhost:'); + expFailure(`mongodb://:${port}`); + jsTest.log("SUCCESSFUL test completion"); })(); diff --git a/jstests/replsets/apply_batches_totalMillis.js b/jstests/replsets/apply_batches_totalMillis.js new file mode 100644 index 00000000000..9e093211cb6 --- /dev/null +++ b/jstests/replsets/apply_batches_totalMillis.js @@ -0,0 +1,63 @@ +/** + * serverStatus.metrics.repl.apply.batches.totalMillis is a cumulative measure of how much time a + * node spends applying batches. This test checks that it includes the time spent waiting for + * batches to finish, by comparing the time recorded after replicating a small and a large load. + */ + +(function() { + "use strict"; + + // Gets the value of metrics.repl.apply.batches.totalMillis. + function getTotalMillis(node) { + return assert.commandWorked(node.adminCommand({serverStatus: 1})) + .metrics.repl.apply.batches.totalMillis; + } + + // Do a bulk insert of documents as: {{key: 0}, {key: 1}, {key: 2}, ... , {key: num-1}} + function performBulkInsert(coll, key, num) { + let bulk = coll.initializeUnorderedBulkOp(); + for (let i = 0; i < num; i++) { + let doc = {}; + doc[key] = i; + bulk.insert(doc); + } + assert.writeOK(bulk.execute()); + rst.awaitReplication(); + } + + let name = "apply_batches_totalMillis"; + let rst = new ReplSetTest({name: name, nodes: 2}); + rst.startSet(); + rst.initiate(); + + let primary = rst.getPrimary(); + let secondary = rst.getSecondary(); + let coll = primary.getDB(name)["foo"]; + + // Perform an initial write on the system and ensure steady state. + assert.writeOK(coll.insert({init: 0})); + rst.awaitReplication(); + let baseTime = getTotalMillis(secondary); + + // Introduce a small load and wait for it to be replicated. + performBulkInsert(coll, "small", 1000); + + // Record the time spent applying the small load. + let timeAfterSmall = getTotalMillis(secondary); + let deltaSmall = timeAfterSmall - baseTime; + + // Insert a significantly larger load. + performBulkInsert(coll, "large", 20000); + + // Record the time spent applying the large load. + let timeAfterLarge = getTotalMillis(secondary); + let deltaLarge = timeAfterLarge - timeAfterSmall; + + jsTestLog(`Recorded deltas: {small: ${deltaSmall}ms, large: ${deltaLarge}ms}.`); + + // We should have recorded at least as much time on the second load as we did on the first. + // This is a crude comparison that is only taken to check that the timer is used correctly. + assert(deltaLarge >= deltaSmall, "Expected a higher net totalMillis for the larger load."); + rst.stopSet(); + +})();
\ No newline at end of file diff --git a/jstests/replsets/apply_ops_concurrent_non_atomic_different_db.js b/jstests/replsets/apply_ops_concurrent_non_atomic_different_db.js new file mode 100644 index 00000000000..05cb6f9e996 --- /dev/null +++ b/jstests/replsets/apply_ops_concurrent_non_atomic_different_db.js @@ -0,0 +1,11 @@ +(function() { + 'use strict'; + + load('jstests/replsets/libs/apply_ops_concurrent_non_atomic.js'); + + new ApplyOpsConcurrentNonAtomicTest({ + ns1: 'test1.coll1', + ns2: 'test2.coll2', + requiresDocumentLevelConcurrency: false, + }).run(); +}()); diff --git a/jstests/replsets/apply_ops_concurrent_non_atomic_same_collection.js b/jstests/replsets/apply_ops_concurrent_non_atomic_same_collection.js new file mode 100644 index 00000000000..004eeaaa52f --- /dev/null +++ b/jstests/replsets/apply_ops_concurrent_non_atomic_same_collection.js @@ -0,0 +1,11 @@ +(function() { + 'use strict'; + + load('jstests/replsets/libs/apply_ops_concurrent_non_atomic.js'); + + new ApplyOpsConcurrentNonAtomicTest({ + ns1: 'test.coll', + ns2: 'test.coll', + requiresDocumentLevelConcurrency: true, + }).run(); +}()); diff --git a/jstests/replsets/apply_ops_concurrent_non_atomic_same_db.js b/jstests/replsets/apply_ops_concurrent_non_atomic_same_db.js new file mode 100644 index 00000000000..10f874382a5 --- /dev/null +++ b/jstests/replsets/apply_ops_concurrent_non_atomic_same_db.js @@ -0,0 +1,11 @@ +(function() { + 'use strict'; + + load('jstests/replsets/libs/apply_ops_concurrent_non_atomic.js'); + + new ApplyOpsConcurrentNonAtomicTest({ + ns1: 'test.coll1', + ns2: 'test.coll2', + requiresDocumentLevelConcurrency: false, + }).run(); +}()); diff --git a/jstests/replsets/clean_shutdown_oplog_state.js b/jstests/replsets/clean_shutdown_oplog_state.js index 5ac2f72d556..91b31fc66d7 100644 --- a/jstests/replsets/clean_shutdown_oplog_state.js +++ b/jstests/replsets/clean_shutdown_oplog_state.js @@ -10,7 +10,7 @@ var rst = new ReplSetTest({ name: "name", nodes: 2, - oplogSize: 100, + oplogSize: 500, }); rst.startSet(); diff --git a/jstests/replsets/index_delete.js b/jstests/replsets/index_delete.js index f7c45e2f2e0..1c352a18581 100644 --- a/jstests/replsets/index_delete.js +++ b/jstests/replsets/index_delete.js @@ -60,7 +60,7 @@ try { } else { return false; } - }, "index not started on secondary", 30000, 50); + }, "index not started on secondary"); } finally { // Turn off failpoint and let the index build resumes. assert.commandWorked( diff --git a/jstests/replsets/initial_sync_rename_collection_unsafe.js b/jstests/replsets/initial_sync_rename_collection_unsafe.js new file mode 100644 index 00000000000..a105e1c9287 --- /dev/null +++ b/jstests/replsets/initial_sync_rename_collection_unsafe.js @@ -0,0 +1,61 @@ +/** + * Tests that renameCollection commands do not abort initial sync when users specify + * 'allowUnsafeRenamesDuringInitialSync'. + */ + +(function() { + 'use strict'; + + load("jstests/libs/check_log.js"); + + var parameters = TestData.setParameters; + if (parameters && parameters.indexOf("use3dot2InitialSync=true") != -1) { + jsTest.log("Skipping this test because use3dot2InitialSync was provided."); + return; + } + + const basename = 'initial_sync_rename_collection_unsafe'; + + const rst = new ReplSetTest({name: basename, nodes: 1}); + rst.startSet(); + rst.initiate(); + + const dbName = 'd'; + const primary = rst.getPrimary(); + const primaryDB = primary.getDB(dbName); + + assert.writeOK(primaryDB['foo'].save({})); + + jsTestLog('Bring up a new node'); + const secondary = rst.add({setParameter: {allowUnsafeRenamesDuringInitialSync: true}}); + assert.commandWorked(secondary.adminCommand( + {configureFailPoint: 'initialSyncHangBeforeCopyingDatabases', mode: 'alwaysOn'})); + rst.reInitiate(); + assert.eq(primary, rst.getPrimary(), 'Primary changed after reconfig'); + + // Wait for fail point message to be logged. + checkLog.contains(secondary, + 'initial sync - initialSyncHangBeforeCopyingDatabases fail point enabled'); + + jsTestLog('Rename collection on the primary'); + assert.commandWorked(primaryDB['foo'].renameCollection('renamed')); + + assert.commandWorked(secondary.adminCommand( + {configureFailPoint: 'initialSyncHangBeforeCopyingDatabases', mode: 'off'})); + + checkLog.contains(secondary, 'allowUnsafeRenamesDuringInitialSync set to true'); + + jsTestLog('Wait for both nodes to be up-to-date'); + rst.awaitSecondaryNodes(); + rst.awaitReplication(); + + jsTestLog('Check that all collections were renamed correctly on the secondary'); + const secondaryDB = secondary.getDB(dbName); + assert.eq(secondaryDB['renamed'].find().itcount(), 1, 'renamed collection does not exist'); + assert.eq(secondaryDB['foo'].find().itcount(), 0, 'collection `foo` exists after rename'); + + let res = assert.commandWorked(secondary.adminCommand({replSetGetStatus: 1, initialSync: 1})); + assert.eq(res.initialSyncStatus.failedInitialSyncAttempts, 0); + + rst.stopSet(); +})(); diff --git a/jstests/replsets/last_vote.js b/jstests/replsets/last_vote.js index b9e0474217c..44d349f3237 100644 --- a/jstests/replsets/last_vote.js +++ b/jstests/replsets/last_vote.js @@ -134,9 +134,6 @@ "replSetRequestVotes response had the wrong term: " + tojson(response)); assert(!response.voteGranted, "node granted vote in term before last vote doc: " + tojson(response)); - assert.eq(response.reason, - "candidate's term is lower than mine", - "replSetRequestVotes response had the wrong reason: " + tojson(response)); assertNodeHasLastVote(node0, term, rst.nodes[0]); assertCurrentTerm(node0, term); @@ -178,9 +175,6 @@ "replSetRequestVotes response had the wrong term: " + tojson(response)); assert(!response.voteGranted, "node granted vote in term of last vote doc: " + tojson(response)); - assert.eq(response.reason, - "already voted for another candidate this term", - "replSetRequestVotes response had the wrong reason: " + tojson(response)); assertNodeHasLastVote(node0, term, rst.nodes[0]); assertCurrentTerm(node0, term); diff --git a/jstests/replsets/libs/apply_ops_concurrent_non_atomic.js b/jstests/replsets/libs/apply_ops_concurrent_non_atomic.js new file mode 100644 index 00000000000..5e170378de2 --- /dev/null +++ b/jstests/replsets/libs/apply_ops_concurrent_non_atomic.js @@ -0,0 +1,232 @@ +/** + * This test ensures that multiple non-atomic applyOps commands can run concurrently. + * Prior to SERVER-29802, applyOps would acquire the global lock regardless of the + * atomicity of the operations (as a whole) being applied. + * + * Every instance of ApplyOpsConcurrentNonAtomicTest is configured with an "options" document + * with the following format: + * { + * ns1: <string>, + * ns1: <string>, + * requiresDocumentLevelConcurrency: <bool>, + * } + * + * ns1: + * Fully qualified namespace of first set of CRUD operations. For simplicity, only insert + * operations will be used. The set of documents generated for the inserts into ns1 will have + * _id values distinct from those generated for ns2. + * + * ns2: + * Fully qualified namespace of second set of CRUD operations. This may be the same namespace as + * ns1. As with ns1, only insert operations will be used. + * + * requiresDocumentLevelConcurrency: + * Set to true if this test case can only be run with a storage engine that supports document + * level concurrency. + */ +var ApplyOpsConcurrentNonAtomicTest = function(options) { + 'use strict'; + + load('jstests/concurrency/fsm_workload_helpers/server_types.js'); + + if (!(this instanceof ApplyOpsConcurrentNonAtomicTest)) { + return new ApplyOpsConcurrentNonAtomicTest(options); + } + + // Capture the 'this' reference + var self = this; + + self.options = options; + + /** + * Logs message using test name as prefix. + */ + function testLog(message) { + jsTestLog('ApplyOpsConcurrentNonAtomicTest: ' + message); + } + + /** + * Creates an array of insert operations for applyOps into collection 'coll'. + */ + function generateInsertOps(coll, numOps, id) { + // Explicit 'use strict' to prevent mozjs from injecting its own "use strict" directive + // (with incorrect indentation) when we convert this function into a string for + // startParallelShell(). + 'use strict'; + const ops = Array(numOps).fill('ignored').map((unused, i) => { + return {op: 'i', ns: coll.getFullName(), o: {_id: (id * numOps + i), id: id}}; + }); + return ops; + } + + /** + * Runs applyOps in non-atomic mode to insert 'numOps' documents into collection 'coll'. + */ + function applyOpsInsertNonAtomic(coll, numOps, id) { + 'use strict'; + const ops = generateInsertOps(coll, numOps, id); + const mydb = coll.getDB(); + assert.commandWorked(mydb.runCommand({applyOps: ops, allowAtomic: false}), + 'failed to insert documents into ' + coll.getFullName()); + } + + /** + * Parses 'numOps' and collection namespace from 'options' and runs applyOps to inserted + * generated documents. + * + * options format: + * { + * ns: <string>, + * numOps: <int>, + * id: <int>, + * } + * + * ns: + * Fully qualified namespace of collection to insert documents into. + * + * numOps: + * Number of insert operations to generate for applyOps command. + * + * id: + * Index of collection for applyOps. Used with 'numOps' to generate _id values that will not + * collide with collections with different indexes. + */ + function insertFunction(options) { + 'use strict'; + + const coll = db.getMongo().getCollection(options.ns); + const numOps = options.numOps; + const id = options.id; + + testLog('Starting to apply ' + numOps + ' operations in collection ' + coll.getFullName()); + applyOpsInsertNonAtomic(coll, numOps, id); + testLog('Successfully applied ' + numOps + ' operations in collection ' + + coll.getFullName()); + } + + /** + * Creates a function for startParallelShell() to run that will insert documents into + * collection 'coll' using applyOps. + */ + function createInsertFunction(coll, numOps, id) { + const options = { + ns: coll.getFullName(), + numOps: numOps, + id: id, + }; + const functionName = 'insertFunction_' + coll.getFullName().replace(/\./g, '_'); + const s = // + '\n\n' + // + 'const testLog = ' + testLog + ';\n\n' + // + 'const generateInsertOps = ' + generateInsertOps + ';\n\n' + // + 'const applyOpsInsertNonAtomic = ' + applyOpsInsertNonAtomic + ';\n\n' + // + 'const ' + functionName + ' = ' + insertFunction + ';\n\n' + // + functionName + '(' + tojson(options) + ');'; // + return s; + } + + /** + * Returns number of insert operations reported by serverStatus. + * In 3.4 'opcountersRepl', not 'opcounters' was previously the correct field. Now non-atomic + * ops are now replicated as they are applied and are counted toward the global op counter. + */ + function getInsertOpCount(serverStatus) { + return serverStatus.opcounters.insert; + } + + /** + * Runs the test. + */ + this.run = function() { + const options = this.options; + + assert(options.ns1, 'collection 1 namespace not provided'); + assert(options.ns2, 'collection 2 namespace not provided'); + + const replTest = new ReplSetTest({nodes: 1}); + replTest.startSet(); + replTest.initiate(); + + const primary = replTest.getPrimary(); + const adminDb = primary.getDB('admin'); + + if (options.requiresDocumentLevelConcurrency && + !supportsDocumentLevelConcurrency(adminDb)) { + testLog('Skipping test because storage engine does not support document level ' + + 'concurrency.'); + return; + } + + const coll1 = primary.getCollection(options.ns1); + const db1 = coll1.getDB(); + const coll2 = primary.getCollection(options.ns2); + const db2 = coll2.getDB(); + + assert.commandWorked(db1.createCollection(coll1.getName())); + if (coll1.getFullName() !== coll2.getFullName()) { + assert.commandWorked(db2.createCollection(coll2.getName())); + } + + // Enable fail point to pause applyOps between operations. + assert.commandWorked(primary.adminCommand( + {configureFailPoint: 'applyOpsPauseBetweenOperations', mode: 'alwaysOn'})); + + // This logs each operation being applied. + const previousLogLevel = + assert.commandWorked(primary.setLogLevel(3, 'replication')).was.replication.verbosity; + + testLog('Applying operations in collections ' + coll1.getFullName() + ' and ' + + coll2.getFullName()); + + const numOps = 100; + const insertProcess1 = + startParallelShell(createInsertFunction(coll1, numOps, 0), replTest.getPort(0)); + const insertProcess2 = + startParallelShell(createInsertFunction(coll2, numOps, 1), replTest.getPort(0)); + + // The fail point will prevent applyOps from advancing past the first operation in each + // batch of operations. If applyOps is applying both sets of operations concurrently without + // holding the global lock, the insert opcounter will eventually be incremented to 2. + try { + let insertOpCount = 0; + const expectedFinalOpCount = 2; + assert.soon( + function() { + const serverStatus = adminDb.serverStatus(); + insertOpCount = getInsertOpCount(serverStatus); + // This assertion may fail if the fail point is not implemented correctly within + // applyOps. This allows us to fail fast instead of waiting for the + // assert.soon() function to time out. + assert.lte(insertOpCount, + expectedFinalOpCount, + 'Expected at most ' + expectedFinalOpCount + + ' documents inserted with fail point enabled. ' + + 'Most recent insert operation count = ' + insertOpCount); + return insertOpCount === expectedFinalOpCount; + }, + 'Insert operation count did not reach ' + expectedFinalOpCount + + ' as expected with fail point enabled. Most recent insert operation count = ' + + insertOpCount); + } finally { + assert.commandWorked(primary.adminCommand( + {configureFailPoint: 'applyOpsPauseBetweenOperations', mode: 'off'})); + } + + insertProcess1(); + insertProcess2(); + + testLog('Successfully applied operations in collections ' + coll1.getFullName() + ' and ' + + coll2.getFullName()); + + // Reset log level. + primary.setLogLevel(previousLogLevel, 'replication'); + + const serverStatus = adminDb.serverStatus(); + assert.eq(200, + getInsertOpCount(serverStatus), + 'incorrect number of insert operations in server status after applyOps: ' + + tojson(serverStatus)); + + replTest.stopSet(); + }; +}; diff --git a/jstests/replsets/libs/apply_ops_insert_write_conflict.js b/jstests/replsets/libs/apply_ops_insert_write_conflict.js index 88b299c08b6..d0a47f5b6d8 100644 --- a/jstests/replsets/libs/apply_ops_insert_write_conflict.js +++ b/jstests/replsets/libs/apply_ops_insert_write_conflict.js @@ -36,14 +36,6 @@ var ApplyOpsInsertWriteConflictTest = function(options) { return {op: 'i', ns: t.getFullName(), o: {_id: i}}; }); - if (!options.atomic) { - // Adding a command to the list of operations to prevent the applyOps command from - // applying - // all the operations atomically. - ops.push({ns: "test.$cmd", op: "c", o: {applyOps: []}}); - numOps++; - } - // Probabilities for WCE are chosen based on empirical testing. // The probability for WCE during an atomic applyOps should be much smaller than that for // the non-atomic case because we have to attempt to re-apply the entire batch of 'numOps' @@ -62,7 +54,7 @@ var ApplyOpsInsertWriteConflictTest = function(options) { var previousLogLevel = assert.commandWorked(primaryDB.setLogLevel(3, 'replication')).was.replication.verbosity; - var applyOpsResult = primaryDB.adminCommand({applyOps: ops}); + var applyOpsResult = primaryDB.adminCommand({applyOps: ops, allowAtomic: options.atomic}); // Reset log level. primaryDB.setLogLevel(previousLogLevel, 'replication'); diff --git a/jstests/replsets/no_flapping_during_network_partition.js b/jstests/replsets/no_flapping_during_network_partition.js index 5a289b2afdd..daab9be2bca 100644 --- a/jstests/replsets/no_flapping_during_network_partition.js +++ b/jstests/replsets/no_flapping_during_network_partition.js @@ -39,7 +39,7 @@ primary.disconnect(secondary); jsTestLog("Wait long enough for the secondary to call for an election."); - checkLog.contains(secondary, "can see a healthy primary of equal or greater priority"); + checkLog.contains(secondary, "can see a healthy primary"); jsTestLog("Verify the primary and secondary do not change during the partition."); assert.eq(primary, replTest.getPrimary()); diff --git a/jstests/replsets/noop_writes_wait_for_write_concern.js b/jstests/replsets/noop_writes_wait_for_write_concern.js new file mode 100644 index 00000000000..61ffc518df4 --- /dev/null +++ b/jstests/replsets/noop_writes_wait_for_write_concern.js @@ -0,0 +1,235 @@ +/** + * This file tests that if a user initiates a write that becomes a noop due to being a duplicate + * operation, that we still wait for write concern. This is because we must wait for write concern + * on the write that made this a noop so that we can be sure it doesn't get rolled back if we + * acknowledge it. + */ + +(function() { + "use strict"; + load('jstests/libs/write_concern_util.js'); + + var name = 'noop_writes_wait_for_write_concern'; + var replTest = new ReplSetTest({ + name: name, + nodes: [{}, {rsConfig: {priority: 0}}, {rsConfig: {priority: 0}}], + }); + replTest.startSet(); + replTest.initiate(); + // Stops node 1 so that all w:3 write concerns time out. We have 3 data bearing nodes so that + // 'dropDatabase' can satisfy its implicit writeConcern: majority but still time out from the + // explicit w:3 write concern. + replTest.stop(1); + + var primary = replTest.getPrimary(); + assert.eq(primary, replTest.nodes[0]); + var dbName = 'testDB'; + var db = primary.getDB(dbName); + var collName = 'testColl'; + var coll = db[collName]; + + function dropTestCollection() { + coll.drop(); + assert.eq(0, coll.find().itcount(), "test collection not empty"); + } + + // Each entry in this array contains a command whose noop write concern behavior needs to be + // tested. Entries have the following structure: + // { + // req: <object>, // Command request object that will result in a noop + // // write after the setup function is called. + // + // setupFunc: <function()>, // Function to run to ensure that the request is a + // // noop. + // + // confirmFunc: <function(res)>, // Function to run after the command is run to ensure + // // that it executed properly. Accepts the result of + // // the noop request to validate it. + // } + var commands = []; + + commands.push({ + req: {applyOps: [{op: "i", ns: coll.getFullName(), o: {_id: 1}}]}, + setupFunc: function() { + assert.writeOK(coll.insert({_id: 1})); + }, + confirmFunc: function(res) { + assert.commandWorked(res); + assert.eq(res.applied, 1); + assert.eq(res.results[0], true); + assert.eq(coll.find().itcount(), 1); + assert.eq(coll.count({_id: 1}), 1); + } + }); + + // 'update' where the document to update does not exist. + commands.push({ + req: {update: collName, updates: [{q: {a: 1}, u: {b: 2}}]}, + setupFunc: function() { + assert.writeOK(coll.insert({a: 1})); + assert.writeOK(coll.update({a: 1}, {b: 2})); + }, + confirmFunc: function(res) { + assert.commandWorked(res); + assert.eq(res.n, 0); + assert.eq(res.nModified, 0); + assert.eq(coll.find().itcount(), 1); + assert.eq(coll.count({b: 2}), 1); + } + }); + + // 'update' where the update has already been done. + commands.push({ + req: {update: collName, updates: [{q: {a: 1}, u: {$set: {b: 2}}}]}, + setupFunc: function() { + assert.writeOK(coll.insert({a: 1})); + assert.writeOK(coll.update({a: 1}, {$set: {b: 2}})); + }, + confirmFunc: function(res) { + assert.commandWorked(res); + assert.eq(res.n, 1); + assert.eq(res.nModified, 0); + assert.eq(coll.find().itcount(), 1); + assert.eq(coll.count({a: 1, b: 2}), 1); + } + }); + + commands.push({ + req: {delete: collName, deletes: [{q: {a: 1}, limit: 1}]}, + setupFunc: function() { + assert.writeOK(coll.insert({a: 1})); + assert.writeOK(coll.remove({a: 1})); + }, + confirmFunc: function(res) { + assert.commandWorked(res); + assert.eq(res.n, 0); + assert.eq(coll.count({a: 1}), 0); + } + }); + + commands.push({ + req: {createIndexes: collName, indexes: [{key: {a: 1}, name: "a_1"}]}, + setupFunc: function() { + assert.writeOK(coll.insert({a: 1})); + assert.commandWorked( + db.runCommand({createIndexes: collName, indexes: [{key: {a: 1}, name: "a_1"}]})); + }, + confirmFunc: function(res) { + assert.commandWorked(res); + assert.eq(res.numIndexesBefore, res.numIndexesAfter); + } + }); + + // 'findAndModify' where the document to update does not exist. + commands.push({ + req: {findAndModify: collName, query: {a: 1}, update: {b: 2}}, + setupFunc: function() { + assert.writeOK(coll.insert({a: 1})); + assert.commandWorked( + db.runCommand({findAndModify: collName, query: {a: 1}, update: {b: 2}})); + }, + confirmFunc: function(res) { + assert.commandWorked(res); + assert.eq(res.lastErrorObject.updatedExisting, false); + assert.eq(coll.find().itcount(), 1); + assert.eq(coll.count({b: 2}), 1); + } + }); + + // 'findAndModify' where the update has already been done. + commands.push({ + req: {findAndModify: collName, query: {a: 1}, update: {$set: {b: 2}}}, + setupFunc: function() { + assert.writeOK(coll.insert({a: 1})); + assert.commandWorked( + db.runCommand({findAndModify: collName, query: {a: 1}, update: {$set: {b: 2}}})); + }, + confirmFunc: function(res) { + assert.commandWorked(res); + assert.eq(res.lastErrorObject.updatedExisting, true); + assert.eq(coll.find().itcount(), 1); + assert.eq(coll.count({a: 1, b: 2}), 1); + } + }); + + commands.push({ + req: {dropDatabase: 1}, + setupFunc: function() { + assert.writeOK(coll.insert({a: 1})); + assert.commandWorked(db.runCommand({dropDatabase: 1})); + }, + confirmFunc: function(res) { + assert.commandWorked(res); + } + }); + + commands.push({ + req: {drop: collName}, + setupFunc: function() { + assert.writeOK(coll.insert({a: 1})); + assert.commandWorked(db.runCommand({drop: collName})); + }, + confirmFunc: function(res) { + assert.commandFailedWithCode(res, ErrorCodes.NamespaceNotFound); + } + }); + + commands.push({ + req: {create: collName}, + setupFunc: function() { + assert.commandWorked(db.runCommand({create: collName})); + }, + confirmFunc: function(res) { + assert.commandFailedWithCode(res, ErrorCodes.NamespaceExists); + } + }); + + commands.push({ + req: {insert: collName, documents: [{_id: 1}]}, + setupFunc: function() { + assert.writeOK(coll.insert({_id: 1})); + }, + confirmFunc: function(res) { + assert.commandWorked(res); + assert.eq(res.n, 0); + assert.eq(res.writeErrors[0].code, ErrorCodes.DuplicateKey); + assert.eq(coll.count({_id: 1}), 1); + } + }); + + function testCommandWithWriteConcern(cmd) { + // Provide a small wtimeout that we expect to time out. + cmd.req.writeConcern = {w: 3, wtimeout: 1000}; + jsTest.log("Testing " + tojson(cmd.req)); + + dropTestCollection(); + + cmd.setupFunc(); + + // We run the command on a different connection. If the the command were run on the + // same connection, then the client last op for the noop write would be set by the setup + // operation. By using a fresh connection the client last op begins as null. + // This test explicitly tests that write concern for noop writes works when the + // client last op has not already been set by a duplicate operation. + var shell2 = new Mongo(primary.host); + + // We check the error code of 'res' in the 'confirmFunc'. + var res = shell2.getDB(dbName).runCommand(cmd.req); + + try { + // Tests that the command receives a write concern error. If we don't wait for write + // concern on noop writes then we won't get a write concern error. + assertWriteConcernError(res); + cmd.confirmFunc(res); + } catch (e) { + // Make sure that we print out the response. + printjson(res); + throw e; + } + } + + commands.forEach(function(cmd) { + testCommandWithWriteConcern(cmd); + }); + +})();
\ No newline at end of file diff --git a/jstests/replsets/read_committed_with_catalog_changes.js b/jstests/replsets/read_committed_with_catalog_changes.js index 02d26759e26..7928b16d3ae 100644 --- a/jstests/replsets/read_committed_with_catalog_changes.js +++ b/jstests/replsets/read_committed_with_catalog_changes.js @@ -214,7 +214,7 @@ load("jstests/replsets/rslib.js"); // For startSetIfSupportsReadMajority. "Expected read of " + coll.getFullName() + " to block"); } - function assertReadsSucceed(coll, timeoutMs = 10000) { + function assertReadsSucceed(coll, timeoutMs = 20000) { var res = coll.runCommand('find', {"readConcern": {"level": "majority"}, "maxTimeMS": timeoutMs}); assert.commandWorked(res, 'reading from ' + coll.getFullName()); diff --git a/jstests/replsets/startParallelShell.js b/jstests/replsets/startParallelShell.js new file mode 100644 index 00000000000..beca88d19a9 --- /dev/null +++ b/jstests/replsets/startParallelShell.js @@ -0,0 +1,33 @@ +// Test startParallelShell() in a replica set. + +var db; + +(function() { + 'use strict'; + + const setName = 'rs0'; + const replSet = new ReplSetTest({name: setName, nodes: 3}); + const nodes = replSet.nodeList(); + replSet.startSet(); + replSet.initiate(); + + const url = replSet.getURL(); + print("* Connecting to " + url); + const mongo = new Mongo(url); + db = mongo.getDB('admin'); + assert.eq(url, mongo.host, "replSet.getURL() should match active connection string"); + + print("* Starting parallel shell on --host " + db.getMongo().host); + startParallelShell('db.coll0.insert({test: "connString only"});'); + assert.soon(function() { + return db.coll0.find({test: "connString only"}).count() === 1; + }); + + const uri = new MongoURI(url); + const port0 = uri.servers[0].port; + print("* Starting parallel shell w/ --port " + port0); + startParallelShell('db.coll0.insert({test: "explicit port"});', port0); + assert.soon(function() { + return db.coll0.find({test: "explicit port"}).count() === 1; + }); +})(); diff --git a/jstests/replsets/too_stale_secondary.js b/jstests/replsets/too_stale_secondary.js index 369662e5f16..1a0c28b3454 100644 --- a/jstests/replsets/too_stale_secondary.js +++ b/jstests/replsets/too_stale_secondary.js @@ -34,7 +34,7 @@ "use strict"; function getFirstOplogEntry(conn) { - return conn.getDB('local').oplog.rs.find().sort({ts: 1}).limit(1)[0]; + return conn.getDB('local').oplog.rs.find().sort({$natural: 1}).limit(1)[0]; } /** diff --git a/jstests/sharding/auth.js b/jstests/sharding/auth.js index 61b25f10dde..037532acaec 100644 --- a/jstests/sharding/auth.js +++ b/jstests/sharding/auth.js @@ -38,7 +38,7 @@ name: "auth", mongos: 1, shards: 0, - other: {keyFile: "jstests/libs/key1", chunkSize: 1, enableAutoSplit: true}, + other: {keyFile: "jstests/libs/key1", chunkSize: 1, enableAutoSplit: false}, }); if (s.getDB('admin').runCommand('buildInfo').bits < 64) { @@ -167,6 +167,7 @@ s.getDB("test").foo.remove({}); var num = 10000; + assert.commandWorked(s.s.adminCommand({split: "test.foo", middle: {x: num / 2}})); var bulk = s.getDB("test").foo.initializeUnorderedBulkOp(); for (i = 0; i < num; i++) { bulk.insert( diff --git a/jstests/sharding/auto_rebalance_parallel.js b/jstests/sharding/auto_rebalance_parallel.js index 955319b8c5d..c7078a6898a 100644 --- a/jstests/sharding/auto_rebalance_parallel.js +++ b/jstests/sharding/auto_rebalance_parallel.js @@ -1,43 +1,71 @@ /** * Tests that the cluster is balanced in parallel in one balancer round (standalone). */ + (function() { 'use strict'; var st = new ShardingTest({shards: 4}); + var config = st.s0.getDB('config'); assert.commandWorked(st.s0.adminCommand({enableSharding: 'TestDB'})); st.ensurePrimaryShard('TestDB', st.shard0.shardName); - assert.commandWorked(st.s0.adminCommand({shardCollection: 'TestDB.TestColl', key: {Key: 1}})); - var coll = st.s0.getDB('TestDB').TestColl; + function prepareCollectionForBalance(collName) { + assert.commandWorked(st.s0.adminCommand({shardCollection: collName, key: {Key: 1}})); + + var coll = st.s0.getCollection(collName); + + // Create 4 chunks initially and ensure they get balanced within 1 balancer round + assert.writeOK(coll.insert({Key: 1, Value: 'Test value 1'})); + assert.writeOK(coll.insert({Key: 10, Value: 'Test value 10'})); + assert.writeOK(coll.insert({Key: 20, Value: 'Test value 20'})); + assert.writeOK(coll.insert({Key: 30, Value: 'Test value 30'})); + + assert.commandWorked(st.splitAt(collName, {Key: 10})); + assert.commandWorked(st.splitAt(collName, {Key: 20})); + assert.commandWorked(st.splitAt(collName, {Key: 30})); + + // Move two of the chunks to shard0001 so we have option to do parallel balancing + assert.commandWorked(st.moveChunk(collName, {Key: 20}, st.shard1.shardName)); + assert.commandWorked(st.moveChunk(collName, {Key: 30}, st.shard1.shardName)); - // Create 4 chunks initially and ensure they get balanced within 1 balancer round - assert.writeOK(coll.insert({Key: 1, Value: 'Test value 1'})); - assert.writeOK(coll.insert({Key: 10, Value: 'Test value 10'})); - assert.writeOK(coll.insert({Key: 20, Value: 'Test value 20'})); - assert.writeOK(coll.insert({Key: 30, Value: 'Test value 30'})); + assert.eq(2, config.chunks.find({ns: collName, shard: st.shard0.shardName}).itcount()); + assert.eq(2, config.chunks.find({ns: collName, shard: st.shard1.shardName}).itcount()); + } - assert.commandWorked(st.splitAt('TestDB.TestColl', {Key: 10})); - assert.commandWorked(st.splitAt('TestDB.TestColl', {Key: 20})); - assert.commandWorked(st.splitAt('TestDB.TestColl', {Key: 30})); + function checkCollectionBalanced(collName) { + assert.eq(1, config.chunks.find({ns: collName, shard: st.shard0.shardName}).itcount()); + assert.eq(1, config.chunks.find({ns: collName, shard: st.shard1.shardName}).itcount()); + assert.eq(1, config.chunks.find({ns: collName, shard: st.shard2.shardName}).itcount()); + assert.eq(1, config.chunks.find({ns: collName, shard: st.shard3.shardName}).itcount()); + } - // Move two of the chunks to shard0001 so we have option to do parallel balancing - assert.commandWorked(st.moveChunk('TestDB.TestColl', {Key: 20}, st.shard1.shardName)); - assert.commandWorked(st.moveChunk('TestDB.TestColl', {Key: 30}, st.shard1.shardName)); + function countMoves(collName) { + return config.changelog.find({what: 'moveChunk.start', ns: collName}).itcount(); + } - assert.eq(2, st.s0.getDB('config').chunks.find({shard: st.shard0.shardName}).itcount()); - assert.eq(2, st.s0.getDB('config').chunks.find({shard: st.shard1.shardName}).itcount()); + prepareCollectionForBalance('TestDB.TestColl1'); + prepareCollectionForBalance('TestDB.TestColl2'); + + // Count the moveChunk start attempts accurately and ensure that only the correct number of + // migrations are scheduled + const testColl1InitialMoves = countMoves('TestDB.TestColl1'); + const testColl2InitialMoves = countMoves('TestDB.TestColl2'); - // Do enable the balancer and wait for a single balancer round st.startBalancer(); st.awaitBalancerRound(); + st.awaitBalancerRound(); st.stopBalancer(); - assert.eq(1, st.s0.getDB('config').chunks.find({shard: st.shard0.shardName}).itcount()); - assert.eq(1, st.s0.getDB('config').chunks.find({shard: st.shard1.shardName}).itcount()); - assert.eq(1, st.s0.getDB('config').chunks.find({shard: st.shard2.shardName}).itcount()); - assert.eq(1, st.s0.getDB('config').chunks.find({shard: st.shard3.shardName}).itcount()); + checkCollectionBalanced('TestDB.TestColl1'); + checkCollectionBalanced('TestDB.TestColl2'); + + assert.eq(2, countMoves('TestDB.TestColl1') - testColl1InitialMoves); + assert.eq(2, countMoves('TestDB.TestColl2') - testColl2InitialMoves); + + // Ensure there are no migration errors reported + assert.eq(0, config.changelog.find({what: 'moveChunk.error'}).itcount()); st.stop(); })(); diff --git a/jstests/sharding/autosplit.js b/jstests/sharding/autosplit.js index bb34021487f..0eba386b6a3 100644 --- a/jstests/sharding/autosplit.js +++ b/jstests/sharding/autosplit.js @@ -4,7 +4,12 @@ (function() { 'use strict'; - var s = new ShardingTest({name: "auto1", shards: 2, mongos: 1, other: {enableAutoSplit: true}}); + var s = new ShardingTest({ + name: "auto1", + shards: 2, + mongos: 1, + other: {enableAutoSplit: true, chunkSize: 10}, + }); assert.commandWorked(s.s0.adminCommand({enablesharding: "test"})); s.ensurePrimaryShard('test', 'shard0001'); diff --git a/jstests/sharding/mapReduce_inSharded_outSharded.js b/jstests/sharding/mapReduce_inSharded_outSharded.js index d1aba2599f0..d73eb517e98 100644 --- a/jstests/sharding/mapReduce_inSharded_outSharded.js +++ b/jstests/sharding/mapReduce_inSharded_outSharded.js @@ -1,60 +1,70 @@ -var verifyOutput = function(out) { - printjson(out); - assert.eq(out.counts.input, 51200, "input count is wrong"); - assert.eq(out.counts.emit, 51200, "emit count is wrong"); - assert.gt(out.counts.reduce, 99, "reduce count is wrong"); - assert.eq(out.counts.output, 512, "output count is wrong"); -}; - -var st = new ShardingTest( - {shards: 2, verbose: 1, mongos: 1, other: {chunkSize: 1, enableBalancer: true}}); - -st.adminCommand({enablesharding: "mrShard"}); -st.ensurePrimaryShard('mrShard', 'shard0001'); -st.adminCommand({shardcollection: "mrShard.srcSharded", key: {"_id": 1}}); - -var db = st.getDB("mrShard"); - -var bulk = db.srcSharded.initializeUnorderedBulkOp(); -for (j = 0; j < 100; j++) { - for (i = 0; i < 512; i++) { - bulk.insert({j: j, i: i}); +(function() { + "use strict"; + + var verifyOutput = function(out) { + printjson(out); + assert.eq(out.counts.input, 51200, "input count is wrong"); + assert.eq(out.counts.emit, 51200, "emit count is wrong"); + assert.gt(out.counts.reduce, 99, "reduce count is wrong"); + assert.eq(out.counts.output, 512, "output count is wrong"); + }; + + var st = new ShardingTest( + {shards: 2, verbose: 1, mongos: 1, other: {chunkSize: 1, enableBalancer: true}}); + + var admin = st.s0.getDB('admin'); + + assert.commandWorked(admin.runCommand({enablesharding: "mrShard"})); + st.ensurePrimaryShard('mrShard', 'shard0001'); + assert.commandWorked( + admin.runCommand({shardcollection: "mrShard.srcSharded", key: {"_id": 1}})); + + var db = st.s0.getDB("mrShard"); + + var bulk = db.srcSharded.initializeUnorderedBulkOp(); + for (var j = 0; j < 100; j++) { + for (var i = 0; i < 512; i++) { + bulk.insert({j: j, i: i}); + } + } + assert.writeOK(bulk.execute()); + + function map() { + emit(this.i, 1); + } + function reduce(key, values) { + return Array.sum(values); } -} -assert.writeOK(bulk.execute()); - -function map() { - emit(this.i, 1); -} -function reduce(key, values) { - return Array.sum(values); -} - -// sharded src sharded dst -var suffix = "InShardedOutSharded"; - -var out = - db.srcSharded.mapReduce(map, reduce, {out: {replace: "mrReplace" + suffix, sharded: true}}); -verifyOutput(out); - -out = db.srcSharded.mapReduce(map, reduce, {out: {merge: "mrMerge" + suffix, sharded: true}}); -verifyOutput(out); - -out = db.srcSharded.mapReduce(map, reduce, {out: {reduce: "mrReduce" + suffix, sharded: true}}); -verifyOutput(out); - -out = db.srcSharded.mapReduce(map, reduce, {out: {inline: 1}}); -verifyOutput(out); -assert(out.results != 'undefined', "no results for inline"); - -out = db.srcSharded.mapReduce( - map, reduce, {out: {replace: "mrReplace" + suffix, db: "mrShardOtherDB", sharded: true}}); -verifyOutput(out); - -out = db.runCommand({ - mapReduce: "srcSharded", // use new name mapReduce rather than mapreduce - map: map, - reduce: reduce, - out: "mrBasic" + "srcSharded", -}); -verifyOutput(out); + + // sharded src sharded dst + var suffix = "InShardedOutSharded"; + + var out = + db.srcSharded.mapReduce(map, reduce, {out: {replace: "mrReplace" + suffix, sharded: true}}); + verifyOutput(out); + + out = db.srcSharded.mapReduce(map, reduce, {out: {merge: "mrMerge" + suffix, sharded: true}}); + verifyOutput(out); + + out = db.srcSharded.mapReduce(map, reduce, {out: {reduce: "mrReduce" + suffix, sharded: true}}); + verifyOutput(out); + + out = db.srcSharded.mapReduce(map, reduce, {out: {inline: 1}}); + verifyOutput(out); + assert(out.results != 'undefined', "no results for inline"); + + out = db.srcSharded.mapReduce( + map, reduce, {out: {replace: "mrReplace" + suffix, db: "mrShardOtherDB", sharded: true}}); + verifyOutput(out); + + out = db.runCommand({ + mapReduce: "srcSharded", // use new name mapReduce rather than mapreduce + map: map, + reduce: reduce, + out: "mrBasic" + "srcSharded", + }); + verifyOutput(out); + + st.stop(); + +})(); diff --git a/jstests/sharding/migrateBig_balancer.js b/jstests/sharding/migrateBig_balancer.js index 9eb50c2168e..03e98fa5493 100644 --- a/jstests/sharding/migrateBig_balancer.js +++ b/jstests/sharding/migrateBig_balancer.js @@ -31,11 +31,8 @@ assert.eq(40, coll.count(), "prep1"); - printjson(coll.stats()); - - admin.printShardingStatus(); - - admin.runCommand({shardcollection: "" + coll, key: {_id: 1}}); + assert.commandWorked(admin.runCommand({shardcollection: "" + coll, key: {_id: 1}})); + st.printShardingStatus(); assert.lt( 5, mongos.getDB("config").chunks.find({ns: "test.stuff"}).count(), "not enough chunks"); @@ -56,5 +53,4 @@ }, "never migrated", 10 * 60 * 1000, 1000); st.stop(); - })(); diff --git a/jstests/sharding/movechunk_commit_changelog_stats.js b/jstests/sharding/movechunk_commit_changelog_stats.js new file mode 100644 index 00000000000..d257bb6ed94 --- /dev/null +++ b/jstests/sharding/movechunk_commit_changelog_stats.js @@ -0,0 +1,41 @@ +// +// Tests that the changelog entry for moveChunk.commit contains stats on the migration. +// + +(function() { + 'use strict'; + + var st = new ShardingTest({mongos: 1, shards: 2}); + var kDbName = 'db'; + + var mongos = st.s0; + var shard0 = st.shard0.shardName; + var shard1 = st.shard1.shardName; + + assert.commandWorked(mongos.adminCommand({enableSharding: kDbName})); + st.ensurePrimaryShard(kDbName, shard0); + + function assertCountsInChangelog() { + let changeLog = st.s.getDB('config').changelog.find({what: 'moveChunk.commit'}).toArray(); + assert.gt(changeLog.length, 0); + for (let i = 0; i < changeLog.length; i++) { + assert(changeLog[i].details.hasOwnProperty('counts') || + changeLog[i].details.hasOwnProperty('clonedBytes')); + } + } + + var ns = kDbName + '.fooHashed'; + assert.commandWorked(mongos.adminCommand({shardCollection: ns, key: {_id: 'hashed'}})); + + var aChunk = mongos.getDB('config').chunks.findOne({_id: RegExp(ns), shard: shard0}); + assert(aChunk); + + // Assert counts field exists in the changelog entry for moveChunk.commit + assert.commandWorked( + mongos.adminCommand({moveChunk: ns, bounds: [aChunk.min, aChunk.max], to: shard1})); + assertCountsInChangelog(); + + mongos.getDB(kDbName).fooHashed.drop(); + + st.stop(); +})();
\ No newline at end of file diff --git a/jstests/sharding/printShardingStatus.js b/jstests/sharding/printShardingStatus.js index 85330311fd4..cdc36999e51 100644 --- a/jstests/sharding/printShardingStatus.js +++ b/jstests/sharding/printShardingStatus.js @@ -7,6 +7,8 @@ var st = new ShardingTest({shards: 1, mongos: 2, config: 1, other: {smallfiles: true}}); + var standalone = MongoRunner.runMongod(); + var mongos = st.s0; var admin = mongos.getDB("admin"); @@ -85,9 +87,10 @@ testBasicVerboseOnly(outputVerbose); // Take a copy of the config db, in order to test the harder-to-setup cases below. + // Copy into a standalone to also test running printShardingStatus() against a config dump. // TODO: Replace this manual copy with copydb once SERVER-13080 is fixed. var config = mongos.getDB("config"); - var configCopy = mongos.getDB("configCopy"); + var configCopy = standalone.getDB("configCopy"); config.getCollectionInfos().forEach(function(c) { // Create collection with options. assert.commandWorked(configCopy.createCollection(c.name, c.options)); @@ -140,11 +143,11 @@ configCopy.mongos.remove({}); var output = grabStatusOutput(configCopy, false); - assertPresentInOutput(output, "most recently active mongoses:\n\tnone", "no mongoses"); + assertPresentInOutput(output, "most recently active mongoses:\n none", "no mongoses"); var output = grabStatusOutput(configCopy, true); assertPresentInOutput( - output, "most recently active mongoses:\n\tnone", "no mongoses (verbose)"); + output, "most recently active mongoses:\n none", "no mongoses (verbose)"); assert(mongos.getDB(dbName).dropDatabase()); @@ -237,5 +240,7 @@ assert(mongos.getDB("test").dropDatabase()); + MongoRunner.stopMongod(standalone); + st.stop(); })(); diff --git a/jstests/sharding/shard_existing_coll_chunk_count.js b/jstests/sharding/shard_existing_coll_chunk_count.js new file mode 100644 index 00000000000..60145fef712 --- /dev/null +++ b/jstests/sharding/shard_existing_coll_chunk_count.js @@ -0,0 +1,165 @@ +/** + * This test confirms that after sharding a collection with some pre-existing data, + * the resulting chunks aren't auto-split too aggressively. + */ +(function() { + 'use strict'; + + var s = new ShardingTest({ + name: "shard_existing_coll_chunk_count", + shards: 1, + mongos: 1, + other: {enableAutoSplit: true}, + }); + + assert.commandWorked(s.s.adminCommand({enablesharding: "test"})); + + var collNum = 0; + var overhead = Object.bsonsize({_id: ObjectId(), i: 1, pad: ""}); + + var getNumberChunks = function(ns) { + return s.configRS.getPrimary().getDB("config").getCollection("chunks").count({ns}); + }; + + var runCase = function(opts) { + // Expected options. + assert.gte(opts.docSize, 0); + assert.gte(opts.stages.length, 2); + + // Compute padding. + if (opts.docSize < overhead) { + var pad = ""; + } else { + var pad = (new Array(opts.docSize - overhead + 1)).join(' '); + } + + collNum++; + var db = s.getDB("test"); + var collName = "coll" + collNum; + var coll = db.getCollection(collName); + var i = 0; + var limit = 0; + var stageNum = 0; + var stage = opts.stages[stageNum]; + + // Insert initial docs. + var bulk = coll.initializeUnorderedBulkOp(); + limit += stage.numDocsToInsert; + for (; i < limit; i++) { + bulk.insert({i, pad}); + } + assert.writeOK(bulk.execute()); + + // Create shard key index. + assert.commandWorked(coll.createIndex({i: 1})); + + // Shard collection. + assert.commandWorked(s.s.adminCommand({shardcollection: coll.getFullName(), key: {i: 1}})); + + // Confirm initial number of chunks. + var numChunks = getNumberChunks(coll.getFullName()); + assert.eq(numChunks, + stage.expectedNumChunks, + 'in ' + coll.getFullName() + ' expected ' + stage.expectedNumChunks + + ' initial chunks, but found ' + numChunks + '\nopts: ' + tojson(opts) + + '\nchunks:\n' + s.getChunksString(coll.getFullName())); + + // Do the rest of the stages. + for (stageNum = 1; stageNum < opts.stages.length; stageNum++) { + stage = opts.stages[stageNum]; + + // Insert the later docs (one at a time, to maximise the autosplit effects). + limit += stage.numDocsToInsert; + for (; i < limit; i++) { + coll.insert({i, pad}); + } + + // Confirm number of chunks for this stage. + var numChunks = getNumberChunks(coll.getFullName()); + assert.eq(numChunks, + stage.expectedNumChunks, + 'in ' + coll.getFullName() + ' expected ' + stage.expectedNumChunks + + ' chunks for stage ' + stageNum + ', but found ' + numChunks + + '\nopts: ' + tojson(opts) + '\nchunks:\n' + + s.getChunksString(coll.getFullName())); + } + }; + + // Original problematic case. + runCase({ + docSize: 0, + stages: [ + {numDocsToInsert: 20000, expectedNumChunks: 1}, + {numDocsToInsert: 7, expectedNumChunks: 1}, + {numDocsToInsert: 1000, expectedNumChunks: 1}, + ], + }); + + // Original problematic case (worse). + runCase({ + docSize: 0, + stages: [ + {numDocsToInsert: 90000, expectedNumChunks: 1}, + {numDocsToInsert: 7, expectedNumChunks: 1}, + {numDocsToInsert: 1000, expectedNumChunks: 1}, + ], + }); + + // Pathological case #1. + runCase({ + docSize: 522, + stages: [ + {numDocsToInsert: 8191, expectedNumChunks: 1}, + {numDocsToInsert: 2, expectedNumChunks: 1}, + {numDocsToInsert: 1000, expectedNumChunks: 1}, + ], + }); + + // Pathological case #2. + runCase({ + docSize: 522, + stages: [ + {numDocsToInsert: 8192, expectedNumChunks: 1}, + {numDocsToInsert: 8192, expectedNumChunks: 1}, + ], + }); + + // Lower chunksize to 1MB, and restart the mongos for it to take. + assert.writeOK( + s.getDB("config").getCollection("settings").update({_id: "chunksize"}, {$set: {value: 1}}, { + upsert: true + })); + s.restartMongos(0); + + // Original problematic case, scaled down to smaller chunksize. + runCase({ + docSize: 0, + stages: [ + {numDocsToInsert: 10000, expectedNumChunks: 1}, + {numDocsToInsert: 10, expectedNumChunks: 1}, + {numDocsToInsert: 20, expectedNumChunks: 1}, + {numDocsToInsert: 40, expectedNumChunks: 1}, + {numDocsToInsert: 1000, expectedNumChunks: 1}, + ], + }); + + // Docs just smaller than half chunk size. + runCase({ + docSize: 510 * 1024, + stages: [ + {numDocsToInsert: 10, expectedNumChunks: 6}, + {numDocsToInsert: 10, expectedNumChunks: 12}, + ], + }); + + // Docs just larger than half chunk size. + runCase({ + docSize: 514 * 1024, + stages: [ + {numDocsToInsert: 10, expectedNumChunks: 10}, + {numDocsToInsert: 10, expectedNumChunks: 20}, + ], + }); + + s.stop(); +})(); diff --git a/jstests/sharding/shard_identity_rollback.js b/jstests/sharding/shard_identity_rollback.js index c7b6fedaacc..37cc8b10726 100644 --- a/jstests/sharding/shard_identity_rollback.js +++ b/jstests/sharding/shard_identity_rollback.js @@ -89,8 +89,9 @@ } }, function() { - var oldPriOplog = priConn.getDB('local').oplog.rs.find().sort({ts: -1}).toArray(); - var newPriOplog = newPriConn.getDB('local').oplog.rs.find().sort({ts: -1}).toArray(); + var oldPriOplog = priConn.getDB('local').oplog.rs.find().sort({$natural: -1}).toArray(); + var newPriOplog = + newPriConn.getDB('local').oplog.rs.find().sort({$natural: -1}).toArray(); return "timed out waiting for original primary to shut down after rollback. " + "Old primary oplog: " + tojson(oldPriOplog) + "; new primary oplog: " + tojson(newPriOplog); diff --git a/jstests/sharding/write_cmd_auto_split.js b/jstests/sharding/write_cmd_auto_split.js index 1cf9b5ab39a..95151b1e7e9 100644 --- a/jstests/sharding/write_cmd_auto_split.js +++ b/jstests/sharding/write_cmd_auto_split.js @@ -40,7 +40,7 @@ assert.eq(1, configDB.chunks.find().itcount()); - for (var x = 0; x < 1100; x++) { + for (var x = 0; x < 3100; x++) { assert.writeOK(testDB.runCommand({ update: 'update', updates: [{q: {x: x}, u: {x: x, v: doc1k}, upsert: true}], @@ -80,7 +80,7 @@ // Note: Estimated 'chunk size' tracked by mongos is initialized with a random value so // we are going to be conservative. - for (var x = 0; x < 1100; x += 400) { + for (var x = 0; x < 3100; x += 400) { var docs = []; for (var y = 0; y < 400; y++) { @@ -101,7 +101,7 @@ assert.eq(1, configDB.chunks.find().itcount()); - for (var x = 0; x < 1100; x += 400) { + for (var x = 0; x < 3100; x += 400) { var docs = []; for (var y = 0; y < 400; y++) { diff --git a/rpm/init.d-mongod b/rpm/init.d-mongod index 56539ef4d42..4e172b9f15c 100755 --- a/rpm/init.d-mongod +++ b/rpm/init.d-mongod @@ -64,6 +64,7 @@ start() ulimit -n 64000 ulimit -m unlimited ulimit -u 64000 + ulimit -l unlimited echo -n $"Starting mongod: " daemon --user "$MONGO_USER" --check $mongod "$NUMACTL $mongod $OPTIONS >/dev/null 2>&1" diff --git a/rpm/init.d-mongod.suse b/rpm/init.d-mongod.suse index fae1fb8b3f3..2216507a009 100644 --- a/rpm/init.d-mongod.suse +++ b/rpm/init.d-mongod.suse @@ -65,6 +65,7 @@ start() ulimit -n 64000 ulimit -m unlimited ulimit -u 64000 + ulimit -l unlimited echo -n "Starting mongod: " $NUMACTL /sbin/start_daemon -u "$MONGO_USER" -p "$PIDFILEPATH" $mongod $OPTIONS >/dev/null 2>&1 diff --git a/rpm/mongod.service b/rpm/mongod.service index 708bf663614..e4d48d08c1e 100644 --- a/rpm/mongod.service +++ b/rpm/mongod.service @@ -13,6 +13,7 @@ ExecStartPre=/usr/bin/chown mongod:mongod /var/run/mongodb ExecStartPre=/usr/bin/chmod 0755 /var/run/mongodb PermissionsStartOnly=true PIDFile=/var/run/mongodb/mongod.pid +Type=forking # file size LimitFSIZE=infinity # cpu time @@ -23,6 +24,8 @@ LimitAS=infinity LimitNOFILE=64000 # processes/threads LimitNPROC=64000 +# locked memory +LimitMEMLOCK=infinity # total threads (user+kernel) TasksMax=infinity TasksAccounting=false diff --git a/site_scons/site_tools/jstoh.py b/site_scons/site_tools/jstoh.py index 62424a6cd4f..dc90b324b28 100644 --- a/site_scons/site_tools/jstoh.py +++ b/site_scons/site_tools/jstoh.py @@ -8,8 +8,8 @@ def jsToHeader(target, source): h = [ '#include "mongo/base/string_data.h"', + '#include "mongo/scripting/engine.h"', 'namespace mongo {', - 'struct JSFile{ const char* name; const StringData& source; };', 'namespace JSFiles{', ] @@ -21,7 +21,7 @@ def jsToHeader(target, source): objname = os.path.split(filename)[1].split('.')[0] stringname = '_jscode_raw_' + objname - h.append('const char ' + stringname + "[] = {") + h.append('constexpr char ' + stringname + "[] = {") with open(filename, 'r') as f: for line in f: @@ -30,8 +30,8 @@ def jsToHeader(target, source): h.append("0};") # symbols aren't exported w/o this h.append('extern const JSFile %s;' % objname) - h.append('const JSFile %s = { "%s", StringData(%s) };' % - (objname, filename.replace('\\', '/'), stringname)) + h.append('const JSFile %s = { "%s", StringData(%s, sizeof(%s) - 1) };' % + (objname, filename.replace('\\', '/'), stringname, stringname)) h.append("} // namespace JSFiles") h.append("} // namespace mongo") diff --git a/src/mongo/SConscript b/src/mongo/SConscript index 2cfb2060e7f..1208446d70f 100644 --- a/src/mongo/SConscript +++ b/src/mongo/SConscript @@ -341,6 +341,7 @@ env.Install( LIBDEPS=[ 'db/conn_pool_options', 'db/commands/core', + 'db/ftdc/ftdc_mongos', 'db/mongodandmongos', 's/client/sharding_connection_hook', 's/commands/cluster_commands', @@ -409,12 +410,17 @@ if not has_option('noshell') and usemozjs: mongo_shell = shellEnv.Program( "mongo", ["shell/dbshell.cpp"] + env.WindowsResourceFile("shell/shell.rc"), - LIBDEPS=["$BUILD_DIR/third_party/shim_pcrecpp", - "shell_core", - "db/server_options_core", - "client/clientdriver", - "$BUILD_DIR/mongo/util/password", - ]) + LIBDEPS=[ + "$BUILD_DIR/third_party/shim_pcrecpp", + "shell_core", + "db/server_options_core", + "client/clientdriver", + "$BUILD_DIR/mongo/util/password", + ], + LIBDEPS_PRIVATE=[ + "$BUILD_DIR/mongo/client/connection_string", + ] + ) shellEnv.Install( '#/', mongo_shell ) else: diff --git a/src/mongo/base/error_codes.err b/src/mongo/base/error_codes.err index c8def6bd695..98d41461fce 100644 --- a/src/mongo/base/error_codes.err +++ b/src/mongo/base/error_codes.err @@ -202,6 +202,12 @@ error_code("CannotBuildIndexKeys", 201) error_code("NetworkInterfaceExceededTimeLimit", 202) error_code("TooManyLocks", 208) error_code("UpdateOperationFailed", 218) +error_code("FTDCPathNotSet", 219) +error_code("FTDCPathAlreadySet", 220) +error_code("ProducerConsumerQueueBatchTooLarge", 247) +error_code("ProducerConsumerQueueEndClosed", 248) + +# Error codes 4000-8999 are reserved. # Non-sequential error codes (for compatibility only) error_code("SocketException", 9001) diff --git a/src/mongo/base/parse_number_test.cpp b/src/mongo/base/parse_number_test.cpp index 0ed768a7dd3..76b1018af21 100644 --- a/src/mongo/base/parse_number_test.cpp +++ b/src/mongo/base/parse_number_test.cpp @@ -290,9 +290,9 @@ TEST(Double, TestParsingNormal) { // not parseable by the Windows SDK libc or the Solaris libc in the mode we build. // See SERVER-14131. - ASSERT_PARSES(double, "0xff", 0xff); - ASSERT_PARSES(double, "-0xff", -0xff); - ASSERT_PARSES(double, "0xabcab.defdefP-10", 0xabcab.defdefP-10); + ASSERT_PARSES(double, "0xff", 255); + ASSERT_PARSES(double, "-0xff", -255); + ASSERT_PARSES(double, "0xabcab.defdefP-10", 687.16784283419838); #endif } diff --git a/src/mongo/base/validate_locale.cpp b/src/mongo/base/validate_locale.cpp index ffb7190c804..b51eac53edb 100644 --- a/src/mongo/base/validate_locale.cpp +++ b/src/mongo/base/validate_locale.cpp @@ -32,6 +32,7 @@ #include <locale> #include "mongo/base/init.h" +#include "mongo/util/mongoutils/str.h" namespace mongo { @@ -40,13 +41,15 @@ MONGO_INITIALIZER_GENERAL(ValidateLocale, MONGO_NO_PREREQUISITES, MONGO_DEFAULT_ try { // Validate that boost can correctly load the user's locale boost::filesystem::path("/").has_root_directory(); - } catch (const std::runtime_error&) { - return Status(ErrorCodes::BadValue, - "Invalid or no user locale set." + } catch (const std::runtime_error& e) { + return Status( + ErrorCodes::BadValue, + str::stream() + << "Invalid or no user locale set. " #ifndef _WIN32 - " Please ensure LANG and/or LC_* environment variables are set correctly." + << " Please ensure LANG and/or LC_* environment variables are set correctly. " #endif - ); + << e.what()); } #ifdef _WIN32 diff --git a/src/mongo/bson/bson_comparator_interface_base.h b/src/mongo/bson/bson_comparator_interface_base.h index f70bec7b872..29d80916f72 100644 --- a/src/mongo/bson/bson_comparator_interface_base.h +++ b/src/mongo/bson/bson_comparator_interface_base.h @@ -28,9 +28,11 @@ #pragma once +#include <boost/container/flat_set.hpp> #include <initializer_list> #include <map> #include <set> +#include <vector> #include "mongo/base/disallow_copying.h" #include "mongo/base/string_data_comparator_interface.h" @@ -51,6 +53,9 @@ class BSONComparatorInterfaceBase { MONGO_DISALLOW_COPYING(BSONComparatorInterfaceBase); public: + BSONComparatorInterfaceBase(BSONComparatorInterfaceBase&& other) = default; + BSONComparatorInterfaceBase& operator=(BSONComparatorInterfaceBase&& other) = default; + /** * A deferred comparison between two objects of type T, which can be converted into a boolean * via the evaluate() method. @@ -122,6 +127,8 @@ public: using Set = std::set<T, LessThan>; + using FlatSet = boost::container::flat_set<T, LessThan>; + using UnorderedSet = stdx::unordered_set<T, Hasher, EqualTo>; template <typename ValueType> @@ -207,6 +214,10 @@ protected: return Set(init, LessThan(this)); } + FlatSet makeFlatSet(const std::vector<T>& elements) const { + return FlatSet(elements.begin(), elements.end(), LessThan(this)); + } + UnorderedSet makeUnorderedSet(std::initializer_list<T> init = {}) const { return UnorderedSet(init, 0, Hasher(this), EqualTo(this)); } diff --git a/src/mongo/bson/bsonelement_comparator_interface.h b/src/mongo/bson/bsonelement_comparator_interface.h index c6223a2d352..d69397a68db 100644 --- a/src/mongo/bson/bsonelement_comparator_interface.h +++ b/src/mongo/bson/bsonelement_comparator_interface.h @@ -33,6 +33,9 @@ namespace mongo { +typedef std::set<BSONElement, BSONElementCmpWithoutField> BSONElementSet; +typedef std::multiset<BSONElement, BSONElementCmpWithoutField> BSONElementMultiSet; + /** * A BSONElement::ComparatorInterface is an abstract class for comparing BSONElement objects. Usage * for comparing two BSON elements, 'lhs' and 'rhs', where 'comparator' is an instance of a class @@ -61,6 +64,14 @@ public: } /** + * Constructs a BSONEltFlatSet whose equivalence classes are given by this comparator. This + * comparator must outlive the returned set. + */ + FlatSet makeBSONEltFlatSet(const std::vector<BSONElement>& elements) const { + return makeFlatSet(elements); + } + + /** * Constructs a BSONEltUnorderedSet whose equivalence classes are given by this * comparator. This comparator must outlive the returned set. */ @@ -91,6 +102,8 @@ public: using BSONEltSet = BSONComparatorInterfaceBase<BSONElement>::Set; +using BSONEltFlatSet = BSONComparatorInterfaceBase<BSONElement>::FlatSet; + using BSONEltUnorderedSet = BSONComparatorInterfaceBase<BSONElement>::UnorderedSet; template <typename ValueType> diff --git a/src/mongo/bson/bsonobj.cpp b/src/mongo/bson/bsonobj.cpp index d9af2c706a9..bc0045f5681 100644 --- a/src/mongo/bson/bsonobj.cpp +++ b/src/mongo/bson/bsonobj.cpp @@ -33,6 +33,7 @@ #include "mongo/base/data_range.h" #include "mongo/bson/bson_validate.h" +#include "mongo/bson/bsonelement_comparator_interface.h" #include "mongo/db/json.h" #include "mongo/util/allocator.h" #include "mongo/util/hex.h" diff --git a/src/mongo/bson/bsonobj.h b/src/mongo/bson/bsonobj.h index 22ab6ff182f..0036fea6518 100644 --- a/src/mongo/bson/bsonobj.h +++ b/src/mongo/bson/bsonobj.h @@ -42,7 +42,6 @@ #include "mongo/base/string_data_comparator_interface.h" #include "mongo/bson/bson_comparator_interface_base.h" #include "mongo/bson/bsonelement.h" -#include "mongo/bson/bsonelement_comparator_interface.h" #include "mongo/bson/bsontypes.h" #include "mongo/bson/oid.h" #include "mongo/bson/timestamp.h" @@ -53,9 +52,6 @@ namespace mongo { -typedef std::set<BSONElement, BSONElementCmpWithoutField> BSONElementSet; -typedef std::multiset<BSONElement, BSONElementCmpWithoutField> BSONElementMSet; - /** C++ representation of a "BSON" object -- that is, an extended JSON-style object in a binary representation. diff --git a/src/mongo/bson/util/builder.h b/src/mongo/bson/util/builder.h index b959c59236e..ba6945e3fbc 100644 --- a/src/mongo/bson/util/builder.h +++ b/src/mongo/bson/util/builder.h @@ -320,15 +320,16 @@ private: } /* "slow" portion of 'grow()' */ void NOINLINE_DECL grow_reallocate(int minSize) { + if (minSize > BufferMaxSize) { + std::stringstream ss; + ss << "BufBuilder attempted to grow() to " << minSize << " bytes, past the 64MB limit."; + msgasserted(13548, ss.str().c_str()); + } + int a = 64; while (a < minSize) a = a * 2; - if (a > BufferMaxSize) { - std::stringstream ss; - ss << "BufBuilder attempted to grow() to " << a << " bytes, past the 64MB limit."; - msgasserted(13548, ss.str().c_str()); - } _buf.realloc(a); size = a; } diff --git a/src/mongo/client/connection_string.h b/src/mongo/client/connection_string.h index e8fc8eb2637..2e888b56dad 100644 --- a/src/mongo/client/connection_string.h +++ b/src/mongo/client/connection_string.h @@ -28,11 +28,13 @@ #pragma once +#include <sstream> #include <string> #include <vector> #include "mongo/base/status_with.h" #include "mongo/base/string_data.h" +#include "mongo/bson/util/builder.h" #include "mongo/stdx/mutex.h" #include "mongo/util/assert_util.h" #include "mongo/util/net/hostandport.h" @@ -157,6 +159,10 @@ public: return _string < other._string; } + + friend std::ostream& operator<<(std::ostream&, const ConnectionString&); + friend StringBuilder& operator<<(StringBuilder&, const ConnectionString&); + private: /** * Creates a SET connection string with the specified set name and servers. @@ -179,4 +185,15 @@ private: static stdx::mutex _connectHookMutex; static ConnectionHook* _connectHook; }; + +inline std::ostream& operator<<(std::ostream& ss, const ConnectionString& cs) { + ss << cs._string; + return ss; +} + +inline StringBuilder& operator<<(StringBuilder& sb, const ConnectionString& cs) { + sb << cs._string; + return sb; +} + } // namespace mongo diff --git a/src/mongo/client/fetcher.cpp b/src/mongo/client/fetcher.cpp index 741efe9afab..4918bbf76c2 100644 --- a/src/mongo/client/fetcher.cpp +++ b/src/mongo/client/fetcher.cpp @@ -171,7 +171,8 @@ Fetcher::Fetcher(executor::TaskExecutor* executor, const BSONObj& findCmdObj, const CallbackFn& work, const BSONObj& metadata, - Milliseconds timeout, + Milliseconds findNetworkTimeout, + Milliseconds getMoreNetworkTimeout, std::unique_ptr<RemoteCommandRetryScheduler::RetryPolicy> firstCommandRetryPolicy) : _executor(executor), _source(source), @@ -179,10 +180,11 @@ Fetcher::Fetcher(executor::TaskExecutor* executor, _cmdObj(findCmdObj.getOwned()), _metadata(metadata.getOwned()), _work(work), - _timeout(timeout), + _findNetworkTimeout(findNetworkTimeout), + _getMoreNetworkTimeout(getMoreNetworkTimeout), _firstRemoteCommandScheduler( _executor, - RemoteCommandRequest(_source, _dbname, _cmdObj, _metadata, nullptr, _timeout), + RemoteCommandRequest(_source, _dbname, _cmdObj, _metadata, nullptr, _findNetworkTimeout), stdx::bind(&Fetcher::_callback, this, stdx::placeholders::_1, kFirstBatchFieldName), std::move(firstCommandRetryPolicy)) { uassert(ErrorCodes::BadValue, "callback function cannot be null", work); @@ -204,10 +206,6 @@ BSONObj Fetcher::getMetadataObject() const { return _metadata; } -Milliseconds Fetcher::getTimeout() const { - return _timeout; -} - std::string Fetcher::toString() const { return getDiagnosticString(); } @@ -221,7 +219,8 @@ std::string Fetcher::getDiagnosticString() const { output << " query: " << _cmdObj; output << " query metadata: " << _metadata; output << " active: " << _isActive_inlock(); - output << " timeout: " << _timeout; + output << " findNetworkTimeout: " << _findNetworkTimeout; + output << " getMoreNetworkTimeout: " << _getMoreNetworkTimeout; output << " shutting down?: " << _isShuttingDown_inlock(); output << " first: " << _first; output << " firstCommandScheduler: " << _firstRemoteCommandScheduler.toString(); @@ -316,7 +315,8 @@ Status Fetcher::_scheduleGetMore(const BSONObj& cmdObj) { } StatusWith<executor::TaskExecutor::CallbackHandle> scheduleResult = _executor->scheduleRemoteCommand( - RemoteCommandRequest(_source, _dbname, cmdObj, _metadata, nullptr, _timeout), + RemoteCommandRequest( + _source, _dbname, cmdObj, _metadata, nullptr, _getMoreNetworkTimeout), stdx::bind(&Fetcher::_callback, this, stdx::placeholders::_1, kNextBatchFieldName)); if (!scheduleResult.isOK()) { diff --git a/src/mongo/client/fetcher.h b/src/mongo/client/fetcher.h index 050ac50f249..3af4c6f329d 100644 --- a/src/mongo/client/fetcher.h +++ b/src/mongo/client/fetcher.h @@ -130,7 +130,8 @@ public: const BSONObj& cmdObj, const CallbackFn& work, const BSONObj& metadata = rpc::ServerSelectionMetadata(true, boost::none).toBSON(), - Milliseconds timeout = RemoteCommandRequest::kNoTimeout, + Milliseconds findNetworkTimeout = RemoteCommandRequest::kNoTimeout, + Milliseconds getMoreNetworkTimeout = RemoteCommandRequest::kNoTimeout, std::unique_ptr<RemoteCommandRetryScheduler::RetryPolicy> firstCommandRetryPolicy = RemoteCommandRetryScheduler::makeNoRetryPolicy()); @@ -152,11 +153,6 @@ public: BSONObj getMetadataObject() const; /** - * Returns timeout for remote commands to complete. - */ - Milliseconds getTimeout() const; - - /** * Returns diagnostic information. */ std::string getDiagnosticString() const; @@ -260,7 +256,8 @@ private: executor::TaskExecutor::CallbackHandle _getMoreCallbackHandle; // Socket timeout - Milliseconds _timeout; + Milliseconds _findNetworkTimeout; + Milliseconds _getMoreNetworkTimeout; // First remote command scheduler. RemoteCommandRetryScheduler _firstRemoteCommandScheduler; diff --git a/src/mongo/client/fetcher_test.cpp b/src/mongo/client/fetcher_test.cpp index 00dae071d3a..66fb026c6b6 100644 --- a/src/mongo/client/fetcher_test.cpp +++ b/src/mongo/client/fetcher_test.cpp @@ -242,6 +242,7 @@ TEST_F(FetcherTest, InvalidConstruction) { unreachableCallback, rpc::makeEmptyMetadata(), RemoteCommandRequest::kNoTimeout, + RemoteCommandRequest::kNoTimeout, std::unique_ptr<RemoteCommandRetryScheduler::RetryPolicy>()), UserException, ErrorCodes::BadValue, @@ -273,7 +274,6 @@ TEST_F(FetcherTest, RemoteCommandRequestShouldContainCommandParametersPassedToCo ASSERT_EQUALS(source, fetcher->getSource()); ASSERT_BSONOBJ_EQ(findCmdObj, fetcher->getCommandObject()); ASSERT_BSONOBJ_EQ(metadataObj, fetcher->getMetadataObject()); - ASSERT_EQUALS(timeout, fetcher->getTimeout()); ASSERT_OK(fetcher->schedule()); @@ -284,6 +284,7 @@ TEST_F(FetcherTest, RemoteCommandRequestShouldContainCommandParametersPassedToCo ASSERT_TRUE(net->hasReadyRequests()); auto noi = net->getNextReadyRequest(); request = noi->getRequest(); + ASSERT_EQUALS(timeout, request.timeout); } ASSERT_EQUALS(source, request.target); @@ -1048,6 +1049,7 @@ TEST_F(FetcherTest, FetcherAppliesRetryPolicyToFirstCommandButNotToGetMoreReques makeCallback(), rpc::makeEmptyMetadata(), executor::RemoteCommandRequest::kNoTimeout, + executor::RemoteCommandRequest::kNoTimeout, std::move(policy)); callbackHook = appendGetMoreRequest; diff --git a/src/mongo/client/mongo_uri.cpp b/src/mongo/client/mongo_uri.cpp index e737479d05b..e2356881290 100644 --- a/src/mongo/client/mongo_uri.cpp +++ b/src/mongo/client/mongo_uri.cpp @@ -32,131 +32,323 @@ #include "mongo/client/mongo_uri.h" +#include <utility> + #include "mongo/base/status_with.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/client/dbclientinterface.h" #include "mongo/client/sasl_client_authenticate.h" +#include "mongo/db/namespace_string.h" +#include "mongo/util/hex.h" #include "mongo/util/mongoutils/str.h" -#include "mongo/util/password_digest.h" #include <boost/algorithm/string/case_conv.hpp> #include <boost/algorithm/string/classification.hpp> +#include <boost/algorithm/string/find_iterator.hpp> #include <boost/algorithm/string/predicate.hpp> -#include <boost/algorithm/string/split.hpp> -#include <boost/regex.hpp> + +namespace { +constexpr std::array<char, 16> hexits{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; +const mongo::StringData kURIPrefix{"mongodb://"}; +} + +/** + * RFC 3986 Section 2.1 - Percent Encoding + * + * Encode data elements in a way which will allow them to be embedded + * into a mongodb:// URI safely. + */ +void mongo::uriEncode(std::ostream& ss, StringData toEncode, StringData passthrough) { + for (const auto& c : toEncode) { + if ((c == '-') || (c == '_') || (c == '.') || (c == '~') || isalnum(c) || + (passthrough.find(c) != std::string::npos)) { + ss << c; + } else { + // Encoding anything not included in section 2.3 "Unreserved characters" + ss << '%' << hexits[(c >> 4) & 0xF] << hexits[c & 0xF]; + } + } +} + +mongo::StatusWith<std::string> mongo::uriDecode(StringData toDecode) { + StringBuilder out; + for (size_t i = 0; i < toDecode.size(); ++i) { + const auto c = toDecode[i]; + if (c == '%') { + if (i + 2 > toDecode.size()) { + return Status(ErrorCodes::FailedToParse, + "Encountered partial escape sequence at end of string"); + } + out << fromHex(toDecode.substr(i + 1, 2)); + i += 2; + } else { + out << c; + } + } + return out.str(); +} namespace mongo { namespace { -const char kMongoDBURL[] = - // scheme: non-capturing - "mongodb://" - // credentials: two inner captures for user and password - "(?:([^:]+)(?::([^@]+))?@)?" +/** + * Helper Method for MongoURI::parse() to split a string into exactly 2 pieces by a char delimeter + */ +std::pair<StringData, StringData> partitionForward(StringData str, const char c) { + const auto delim = str.find(c); + if (delim == std::string::npos) { + return {str, StringData()}; + } + return {str.substr(0, delim), str.substr(delim + 1)}; +} - // servers: grabs all host:port or UNIX socket names - "((?:[^\\/]+|/.+\\.sock)(?:,(?:[^\\/]+|/.+\\.sock))*)" +/** + * Helper method for MongoURI::parse() to split a string into exactly 2 pieces by a char delimiter + * searching backward from the end of the string. + */ +std::pair<StringData, StringData> partitionBackward(StringData str, const char c) { + const auto delim = str.rfind(c); + if (delim == std::string::npos) { + return {StringData(), str}; + } + return {str.substr(0, delim), str.substr(delim + 1)}; +} - // database and options are grouped together - "(?:/" +/** + * Breakout method for parsing application/x-www-form-urlencoded option pairs + * + * foo=bar&baz=qux&... + */ +StatusWith<MongoURI::OptionsMap> parseOptions(StringData options, StringData url) { + MongoURI::OptionsMap ret; + if (options.empty()) { + return ret; + } - // database: matches anything but the chars that cannot be part of a MongoDB database name which - // are (in order) - forward slash, back slash, dot, space, double-quote, dollar sign, asterisk, - // less than, greater than, colon, pipe, question mark. - "([^/\\\\\\.\\ \"\\$*<>:\\|\\?]*)?" + if (options.find('?') != std::string::npos) { + return Status(ErrorCodes::FailedToParse, + str::stream() + << "URI Cannot Contain multiple questions marks for mongodb:// URL: " + << url); + } - // options - "(?:\\?([^&=?]+=[^&=?]+(?:&[^&=?]+=[^&=?]+)*))?" + const auto optionsStr = options.toString(); + for (auto i = + boost::make_split_iterator(optionsStr, boost::first_finder("&", boost::is_iequal())); + i != std::remove_reference<decltype((i))>::type{}; + ++i) { + const auto opt = boost::copy_range<std::string>(*i); + if (opt.empty()) { + return Status(ErrorCodes::FailedToParse, + str::stream() + << "Missing a key/value pair in the options for mongodb:// URL: " + << url); + } - // close db/options group - ")?"; + const auto kvPair = partitionForward(opt, '='); + const auto keyRaw = kvPair.first; + if (keyRaw.empty()) { + return Status( + ErrorCodes::FailedToParse, + str::stream() + << "Missing a key for key/value pair in the options for mongodb:// URL: " + << url); + } + const auto key = uriDecode(keyRaw); + if (!key.isOK()) { + return Status( + ErrorCodes::FailedToParse, + str::stream() << "Key '" << keyRaw + << "' in options cannot properly be URL decoded for mongodb:// URL: " + << url); + } + const auto valRaw = kvPair.second; + if (valRaw.empty()) { + return Status(ErrorCodes::FailedToParse, + str::stream() << "Missing value for key '" << keyRaw + << "' in the options for mongodb:// URL: " + << url); + } + const auto val = uriDecode(valRaw); + if (!val.isOK()) { + return Status( + ErrorCodes::FailedToParse, + str::stream() << "Value '" << valRaw << "' for key '" << keyRaw + << "' in options cannot properly be URL decoded for mongodb:// URL: " + << url); + } -} // namespace + ret[key.getValue()] = val.getValue(); + } + + return ret; +} +} // namespace StatusWith<MongoURI> MongoURI::parse(const std::string& url) { - if (!boost::algorithm::starts_with(url, "mongodb://")) { - auto cs_status = ConnectionString::parse(url); + const StringData urlSD(url); + + // 1. Validate and remove the scheme prefix mongodb:// + if (!urlSD.startsWith(kURIPrefix)) { + const auto cs_status = ConnectionString::parse(url); if (!cs_status.isOK()) { return cs_status.getStatus(); } - return MongoURI(cs_status.getValue()); } + const auto uriWithoutPrefix = urlSD.substr(kURIPrefix.size()); + + // 2. Split the string by the first, unescaped / (if any), yielding: + // split[0]: User information and host identifers + // split[1]: Auth database and connection options + const auto userAndDb = partitionForward(uriWithoutPrefix, '/'); + const auto userAndHostInfo = userAndDb.first; + const auto databaseAndOptions = userAndDb.second; + + // 2.b Make sure that there are no question marks in the left side of the / + // as any options after the ? must still have the / delimeter + if (databaseAndOptions.empty() && userAndHostInfo.find('?') != std::string::npos) { + return Status( + ErrorCodes::FailedToParse, + str::stream() + << "URI must contain slash delimeter between hosts and options for mongodb:// URL: " + << url); + } - const boost::regex mongoUrlRe(kMongoDBURL); - - boost::smatch matches; - if (!boost::regex_match(url, matches, mongoUrlRe)) { + // 3. Split the user information and host identifiers string by the last, unescaped @, yielding: + // split[0]: User information + // split[1]: Host identifiers; + const auto userAndHost = partitionBackward(userAndHostInfo, '@'); + const auto userInfo = userAndHost.first; + const auto hostIdentifiers = userAndHost.second; + + // 4. Validate, split (if applicable), and URL decode the user information, yielding: + // split[0] = username + // split[1] = password + const auto userAndPass = partitionForward(userInfo, ':'); + const auto usernameSD = userAndPass.first; + const auto passwordSD = userAndPass.second; + + const auto containsColonOrAt = [](StringData str) { + return (str.find(':') != std::string::npos) || (str.find('@') != std::string::npos); + }; + + if (containsColonOrAt(usernameSD)) { return Status(ErrorCodes::FailedToParse, - str::stream() << "Failed to parse mongodb:// URL: " << url); + str::stream() << "Username must be URL Encoded for mongodb:// URL: " << url); } - - // We have the whole input plus 5 top level captures (user, password, host, db, options). - invariant(matches.size() == 6); - - if (!matches[3].matched) { - return Status(ErrorCodes::FailedToParse, "No server(s) specified"); + if (containsColonOrAt(passwordSD)) { + return Status(ErrorCodes::FailedToParse, + str::stream() << "Password must be URL Encoded for mongodb:// URL: " << url); } - std::map<std::string, std::string> options; + // Get the username and make sure it did not fail to decode + const auto usernameWithStatus = uriDecode(usernameSD); + if (!usernameWithStatus.isOK()) { + return Status( + ErrorCodes::FailedToParse, + str::stream() << "Username cannot properly be URL decoded for mongodb:// URL: " << url); + } + const auto username = usernameWithStatus.getValue(); + + // Get the password and make sure it did not fail to decode + const auto passwordWithStatus = uriDecode(passwordSD); + if (!passwordWithStatus.isOK()) + return Status( + ErrorCodes::FailedToParse, + str::stream() << "Password cannot properly be URL decoded for mongodb:// URL: " << url); + const auto password = passwordWithStatus.getValue(); + + // 5. Validate, split, and URL decode the host identifiers. + const auto hostIdentifiersStr = hostIdentifiers.toString(); + std::vector<HostAndPort> servers; + for (auto i = boost::make_split_iterator(hostIdentifiersStr, + boost::first_finder(",", boost::is_iequal())); + i != std::remove_reference<decltype((i))>::type{}; + ++i) { + const auto hostWithStatus = uriDecode(boost::copy_range<std::string>(*i)); + if (!hostWithStatus.isOK()) { + return Status( + ErrorCodes::FailedToParse, + str::stream() << "Host cannot properly be URL decoded for mongodb:// URL: " << url); + } - if (matches[5].matched) { - const std::string optionsMatch = matches[5].str(); + const auto host = hostWithStatus.getValue(); + if (host.empty()) { + continue; + } - std::vector<boost::iterator_range<std::string::const_iterator>> optionsTokens; - boost::algorithm::split(optionsTokens, optionsMatch, boost::algorithm::is_any_of("=&")); + if ((host.find('/') != std::string::npos) && !StringData(host).endsWith(".sock")) { + return Status( + ErrorCodes::FailedToParse, + str::stream() << "'" << host << "' in '" << url + << "' appears to be a unix socket, but does not end in '.sock'"); + } - if (optionsTokens.size() % 2 != 0) { - return Status(ErrorCodes::FailedToParse, - str::stream() - << "Missing a key or value in the options for mongodb:// URL: " - << url); - ; + const auto statusHostAndPort = HostAndPort::parse(host); + if (!statusHostAndPort.isOK()) { + return statusHostAndPort.getStatus(); } + servers.push_back(statusHostAndPort.getValue()); + } + if (servers.empty()) { + return Status(ErrorCodes::FailedToParse, "No server(s) specified"); + } - for (size_t i = 0; i != optionsTokens.size(); i = i + 2) - options[std::string(optionsTokens[i].begin(), optionsTokens[i].end())] = - std::string(optionsTokens[i + 1].begin(), optionsTokens[i + 1].end()); + // 6. Split the auth database and connection options string by the first, unescaped ?, yielding: + // split[0] = auth database + // split[1] = connection options + const auto dbAndOpts = partitionForward(databaseAndOptions, '?'); + const auto databaseSD = dbAndOpts.first; + const auto connectionOptions = dbAndOpts.second; + const auto databaseWithStatus = uriDecode(databaseSD); + if (!databaseWithStatus.isOK()) { + return Status(ErrorCodes::FailedToParse, + str::stream() + << "Database name cannot properly be URL decoded for mongodb:// URL: " + << url); + } + const auto database = databaseWithStatus.getValue(); + + // 7. Validate the database contains no prohibited characters + // Prohibited characters: + // slash ("/"), backslash ("\"), space (" "), double-quote ("""), or dollar sign ("$") + // period (".") is also prohibited, but drivers MAY allow periods + if (!database.empty() && + !NamespaceString::validDBName(database, + NamespaceString::DollarInDbNameBehavior::Disallow)) { + return Status(ErrorCodes::FailedToParse, + str::stream() + << "Database name cannot have reserved characters for mongodb:// URL: " + << url); } - OptionsMap::const_iterator optIter; + // 8. Validate, split, and URL decode the connection options + const auto optsWith = parseOptions(connectionOptions, url); + if (!optsWith.isOK()) { + return optsWith.getStatus(); + } + const auto options = optsWith.getValue(); // If a replica set option was specified, store it in the 'setName' field. - bool haveSetName; + const auto optIter = options.find("replicaSet"); std::string setName; - if ((haveSetName = ((optIter = options.find("replicaSet")) != options.end()))) { + if (optIter != options.end()) { setName = optIter->second; + invariant(!setName.empty()); } - std::vector<HostAndPort> servers; - - { - std::vector<std::string> servers_split; - const std::string serversStr = matches[3].str(); - boost::algorithm::split(servers_split, serversStr, boost::is_any_of(",")); - for (auto&& s : servers_split) { - auto statusHostAndPort = HostAndPort::parse(s); - if (!statusHostAndPort.isOK()) { - return statusHostAndPort.getStatus(); - } - - servers.push_back(statusHostAndPort.getValue()); - } - } - - const bool direct = !haveSetName && (servers.size() == 1); - - if (!direct && setName.empty()) { + if ((servers.size() > 1) && setName.empty()) { return Status(ErrorCodes::FailedToParse, "Cannot list multiple servers in URL without 'replicaSet' option"); } ConnectionString cs( - direct ? ConnectionString::MASTER : ConnectionString::SET, servers, setName); - return MongoURI( - std::move(cs), matches[1].str(), matches[2].str(), matches[4].str(), std::move(options)); + setName.empty() ? ConnectionString::MASTER : ConnectionString::SET, servers, setName); + return MongoURI(std::move(cs), username, password, database, std::move(options)); } } // namespace mongo diff --git a/src/mongo/client/mongo_uri.h b/src/mongo/client/mongo_uri.h index 7b6364761d5..f7a7c4aeb81 100644 --- a/src/mongo/client/mongo_uri.h +++ b/src/mongo/client/mongo_uri.h @@ -29,12 +29,14 @@ #pragma once #include <map> +#include <sstream> #include <string> #include <vector> #include "mongo/base/status_with.h" #include "mongo/base/string_data.h" #include "mongo/bson/bsonobj.h" +#include "mongo/bson/util/builder.h" #include "mongo/client/connection_string.h" #include "mongo/stdx/mutex.h" #include "mongo/util/assert_util.h" @@ -43,12 +45,38 @@ namespace mongo { /** + * Encode a string for embedding in a URI. + * Replaces reserved bytes with %xx sequences. + * + * Optionally allows passthrough characters to remain unescaped. + */ +void uriEncode(std::ostream& ss, StringData str, StringData passthrough = ""_sd); +inline std::string uriEncode(StringData str, StringData passthrough = ""_sd) { + std::ostringstream ss; + uriEncode(ss, str, passthrough); + return ss.str(); +} + +/** + * Decode a URI encoded string. + * Replaces + and %xx sequences with their original byte. + */ +StatusWith<std::string> uriDecode(StringData str); + +/** * MongoURI handles parsing of URIs for mongodb, and falls back to old-style * ConnectionString parsing. It's used primarily by the shell. * It parses URIs with the following format: * * mongodb://[usr:pwd@]host1[:port1]...[,hostN[:portN]]][/[db][?options]] * + * While this format is generally RFC 3986 compliant, some exceptions do exist: + * 1. The 'host' field, as defined by section 3.2.2 is expanded in the following ways: + * a. Multiple hosts may be specified as a comma separated list. + * b. Hosts may take the form of absolute paths for unix domain sockets. + * i. Sockets must end in the suffix '.sock' + * 2. The 'fragment' field, as defined by section 3.5 is not permitted. + * * For a complete list of URI string options, see * https://wiki.mongodb.com/display/DH/Connection+String+Format * @@ -125,6 +153,9 @@ public: MongoURI() = default; + friend std::ostream& operator<<(std::ostream&, const MongoURI&); + friend StringBuilder& operator<<(StringBuilder&, const MongoURI&); + private: MongoURI(ConnectionString connectString, const std::string& user, @@ -146,4 +177,14 @@ private: OptionsMap _options; }; +inline std::ostream& operator<<(std::ostream& ss, const MongoURI& uri) { + ss << uri._connectString; + return ss; +} + +inline StringBuilder& operator<<(StringBuilder& sb, const MongoURI& uri) { + sb << uri._connectString; + return sb; +} + } // namespace mongo diff --git a/src/mongo/client/mongo_uri_test.cpp b/src/mongo/client/mongo_uri_test.cpp index 6a5f6c9882a..c32b1c0b5c5 100644 --- a/src/mongo/client/mongo_uri_test.cpp +++ b/src/mongo/client/mongo_uri_test.cpp @@ -28,11 +28,17 @@ #include "mongo/platform/basic.h" -#include "mongo/client/mongo_uri.h" +#include <fstream> #include "mongo/base/string_data.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/bsontypes.h" +#include "mongo/bson/json.h" +#include "mongo/client/mongo_uri.h" #include "mongo/unittest/unittest.h" +#include <boost/filesystem/operations.hpp> + namespace { using mongo::MongoURI; @@ -60,16 +66,77 @@ const URITestCase validCases[] = { {"mongodb://user@127.0.0.1", "user", "", kMaster, "", 1, 0, ""}, - {"mongodb://127.0.0.1/dbName?foo=a&c=b", "", "", kMaster, "", 1, 2, "dbName"}, - {"mongodb://localhost/?foo=bar", "", "", kMaster, "", 1, 1, ""}, + {"mongodb://localhost,/?foo=bar", "", "", kMaster, "", 1, 1, ""}, + {"mongodb://user:pwd@127.0.0.1:1234", "user", "pwd", kMaster, "", 1, 0, ""}, {"mongodb://user@127.0.0.1:1234", "user", "", kMaster, "", 1, 0, ""}, {"mongodb://127.0.0.1:1234/dbName?foo=a&c=b", "", "", kMaster, "", 1, 2, "dbName"}, + {"mongodb://127.0.0.1/dbName?foo=a&c=b", "", "", kMaster, "", 1, 2, "dbName"}, + + {"mongodb://user:pwd@127.0.0.1,/dbName?foo=a&c=b", "user", "pwd", kMaster, "", 1, 2, "dbName"}, + + {"mongodb://user:pwd@127.0.0.1,127.0.0.2/dbname?a=b&replicaSet=replName", + "user", + "pwd", + kSet, + "replName", + 2, + 2, + "dbname"}, + + {"mongodb://needs%20encoding%25%23!%3C%3E:pwd@127.0.0.1,127.0.0.2/" + "dbname?a=b&replicaSet=replName", + "needs encoding%#!<>", + "pwd", + kSet, + "replName", + 2, + 2, + "dbname"}, + + {"mongodb://needs%20encoding%25%23!%3C%3E:pwd@127.0.0.1,127.0.0.2/" + "db@name?a=b&replicaSet=replName", + "needs encoding%#!<>", + "pwd", + kSet, + "replName", + 2, + 2, + "db@name"}, + + {"mongodb://user:needs%20encoding%25%23!%3C%3E@127.0.0.1,127.0.0.2/" + "dbname?a=b&replicaSet=replName", + "user", + "needs encoding%#!<>", + kSet, + "replName", + 2, + 2, + "dbname"}, + + {"mongodb://user:pwd@127.0.0.1,127.0.0.2/dbname?a=b&replicaSet=needs%20encoding%25%23!%3C%3E", + "user", + "pwd", + kSet, + "needs encoding%#!<>", + 2, + 2, + "dbname"}, + + {"mongodb://user:pwd@127.0.0.1,127.0.0.2/needsencoding%40hello?a=b&replicaSet=replName", + "user", + "pwd", + kSet, + "replName", + 2, + 2, + "needsencoding@hello"}, + {"mongodb://user:pwd@127.0.0.1,127.0.0.2/?replicaSet=replName", "user", "pwd", @@ -259,21 +326,27 @@ const URITestCase validCases[] = { 1, 2, ""}, - {"mongodb:///tmp/mongodb-27017.sock", "", "", kMaster, "", 1, 0, ""}, - {"mongodb:///tmp/mongodb-27017.sock,/tmp/mongodb-27018.sock/?replicaSet=replName", + {"mongodb://%2Ftmp%2Fmongodb-27017.sock", "", "", kMaster, "", 1, 0, ""}, + + {"mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/?replicaSet=replName", "", "", kSet, "replName", 2, 1, - ""}}; + ""}, +}; const InvalidURITestCase invalidCases[] = { // No host. {"mongodb://"}, + {"mongodb://usr:pwd@/dbname?a=b"}, + + // Username and password must be encoded (cannot have ':' or '@') + {"mongodb://usr:pwd:@127.0.0.1/dbName?foo=a&c=b"}, // Needs a "/" after the hosts and before the options. {"mongodb://localhost:27017,localhost:27018?replicaSet=missingSlash"}, @@ -282,32 +355,109 @@ const InvalidURITestCase invalidCases[] = { {"mongodb://localhost:27017localhost:27018"}, // Domain sockets have to end in ".sock". - {"mongodb:///notareal/domainsock"}, + {"mongodb://%2Fnotareal%2Fdomainsock"}, + + // Database name cannot contain slash ("/"), backslash ("\"), space (" "), double-quote ("""), + // or dollar sign ("$") + {"mongodb://usr:pwd@localhost:27017/db$name?a=b"}, + {"mongodb://usr:pwd@localhost:27017/db/name?a=b"}, + {"mongodb://usr:pwd@localhost:27017/db\\name?a=b"}, + {"mongodb://usr:pwd@localhost:27017/db name?a=b"}, + {"mongodb://usr:pwd@localhost:27017/db\"name?a=b"}, + + // Options must have a key + {"mongodb://usr:pwd@localhost:27017/dbname?=b"}, + + // Cannot skip a key value pair + {"mongodb://usr:pwd@localhost:27017/dbname?a=b&&b=c"}, + + // Multiple Unix domain sockets and auth DB resembling a socket (relative path) + {"mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin.sock?replicaSet=replName"}, + + // Multiple Unix domain sockets with auth DB resembling a path (relative path) + {"mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin.shoe?replicaSet=replName"}, + + // Multiple Unix domain sockets and auth DB resembling a socket (absolute path) + {"mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/" + "admin.sock?replicaSet=replName"}, + + // Multiple Unix domain sockets with auth DB resembling a path (absolute path) + {"mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/" + "admin.shoe?replicaSet=replName"}, + + // Missing value in key value pair for options + {"mongodb://127.0.0.1:1234/dbName?foo=a&c=b&d"}, + {"mongodb://127.0.0.1:1234/dbName?foo=a&c=b&d="}, + {"mongodb://127.0.0.1:1234/dbName?foo=a&h=&c=b&d=6"}, + {"mongodb://127.0.0.1:1234/dbName?foo=a&h&c=b&d=6"}, + + // Missing a hostname, or unparsable hostname(s) + {"mongodb://,/dbName"}, + {"mongodb://user:pwd@,/dbName"}, + {"mongodb://localhost:1234:5678/dbName"}, // Options can't have multiple question marks. Only one. {"mongodb://localhost:27017/?foo=a?c=b&d=e?asdf=foo"}, + + // Missing a key in key value pair for options + {"mongodb://127.0.0.1:1234/dbName?foo=a&=d&c=b"}, + + // Missing an entire key-value pair + {"mongodb://127.0.0.1:1234/dbName?foo=a&&c=b"}, }; +// Helper Method to take a filename for a json file and return the array of tests inside of it +mongo::BSONObj getBsonFromJsonFile(std::string fileName) { + boost::filesystem::path directoryPath = boost::filesystem::current_path(); + boost::filesystem::path filePath(directoryPath / "src" / "mongo" / "client" / + "mongo_uri_tests" / fileName); + std::string filename(filePath.string()); + std::ifstream infile(filename.c_str()); + std::string data((std::istreambuf_iterator<char>(infile)), std::istreambuf_iterator<char>()); + if (data.empty()) { + // v3.4 test infra is unable to see these extra files. + return mongo::BSONObj(); + } + mongo::BSONObj obj = mongo::fromjson(data); + ASSERT_TRUE(obj.valid(mongo::BSONVersion::kLatest)); + ASSERT_TRUE(obj.hasField("tests")); + mongo::BSONObj arr = obj.getField("tests").embeddedObject().getOwned(); + ASSERT_TRUE(arr.couldBeArray()); + return arr; +} + +// Helper method to take a BSONElement and either extract its string or return an empty string +std::string returnStringFromElementOrNull(mongo::BSONElement element) { + ASSERT_TRUE(!element.eoo()); + if (element.type() == mongo::jstNULL) { + return std::string(); + } + ASSERT_EQ(element.type(), mongo::String); + return element.String(); +} + +// Helper method to take a valid test case, parse() it, and assure the output is correct +void testValidURIFormat(URITestCase testCase) { + mongo::unittest::log() << "Testing URI: " << testCase.URI << '\n'; + std::string errMsg; + const auto cs_status = MongoURI::parse(testCase.URI); + ASSERT_OK(cs_status); + auto result = cs_status.getValue(); + ASSERT_EQ(testCase.uname, result.getUser()); + ASSERT_EQ(testCase.password, result.getPassword()); + ASSERT_EQ(testCase.type, result.type()); + ASSERT_EQ(testCase.setname, result.getSetName()); + ASSERT_EQ(testCase.numservers, result.getServers().size()); + ASSERT_EQ(testCase.numOptions, result.getOptions().size()); + ASSERT_EQ(testCase.database, result.getDatabase()); +} + TEST(MongoURI, GoodTrickyURIs) { const size_t numCases = sizeof(validCases) / sizeof(validCases[0]); for (size_t i = 0; i != numCases; ++i) { const URITestCase testCase = validCases[i]; - mongo::unittest::log() << "Testing URI: " << testCase.URI << '\n'; - std::string errMsg; - auto cs_status = MongoURI::parse(testCase.URI); - if (!cs_status.getStatus().toString().empty()) { - mongo::unittest::log() << "error with uri: " << cs_status.getStatus().toString(); - } - ASSERT_TRUE(cs_status.isOK()); - auto result = cs_status.getValue(); - ASSERT_EQ(testCase.uname, result.getUser()); - ASSERT_EQ(testCase.password, result.getPassword()); - ASSERT_EQ(testCase.type, result.type()); - ASSERT_EQ(testCase.setname, result.getSetName()); - ASSERT_EQ(testCase.numservers, result.getServers().size()); - ASSERT_EQ(testCase.numOptions, result.getOptions().size()); - ASSERT_EQ(testCase.database, result.getDatabase()); + testValidURIFormat(testCase); } } @@ -318,7 +468,7 @@ TEST(MongoURI, InvalidURIs) { const InvalidURITestCase testCase = invalidCases[i]; mongo::unittest::log() << "Testing URI: " << testCase.URI << '\n'; auto cs_status = MongoURI::parse(testCase.URI); - ASSERT_FALSE(cs_status.isOK()); + ASSERT_NOT_OK(cs_status); } } @@ -357,4 +507,100 @@ TEST(MongoURI, CloneURIForServer) { ASSERT_EQ(clonedURIOptions.at("ssl"), "true"); } +/** + * These tests come from the Mongo Uri Specifications for the drivers found at: + * https://github.com/mongodb/specifications/tree/master/source/connection-string/tests + * They have been slighly altered as the Drivers specification is slighly different + * from the server specification. + */ +TEST(MongoURI, specTests) { + const std::string files[] = { + "mongo-uri-valid-auth.json", + "mongo-uri-options.json", + "mongo-uri-unix-sockets-absolute.json", + "mongo-uri-unix-sockets-relative.json", + "mongo-uri-warnings.json", + "mongo-uri-host-identifiers.json", + "mongo-uri-invalid.json", + }; + + for (const auto& file : files) { + const auto testBson = getBsonFromJsonFile(file); + + for (const auto& testElement : testBson) { + ASSERT_EQ(testElement.type(), mongo::Object); + const auto test = testElement.Obj(); + + // First extract the valid field and the uri field + const auto validDoc = test.getField("valid"); + ASSERT_FALSE(validDoc.eoo()); + ASSERT_TRUE(validDoc.isBoolean()); + const auto valid = validDoc.Bool(); + + const auto uriDoc = test.getField("uri"); + ASSERT_FALSE(uriDoc.eoo()); + ASSERT_EQ(uriDoc.type(), mongo::String); + const auto uri = uriDoc.String(); + + if (!valid) { + // This uri string is invalid --> parse the uri and ensure it fails + const InvalidURITestCase testCase = {uri}; + mongo::unittest::log() << "Testing URI: " << testCase.URI << '\n'; + auto cs_status = MongoURI::parse(testCase.URI); + ASSERT_NOT_OK(cs_status); + } else { + // This uri is valid -- > parse the remaining necessary fields + + // parse the auth options + std::string database, username, password; + + const auto auth = test.getField("auth"); + ASSERT_FALSE(auth.eoo()); + if (auth.type() != mongo::jstNULL) { + ASSERT_EQ(auth.type(), mongo::Object); + const auto authObj = auth.embeddedObject(); + database = returnStringFromElementOrNull(authObj.getField("db")); + username = returnStringFromElementOrNull(authObj.getField("username")); + password = returnStringFromElementOrNull(authObj.getField("password")); + } + + // parse the hosts + const auto hosts = test.getField("hosts"); + ASSERT_FALSE(hosts.eoo()); + ASSERT_EQ(hosts.type(), mongo::Array); + const auto numHosts = static_cast<size_t>(hosts.Obj().nFields()); + + // parse the options + mongo::ConnectionString::ConnectionType connectionType = kMaster; + size_t numOptions = 0; + std::string setName; + const auto optionsElement = test.getField("options"); + ASSERT_FALSE(optionsElement.eoo()); + if (optionsElement.type() != mongo::jstNULL) { + ASSERT_EQ(optionsElement.type(), mongo::Object); + const auto optionsObj = optionsElement.Obj(); + numOptions = optionsObj.nFields(); + const auto replsetElement = optionsObj.getField("replicaSet"); + if (!replsetElement.eoo()) { + ASSERT_EQ(replsetElement.type(), mongo::String); + setName = replsetElement.String(); + connectionType = kSet; + } + } + + // Create the URITestCase abnd + const URITestCase testCase = {uri, + username, + password, + connectionType, + setName, + numHosts, + numOptions, + database}; + testValidURIFormat(testCase); + } + } + } +} + } // namespace diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-host-identifiers.json b/src/mongo/client/mongo_uri_tests/mongo-uri-host-identifiers.json new file mode 100644 index 00000000000..8a1fda580a8 --- /dev/null +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-host-identifiers.json @@ -0,0 +1,203 @@ +{ + "tests": [ + { + "description": "Single IPv4 host without port", + "uri": "mongodb://127.0.0.1", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "ipv4", + "host": "127.0.0.1", + "port": null + } + ], + "auth": null, + "options": null + }, + { + "description": "Single IPv4 host with port", + "uri": "mongodb://127.0.0.1:27018", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "ipv4", + "host": "127.0.0.1", + "port": 27018 + } + ], + "auth": null, + "options": null + }, + { + "description": "Single IP literal host without port", + "uri": "mongodb://[::1]", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "ip_literal", + "host": "::1", + "port": null + } + ], + "auth": null, + "options": null + }, + { + "description": "Single IP literal host with port", + "uri": "mongodb://[::1]:27019", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "ip_literal", + "host": "::1", + "port": 27019 + } + ], + "auth": null, + "options": null + }, + { + "description": "Single hostname without port", + "uri": "mongodb://example.com", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "hostname", + "host": "example.com", + "port": null + } + ], + "auth": null, + "options": null + }, + { + "description": "Single hostname with port", + "uri": "mongodb://example.com:27020", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "hostname", + "host": "example.com", + "port": 27020 + } + ], + "auth": null, + "options": null + }, + { + "description": "Single hostname (resembling IPv4) without port", + "uri": "mongodb://256.0.0.1", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "hostname", + "host": "256.0.0.1", + "port": null + } + ], + "auth": null, + "options": null + }, + { + "description": "Multiple hosts (mixed formats)", + "uri": "mongodb://127.0.0.1,[::1]:27018,example.com:27019", + "valid": false, + "warning": false, + "hosts": [ + { + "type": "ipv4", + "host": "127.0.0.1", + "port": null + }, + { + "type": "ip_literal", + "host": "::1", + "port": 27018 + }, + { + "type": "hostname", + "host": "example.com", + "port": 27019 + } + ], + "auth": null, + "options": null + }, + { + "description": "Multiple hosts (mixed formats)", + "uri": "mongodb://127.0.0.1,[::1]:27018,example.com:27019/?replicaSet=replset", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "ipv4", + "host": "127.0.0.1", + "port": null + }, + { + "type": "ip_literal", + "host": "::1", + "port": 27018 + }, + { + "type": "hostname", + "host": "example.com", + "port": 27019 + } + ], + "auth": null, + "options": { + "replicaSet": "replset" + } + }, + { + "description": "UTF-8 hosts", + "uri": "mongodb://bücher.example.com,umläut.example.com/", + "valid": false, + "warning": false, + "hosts": [ + { + "type": "hostname", + "host": "bücher.example.com", + "port": null + }, + { + "type": "hostname", + "host": "umläut.example.com", + "port": null + } + ], + "auth": null, + "options": null + }, + { + "description": "UTF-8 hosts", + "uri": "mongodb://bücher.example.com,umläut.example.com/?replicaSet=replset", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "hostname", + "host": "bücher.example.com", + "port": null + }, + { + "type": "hostname", + "host": "umläut.example.com", + "port": null + } + ], + "auth": null, + "options": { + "replicaSet": "replset" + } + } + ] +}
\ No newline at end of file diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-invalid.json b/src/mongo/client/mongo_uri_tests/mongo-uri-invalid.json new file mode 100644 index 00000000000..3ee82bd535e --- /dev/null +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-invalid.json @@ -0,0 +1,247 @@ +{ + "tests": [ + { + "auth": null, + "description": "Empty string", + "hosts": null, + "options": null, + "uri": "", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Missing host", + "hosts": null, + "options": null, + "uri": "mongodb://", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Double colon in host identifier", + "hosts": null, + "options": null, + "uri": "mongodb://localhost::27017", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Double colon in host identifier and trailing slash", + "hosts": null, + "options": null, + "uri": "mongodb://localhost::27017/", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Double colon in host identifier with missing host and port", + "hosts": null, + "options": null, + "uri": "mongodb://::", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Double colon in host identifier with missing port", + "hosts": null, + "options": null, + "uri": "mongodb://localhost,localhost::", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Double colon in host identifier and second host", + "hosts": null, + "options": null, + "uri": "mongodb://localhost::27017,abc", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (negative number) with hostname", + "hosts": null, + "options": null, + "uri": "mongodb://localhost:-1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (zero) with hostname", + "hosts": null, + "options": null, + "uri": "mongodb://localhost:0/", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (positive number) with hostname", + "hosts": null, + "options": null, + "uri": "mongodb://localhost:65536", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (positive number) with hostname and trailing slash", + "hosts": null, + "options": null, + "uri": "mongodb://localhost:65536/", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (non-numeric string) with hostname", + "hosts": null, + "options": null, + "uri": "mongodb://localhost:foo", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (negative number) with IP literal", + "hosts": null, + "options": null, + "uri": "mongodb://[::1]:-1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (zero) with IP literal", + "hosts": null, + "options": null, + "uri": "mongodb://[::1]:0/", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (positive number) with IP literal", + "hosts": null, + "options": null, + "uri": "mongodb://[::1]:65536", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (positive number) with IP literal and trailing slash", + "hosts": null, + "options": null, + "uri": "mongodb://[::1]:65536/", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (non-numeric string) with IP literal", + "hosts": null, + "options": null, + "uri": "mongodb://[::1]:foo", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Missing delimiting slash between hosts and options", + "hosts": null, + "options": null, + "uri": "mongodb://example.com?w=1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Incomplete key value pair for option", + "hosts": null, + "options": null, + "uri": "mongodb://example.com/?w", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Username with password containing an unescaped colon", + "hosts": null, + "options": null, + "uri": "mongodb://alice:foo:bar@127.0.0.1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Username with password containing an unescaped colon", + "hosts": null, + "options": null, + "uri": "mongodb://alice:foo:bar@127.0.0.1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Username containing an unescaped at-sign", + "hosts": null, + "options": null, + "uri": "mongodb://alice@@127.0.0.1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Username with password containing an unescaped at-sign", + "hosts": null, + "options": null, + "uri": "mongodb://alice@foo:bar@127.0.0.1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Username containing an unescaped slash", + "hosts": null, + "options": null, + "uri": "mongodb://alice/@localhost/db", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Username containing unescaped slash with password", + "hosts": null, + "options": null, + "uri": "mongodb://alice/bob:foo@localhost/db", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Username with password containing an unescaped slash", + "hosts": null, + "options": null, + "uri": "mongodb://alice:foo/bar@localhost/db", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Host with unescaped slash", + "hosts": null, + "options": null, + "uri": "mongodb:///tmp/mongodb-27017.sock/", + "valid": false, + "warning": null + } + ] +} diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-options.json b/src/mongo/client/mongo_uri_tests/mongo-uri-options.json new file mode 100644 index 00000000000..4c2bded9e72 --- /dev/null +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-options.json @@ -0,0 +1,25 @@ +{ + "tests": [ + { + "description": "Option names are normalized to lowercase", + "uri": "mongodb://alice:secret@example.com/admin?AUTHMechanism=MONGODB-CR", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "hostname", + "host": "example.com", + "port": null + } + ], + "auth": { + "username": "alice", + "password": "secret", + "db": "admin" + }, + "options": { + "authmechanism": "MONGODB-CR" + } + } + ] +} diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-absolute.json b/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-absolute.json new file mode 100644 index 00000000000..d167c315bbc --- /dev/null +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-absolute.json @@ -0,0 +1,373 @@ +{ + "tests": [ + { + "auth": null, + "description": "Unix domain socket (absolute path with trailing slash)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock/", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket (absolute path without trailing slash)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket (absolute path with spaces in path)", + "hosts": [ + { + "host": "/tmp/ /mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2F %2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple Unix domain sockets (absolute paths)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock", + "valid": false, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (absolute path and ipv4)", + "hosts": [ + { + "host": "127.0.0.1", + "port": 27017, + "type": "ipv4" + }, + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://127.0.0.1:27017,%2Ftmp%2Fmongodb-27017.sock", + "valid": false, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (absolute path and hostname resembling relative path)", + "hosts": [ + { + "host": "mongodb-27017.sock", + "port": null, + "type": "hostname" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://mongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock", + "valid": false, + "warning": false + }, + { + "auth": null, + "description": "Multiple Unix domain sockets (absolute paths)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "replicaSet": "replset" + }, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/?replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (absolute path and ipv4)", + "hosts": [ + { + "host": "127.0.0.1", + "port": 27017, + "type": "ipv4" + }, + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "replicaSet": "replset" + }, + "uri": "mongodb://127.0.0.1:27017,%2Ftmp%2Fmongodb-27017.sock/?replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (absolute path and hostname resembling relative path)", + "hosts": [ + { + "host": "mongodb-27017.sock", + "port": null, + "type": "hostname" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "replicaSet": "replset" + }, + "uri": "mongodb://mongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/?replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "foo", + "username": "alice" + }, + "description": "Unix domain socket with auth database (absolute path)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://alice:foo@%2Ftmp%2Fmongodb-27017.sock/admin", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket with path resembling socket file (absolute path with trailing slash)", + "hosts": [ + { + "host": "/tmp/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fpath.to.sock%2Fmongodb-27017.sock/", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket with path resembling socket file (absolute path without trailing slash)", + "hosts": [ + { + "host": "/tmp/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fpath.to.sock%2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Unix domain socket with path resembling socket file and auth (absolute path)", + "hosts": [ + { + "host": "/tmp/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://bob:bar@%2Ftmp%2Fpath.to.sock%2Fmongodb-27017.sock/admin", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": null, + "username": null + }, + "description": "Multiple Unix domain sockets and auth DB (absolute path)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/admin", + "valid": false, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": null, + "username": null + }, + "description": "Multiple Unix domain sockets and auth DB (absolute path)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "replicaSet": "replset" + }, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/admin?replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Multiple Unix domain sockets with auth and query string (absolute path)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "w": 1 + }, + "uri": "mongodb://bob:bar@%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/admin?w=1", + "valid": false, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Multiple Unix domain sockets with auth and query string (absolute path)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "w": 1, + "replicaSet":"replset" + }, + "uri": "mongodb://bob:bar@%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/admin?w=1&replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Multiple Unix domain sockets with auth and query string (absolute path)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "w": 1, + "replicaSet":"replset" + }, + "uri": "mongodb://bob:bar@%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/admin?replicaSet=replset&w=1", + "valid": true, + "warning": false + } + ] +} diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-relative.json b/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-relative.json new file mode 100644 index 00000000000..8da45c4e1b9 --- /dev/null +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-relative.json @@ -0,0 +1,466 @@ +{ + "tests": [ + { + "auth": null, + "description": "Unix domain socket (relative path with trailing slash)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock/", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket (relative path without trailing slash)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket (relative path with spaces)", + "hosts": [ + { + "host": "rel/ /mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2F %2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple Unix domain sockets (relative paths)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock", + "valid": false, + "warning": false + }, + { + "auth": null, + "description": "Multiple Unix domain sockets (relative paths)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "replicaSet": "replset" + }, + "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/?replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple Unix domain sockets (relative and absolute paths)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock", + "valid": false, + "warning": false + }, + { + "auth": null, + "description": "Multiple Unix domain sockets (relative and absolute paths)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "replicaSet": "replset" + }, + "uri": "mongodb://rel%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/?replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (relative path and ipv4)", + "hosts": [ + { + "host": "127.0.0.1", + "port": 27017, + "type": "ipv4" + }, + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://127.0.0.1:27017,rel%2Fmongodb-27017.sock", + "valid": false, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (relative path and ipv4)", + "hosts": [ + { + "host": "127.0.0.1", + "port": 27017, + "type": "ipv4" + }, + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "replicaSet": "replset" + }, + "uri": "mongodb://127.0.0.1:27017,rel%2Fmongodb-27017.sock/?replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (relative path and hostname resembling relative path)", + "hosts": [ + { + "host": "mongodb-27017.sock", + "port": null, + "type": "hostname" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://mongodb-27017.sock,rel%2Fmongodb-27018.sock", + "valid": false, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (relative path and hostname resembling relative path)", + "hosts": [ + { + "host": "mongodb-27017.sock", + "port": null, + "type": "hostname" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "replicaSet": "replset" + }, + "uri": "mongodb://mongodb-27017.sock,rel%2Fmongodb-27018.sock/?replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "foo", + "username": "alice" + }, + "description": "Unix domain socket with auth database (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://alice:foo@rel%2Fmongodb-27017.sock/admin", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket with path resembling socket file (relative path with trailing slash)", + "hosts": [ + { + "host": "rel/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fpath.to.sock%2Fmongodb-27017.sock/", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket with path resembling socket file (relative path without trailing slash)", + "hosts": [ + { + "host": "rel/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fpath.to.sock%2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Unix domain socket with path resembling socket file and auth (relative path)", + "hosts": [ + { + "host": "rel/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://bob:bar@rel%2Fpath.to.sock%2Fmongodb-27017.sock/admin", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": null, + "username": null + }, + "description": "Multiple Unix domain sockets and auth DB resembling a socket (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin", + "valid": false, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": null, + "username": null + }, + "description": "Multiple Unix domain sockets and auth DB resembling a socket (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "replicaSet": "replset" + }, + "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin?replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": null, + "username": null + }, + "description": "Multiple Unix domain sockets with auth DB resembling a path (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin", + "valid": false, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": null, + "username": null + }, + "description": "Multiple Unix domain sockets with auth DB resembling a path (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "replicaSet": "replset" + }, + "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin?replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Multiple Unix domain sockets with auth and query string (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "w": 1 + }, + "uri": "mongodb://bob:bar@rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin?w=1", + "valid": false, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Multiple Unix domain sockets with auth and query string (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "w": 1, + "replicaSet": "replset" + }, + "uri": "mongodb://bob:bar@rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin?w=1&replicaSet=replset", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Multiple Unix domain sockets with auth and query string (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "w": 1, + "replicaSet": "replset", + "b": 4 + }, + "uri": "mongodb://bob:bar@rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin?w=1&replicaSet=replset&b=4", + "valid": true, + "warning": false + } + ] +} diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-valid-auth.json b/src/mongo/client/mongo_uri_tests/mongo-uri-valid-auth.json new file mode 100644 index 00000000000..a44236e7b62 --- /dev/null +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-valid-auth.json @@ -0,0 +1,311 @@ +{ + "tests": [ + { + "auth": { + "db": null, + "password": "foo", + "username": "alice" + }, + "description": "User info for single IPv4 host without database", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://alice:foo@127.0.0.1", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "test", + "password": "foo", + "username": "alice" + }, + "description": "User info for single IPv4 host with database", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://alice:foo@127.0.0.1/test", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": "bar", + "username": "bob" + }, + "description": "User info for single IP literal host without database", + "hosts": [ + { + "host": "::1", + "port": 27018, + "type": "ip_literal" + } + ], + "options": null, + "uri": "mongodb://bob:bar@[::1]:27018", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "User info for single IP literal host with database", + "hosts": [ + { + "host": "::1", + "port": 27018, + "type": "ip_literal" + } + ], + "options": null, + "uri": "mongodb://bob:bar@[::1]:27018/admin", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": "baz", + "username": "eve" + }, + "description": "User info for single hostname without database", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://eve:baz@example.com", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "db2", + "password": "baz", + "username": "eve" + }, + "description": "User info for single hostname with database", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://eve:baz@example.com/db2", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": "secret", + "username": "alice" + }, + "description": "User info for multiple hosts without database", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + }, + { + "host": "example.com", + "port": 27018, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://alice:secret@127.0.0.1,example.com:27018", + "valid": false, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "secret", + "username": "alice" + }, + "description": "User info for multiple hosts with database", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + }, + { + "host": "::1", + "port": 27019, + "type": "ip_literal" + } + ], + "options": null, + "uri": "mongodb://alice:secret@example.com,[::1]:27019/admin", + "valid": false, + "warning": false + }, + { + "auth": { + "db": null, + "password": null, + "username": "alice" + }, + "description": "Username without password", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://alice@127.0.0.1", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": "", + "username": "alice" + }, + "description": "Username with empty password", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://alice:@127.0.0.1", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "my=db", + "password": null, + "username": "@l:ce/=" + }, + "description": "Escaped username and database without password", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://%40l%3Ace%2F%3D@example.com/my%3Ddb", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin=", + "password": "fizzb@zz=", + "username": "$am" + }, + "description": "Escaped user info and database (MONGODB-CR)", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": { + "authmechanism": "MONGODB-CR" + }, + "uri": "mongodb://%24am:fizzb%40zz%3D@127.0.0.1/admin%3D?authMechanism=MONGODB-CR", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": null, + "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry" + }, + "description": "Escaped username (MONGODB-X509)", + "hosts": [ + { + "host": "localhost", + "port": null, + "type": "hostname" + } + ], + "options": { + "authmechanism": "MONGODB-X509" + }, + "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": "secret", + "username": "user@EXAMPLE.COM" + }, + "description": "Escaped username (GSSAPI)", + "hosts": [ + { + "host": "localhost", + "port": null, + "type": "hostname" + } + ], + "options": { + "authmechanism": "GSSAPI", + "authmechanismproperties": { + "CANONICALIZE_HOST_NAME": true, + "SERVICE_NAME": "other" + } + }, + "uri": "mongodb://user%40EXAMPLE.COM:secret@localhost/?authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true&authMechanism=GSSAPI", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "secret", + "username": "alice" + }, + "description": "At-signs in options aren't part of the userinfo", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": { + "replicaSet": "my@replicaset" + }, + "uri": "mongodb://alice:secret@example.com/admin?replicaSet=my@replicaset", + "valid": true, + "warning": false + } + ] +} diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-warnings.json b/src/mongo/client/mongo_uri_tests/mongo-uri-warnings.json new file mode 100644 index 00000000000..a04246847ab --- /dev/null +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-warnings.json @@ -0,0 +1,56 @@ +{ + "tests": [ + { + "description": "Unrecognized option keys are ignored", + "uri": "mongodb://example.com/?foo=bar", + "valid": true, + "warning": true, + "hosts": [ + { + "type": "hostname", + "host": "example.com", + "port": null + } + ], + "auth": null, + "options": { + "foo": "bar" + } + }, + { + "description": "Unsupported option values are ignored", + "uri": "mongodb://example.com/?fsync=ifPossible", + "valid": true, + "warning": true, + "hosts": [ + { + "type": "hostname", + "host": "example.com", + "port": null + } + ], + "auth": null, + "options": { + "fsync": "ifPossible" + } + }, + { + "description": "Deprecated (or unknown) options are ignored if replacement exists", + "uri": "mongodb://example.com/?wtimeout=5&wtimeoutMS=10", + "valid": true, + "warning": true, + "hosts": [ + { + "type": "hostname", + "host": "example.com", + "port": null + } + ], + "auth": null, + "options": { + "wtimeoutMS": 10, + "wtimeout": 5 + } + } + ] +}
\ No newline at end of file diff --git a/src/mongo/crypto/mechanism_scram.cpp b/src/mongo/crypto/mechanism_scram.cpp index 8cd59142d67..6a2757d383d 100644 --- a/src/mongo/crypto/mechanism_scram.cpp +++ b/src/mongo/crypto/mechanism_scram.cpp @@ -226,6 +226,10 @@ bool verifyClientProof(StringData clientProof, StringData storedKey, StringData SHA1Block computedStoredKey = SHA1Block::computeHash(clientSignature.data(), clientSignature.size()); + if (storedKey.size() != computedStoredKey.size()) { + return false; + } + return consttimeMemEqual(reinterpret_cast<const unsigned char*>(storedKey.rawData()), computedStoredKey.data(), computedStoredKey.size()); diff --git a/src/mongo/crypto/mechanism_scram.h b/src/mongo/crypto/mechanism_scram.h index b12cebbf2e1..6045f037078 100644 --- a/src/mongo/crypto/mechanism_scram.h +++ b/src/mongo/crypto/mechanism_scram.h @@ -53,15 +53,22 @@ const std::string serverKeyFieldName = "serverKey"; */ struct SCRAMPresecrets { SCRAMPresecrets(std::string hashedPassword, - std::vector<std::uint8_t> salt, - size_t iterationCount) + std::vector<std::uint8_t> salt_, + size_t iterationCount_) : hashedPassword(std::move(hashedPassword)), - salt(std::move(salt)), - iterationCount(iterationCount) {} + salt(std::move(salt_)), + iterationCount(iterationCount_) { + uassert(ErrorCodes::BadValue, + "Invalid salt for SCRAM mechanism", + salt.size() >= kSaltLengthMin); + } std::string hashedPassword; std::vector<std::uint8_t> salt; size_t iterationCount; + +private: + static const size_t kSaltLengthMin = 16; }; inline bool operator==(const SCRAMPresecrets& lhs, const SCRAMPresecrets& rhs) { diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript index e2be152f126..ded6480875c 100644 --- a/src/mongo/db/SConscript +++ b/src/mongo/db/SConscript @@ -445,6 +445,7 @@ env.Library( LIBDEPS=[ '$BUILD_DIR/mongo/db/serveronly', '$BUILD_DIR/mongo/util/options_parser/options_parser_init', + 'repl/replica_set_messages', 'server_options', ], ) @@ -766,9 +767,12 @@ env.Library( 'log_process_details.cpp', ], LIBDEPS=[ + 'repl/repl_coordinator_global', + 'repl/repl_coordinator_interface', + 'repl/replica_set_messages', + 'server_options', '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/util/net/network', - 'server_options', ], ) @@ -818,3 +822,4 @@ asioEnv.CppIntegrationTest( '$BUILD_DIR/mongo/executor/network_interface_asio_fixture', ], ) + diff --git a/src/mongo/db/auth/SConscript b/src/mongo/db/auth/SConscript index 792314035b5..fc85149de7f 100644 --- a/src/mongo/db/auth/SConscript +++ b/src/mongo/db/auth/SConscript @@ -138,6 +138,7 @@ env.Library('authmongod', ], LIBDEPS=[ 'authservercommon', + '$BUILD_DIR/mongo/db/catalog/index_key_validate', '$BUILD_DIR/mongo/db/db_raii', '$BUILD_DIR/mongo/db/dbdirectclient', '$BUILD_DIR/mongo/db/dbhelpers', diff --git a/src/mongo/db/auth/auth_index_d.cpp b/src/mongo/db/auth/auth_index_d.cpp index 427900c8377..47987ac40c4 100644 --- a/src/mongo/db/auth/auth_index_d.cpp +++ b/src/mongo/db/auth/auth_index_d.cpp @@ -34,15 +34,20 @@ #include "mongo/base/init.h" #include "mongo/base/status.h" +#include "mongo/client/index_spec.h" #include "mongo/db/auth/authorization_manager.h" #include "mongo/db/auth/authorization_manager_global.h" #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/index_catalog.h" +#include "mongo/db/catalog/index_create.h" +#include "mongo/db/catalog/index_key_validate.h" #include "mongo/db/client.h" -#include "mongo/db/client.h" +#include "mongo/db/concurrency/write_conflict_exception.h" +#include "mongo/db/curop.h" #include "mongo/db/db_raii.h" #include "mongo/db/index/index_descriptor.h" #include "mongo/db/jsobj.h" +#include "mongo/db/storage/storage_options.h" #include "mongo/util/assert_util.h" #include "mongo/util/log.h" @@ -58,6 +63,8 @@ BSONObj v3SystemUsersKeyPattern; BSONObj v3SystemRolesKeyPattern; std::string v3SystemUsersIndexName; std::string v3SystemRolesIndexName; +IndexSpec v3SystemUsersIndexSpec; +IndexSpec v3SystemRolesIndexSpec; MONGO_INITIALIZER(AuthIndexKeyPatterns)(InitializerContext*) { v1SystemUsersKeyPattern = BSON("user" << 1 << "userSource" << 1); @@ -76,13 +83,66 @@ MONGO_INITIALIZER(AuthIndexKeyPatterns)(InitializerContext*) { << AuthorizationManager::ROLE_DB_FIELD_NAME << "_1"); + v3SystemUsersIndexSpec.addKeys(v3SystemUsersKeyPattern); + v3SystemUsersIndexSpec.unique(); + v3SystemUsersIndexSpec.name(v3SystemUsersIndexName); + + v3SystemRolesIndexSpec.addKeys(v3SystemRolesKeyPattern); + v3SystemRolesIndexSpec.unique(); + v3SystemRolesIndexSpec.name(v3SystemRolesIndexName); + return Status::OK(); } +void generateSystemIndexForExistingCollection(OperationContext* opCtx, + Collection* collection, + const NamespaceString& ns, + const IndexSpec& spec) { + // Do not try and generate any system indexes in read only mode. + if (storageGlobalParams.readOnly) { + warning() << "Running in queryable backup mode. Unable to create authorization index on " + << ns; + return; + } + + try { + auto indexSpecStatus = index_key_validate::validateIndexSpec( + spec.toBSON(), ns, serverGlobalParams.featureCompatibility); + BSONObj indexSpec = fassertStatusOK(40452, indexSpecStatus); + + log() << "No authorization index detected on " << ns + << " collection. Attempting to recover by creating an index with spec: " << indexSpec; + + MultiIndexBlock indexer(opCtx, collection); + + MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN { + fassertStatusOK(40453, indexer.init(indexSpec)); + } + MONGO_WRITE_CONFLICT_RETRY_LOOP_END(opCtx, "authorization index regeneration", ns.ns()); + + fassertStatusOK(40454, indexer.insertAllDocumentsInCollection()); + + MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN { + WriteUnitOfWork wunit(opCtx); + + indexer.commit(); + + wunit.commit(); + } + MONGO_WRITE_CONFLICT_RETRY_LOOP_END(opCtx, "authorization index regeneration", ns.ns()); + + log() << "Authorization index construction on " << ns << " is complete"; + } catch (const DBException& e) { + severe() << "Failed to regenerate index for " << ns << ". Exception: " << e.what(); + throw; + } +} + } // namespace Status verifySystemIndexes(OperationContext* txn) { - const NamespaceString systemUsers = AuthorizationManager::usersCollectionNamespace; + const NamespaceString& systemUsers = AuthorizationManager::usersCollectionNamespace; + const NamespaceString& systemRoles = AuthorizationManager::rolesCollectionNamespace; // Make sure the old unique index from v2.4 on system.users doesn't exist. ScopedTransaction scopedXact(txn, MODE_IX); @@ -91,23 +151,54 @@ Status verifySystemIndexes(OperationContext* txn) { return Status::OK(); } - Collection* collection = autoDb.getDb()->getCollection(NamespaceString(systemUsers)); - if (!collection) { - return Status::OK(); + Collection* collection = autoDb.getDb()->getCollection(systemUsers); + if (collection) { + IndexCatalog* indexCatalog = collection->getIndexCatalog(); + invariant(indexCatalog); + + // Make sure the old unique index from v2.4 on system.users doesn't exist. + std::vector<IndexDescriptor*> indexes; + indexCatalog->findIndexesByKeyPattern(txn, v1SystemUsersKeyPattern, false, &indexes); + + if (!indexes.empty()) { + fassert(ErrorCodes::AmbiguousIndexKeyPattern, indexes.size() == 1); + return Status(ErrorCodes::AuthSchemaIncompatible, + "Old 2.4 style user index identified. " + "The authentication schema needs to be updated by " + "running authSchemaUpgrade on a 2.6 server."); + } + + // Ensure that system indexes exist for the user collection + indexCatalog->findIndexesByKeyPattern(txn, v3SystemUsersKeyPattern, false, &indexes); + if (indexes.empty()) { + try { + generateSystemIndexForExistingCollection( + txn, collection, systemUsers, v3SystemUsersIndexSpec); + } catch (...) { + return exceptionToStatus(); + } + } } - IndexCatalog* indexCatalog = collection->getIndexCatalog(); - std::vector<IndexDescriptor*> indexes; - indexCatalog->findIndexesByKeyPattern(txn, v1SystemUsersKeyPattern, false, &indexes); - - if (indexCatalog && !indexes.empty()) { - fassert(ErrorCodes::AmbiguousIndexKeyPattern, indexes.size() == 1); - return Status(ErrorCodes::AuthSchemaIncompatible, - "Old 2.4 style user index identified. " - "The authentication schema needs to be updated by " - "running authSchemaUpgrade on a 2.6 server."); + // Ensure that system indexes exist for the roles collection, if it exists. + collection = autoDb.getDb()->getCollection(systemRoles); + if (collection) { + IndexCatalog* indexCatalog = collection->getIndexCatalog(); + invariant(indexCatalog); + + std::vector<IndexDescriptor*> indexes; + indexCatalog->findIndexesByKeyPattern(txn, v3SystemRolesKeyPattern, false, &indexes); + if (indexes.empty()) { + try { + generateSystemIndexForExistingCollection( + txn, collection, systemRoles, v3SystemRolesIndexSpec); + } catch (...) { + return exceptionToStatus(); + } + } } + return Status::OK(); } @@ -115,19 +206,21 @@ void createSystemIndexes(OperationContext* txn, Collection* collection) { invariant(collection); const NamespaceString& ns = collection->ns(); if (ns == AuthorizationManager::usersCollectionNamespace) { - collection->getIndexCatalog()->createIndexOnEmptyCollection( - txn, - BSON("name" << v3SystemUsersIndexName << "ns" << collection->ns().ns() << "key" - << v3SystemUsersKeyPattern - << "unique" - << true)); + auto indexSpec = fassertStatusOK( + 40455, + index_key_validate::validateIndexSpec( + v3SystemUsersIndexSpec.toBSON(), ns, serverGlobalParams.featureCompatibility)); + + fassertStatusOK( + 40456, collection->getIndexCatalog()->createIndexOnEmptyCollection(txn, indexSpec)); } else if (ns == AuthorizationManager::rolesCollectionNamespace) { - collection->getIndexCatalog()->createIndexOnEmptyCollection( - txn, - BSON("name" << v3SystemRolesIndexName << "ns" << collection->ns().ns() << "key" - << v3SystemRolesKeyPattern - << "unique" - << true)); + auto indexSpec = fassertStatusOK( + 40457, + index_key_validate::validateIndexSpec( + v3SystemRolesIndexSpec.toBSON(), ns, serverGlobalParams.featureCompatibility)); + + fassertStatusOK( + 40458, collection->getIndexCatalog()->createIndexOnEmptyCollection(txn, indexSpec)); } } diff --git a/src/mongo/db/auth/sasl_scramsha1_server_conversation.cpp b/src/mongo/db/auth/sasl_scramsha1_server_conversation.cpp index f48ab9b8953..900d0b818fc 100644 --- a/src/mongo/db/auth/sasl_scramsha1_server_conversation.cpp +++ b/src/mongo/db/auth/sasl_scramsha1_server_conversation.cpp @@ -198,6 +198,12 @@ StatusWith<bool> SaslSCRAMSHA1ServerConversation::_firstStep(std::vector<string> _creds.scram.serverKey = scramCreds[scram::serverKeyFieldName].String(); } + if (!_creds.scram.isValid()) { + return Status(ErrorCodes::AuthenticationFailed, + "Unable to perform SCRAM-SHA-1 authentication for a user with missing " + "or invalid SCRAM credentials"); + } + // Generate server-first-message // Create text-based nonce as base64 encoding of a binary blob of length multiple of 3 const int nonceLenQWords = 3; @@ -280,6 +286,7 @@ StatusWith<bool> SaslSCRAMSHA1ServerConversation::_secondStep(const std::vector< // ClientSignature := HMAC(StoredKey, AuthMessage) // ClientKey := ClientSignature XOR ClientProof // ServerSignature := HMAC(ServerKey, AuthMessage) + invariant(_creds.scram.isValid()); if (!scram::verifyClientProof( base64::decode(clientProof), base64::decode(_creds.scram.storedKey), _authMessage)) { diff --git a/src/mongo/db/auth/user.h b/src/mongo/db/auth/user.h index d4aea7e442b..cdef6e52ddd 100644 --- a/src/mongo/db/auth/user.h +++ b/src/mongo/db/auth/user.h @@ -66,6 +66,17 @@ public: std::string salt; std::string serverKey; std::string storedKey; + + bool isValid() const { + // 160bit -> 20octets -> * 4/3 -> 26.667 -> padded to 28 + const size_t kEncodedSHA1Length = 28; + // 128bit -> 16octets -> * 4/3 -> 21.333 -> padded to 24 + const size_t kEncodedSaltLength = 24; + + return (salt.size() == kEncodedSaltLength) && + (serverKey.size() == kEncodedSHA1Length) && + (storedKey.size() == kEncodedSHA1Length); + } }; struct CredentialData { CredentialData() : password(""), scram(), isExternal(false) {} diff --git a/src/mongo/db/auth/user_document_parser_test.cpp b/src/mongo/db/auth/user_document_parser_test.cpp index 273eaff86f5..9ab688750d8 100644 --- a/src/mongo/db/auth/user_document_parser_test.cpp +++ b/src/mongo/db/auth/user_document_parser_test.cpp @@ -434,6 +434,7 @@ TEST_F(V2UserDocumentParsing, V2CredentialExtraction) { << BSON("MONGODB-CR" << "a")))); ASSERT(user->getCredentials().password == "a"); + ASSERT(!user->getCredentials().scram.isValid()); ASSERT(!user->getCredentials().isExternal); // Credentials are {external:true if users's db is $external @@ -446,6 +447,7 @@ TEST_F(V2UserDocumentParsing, V2CredentialExtraction) { << "credentials" << BSON("external" << true)))); ASSERT(user->getCredentials().password.empty()); + ASSERT(!user->getCredentials().scram.isValid()); ASSERT(user->getCredentials().isExternal); } diff --git a/src/mongo/db/bson/dotted_path_support.cpp b/src/mongo/db/bson/dotted_path_support.cpp index 1a9fda88501..31a92a5c54c 100644 --- a/src/mongo/db/bson/dotted_path_support.cpp +++ b/src/mongo/db/bson/dotted_path_support.cpp @@ -176,7 +176,7 @@ void extractAllElementsAlongPath(const BSONObj& obj, void extractAllElementsAlongPath(const BSONObj& obj, StringData path, - BSONElementMSet& elements, + BSONElementMultiSet& elements, bool expandArrayOnTrailingField, std::set<size_t>* arrayComponents) { const size_t initialDepth = 0; diff --git a/src/mongo/db/bson/dotted_path_support.h b/src/mongo/db/bson/dotted_path_support.h index c9c6676c75a..0a3d072c56a 100644 --- a/src/mongo/db/bson/dotted_path_support.h +++ b/src/mongo/db/bson/dotted_path_support.h @@ -31,6 +31,7 @@ #include <cstddef> #include <set> +#include "mongo/bson/bsonelement_comparator_interface.h" #include "mongo/bson/bsonobj.h" namespace mongo { @@ -104,7 +105,7 @@ void extractAllElementsAlongPath(const BSONObj& obj, void extractAllElementsAlongPath(const BSONObj& obj, StringData path, - BSONElementMSet& elements, + BSONElementMultiSet& elements, bool expandArrayOnTrailingField = true, std::set<std::size_t>* arrayComponents = nullptr); diff --git a/src/mongo/db/catalog/apply_ops.cpp b/src/mongo/db/catalog/apply_ops.cpp index 672a37a10c7..ec5c7681f0e 100644 --- a/src/mongo/db/catalog/apply_ops.cpp +++ b/src/mongo/db/catalog/apply_ops.cpp @@ -32,6 +32,7 @@ #include "mongo/db/catalog/apply_ops.h" +#include "mongo/bson/util/bson_extract.h" #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/database.h" #include "mongo/db/catalog/database_holder.h" @@ -42,21 +43,31 @@ #include "mongo/db/curop.h" #include "mongo/db/db_raii.h" #include "mongo/db/dbdirectclient.h" +#include "mongo/db/index/index_descriptor.h" #include "mongo/db/matcher/extensions_callback_disallow_extensions.h" #include "mongo/db/matcher/matcher.h" #include "mongo/db/op_observer.h" #include "mongo/db/operation_context.h" +#include "mongo/db/query/collation/collation_spec.h" #include "mongo/db/repl/oplog.h" #include "mongo/db/repl/replication_coordinator_global.h" #include "mongo/db/service_context.h" +#include "mongo/rpc/get_status_from_command_result.h" +#include "mongo/util/fail_point_service.h" #include "mongo/util/log.h" namespace mongo { namespace { + +const auto kPreconditionFieldName = "preCondition"_sd; + +// If enabled, causes loop in _applyOps() to hang after applying current operation. +MONGO_FP_DECLARE(applyOpsPauseBetweenOperations); + /** * Return true iff the applyOpsCmd can be executed in a single WriteUnitOfWork. */ -bool canBeAtomic(const BSONObj& applyOpCmd) { +bool _areOpsCrudOnly(const BSONObj& applyOpCmd) { for (const auto& elem : applyOpCmd.firstElement().Obj()) { const char* names[] = {"ns", "op"}; BSONElement fields[2]; @@ -89,14 +100,11 @@ bool canBeAtomic(const BSONObj& applyOpCmd) { return true; } -Status _applyOps(OperationContext* txn, +Status _applyOps(OperationContext* opCtx, const std::string& dbName, const BSONObj& applyOpCmd, BSONObjBuilder* result, int* numApplied) { - dassert(txn->lockState()->isLockHeldForMode( - ResourceId(RESOURCE_GLOBAL, ResourceId::SINGLETON_GLOBAL), MODE_X)); - BSONObj ops = applyOpCmd.firstElement().Obj(); // apply @@ -107,148 +115,169 @@ Status _applyOps(OperationContext* txn, BSONArrayBuilder ab; const bool alwaysUpsert = applyOpCmd.hasField("alwaysUpsert") ? applyOpCmd["alwaysUpsert"].trueValue() : true; - const bool haveWrappingWUOW = txn->lockState()->inAWriteUnitOfWork(); - - { - repl::UnreplicatedWritesBlock uwb(txn); + const bool haveWrappingWUOW = opCtx->lockState()->inAWriteUnitOfWork(); - while (i.more()) { - BSONElement e = i.next(); - const BSONObj& opObj = e.Obj(); + while (i.more()) { + BSONElement e = i.next(); + const BSONObj& opObj = e.Obj(); - // Ignore 'n' operations. - const char* opType = opObj["op"].valuestrsafe(); - if (*opType == 'n') - continue; + // Ignore 'n' operations. + const char* opType = opObj["op"].valuestrsafe(); + if (*opType == 'n') + continue; - const std::string ns = opObj["ns"].String(); + const NamespaceString nss(opObj["ns"].String()); - // Need to check this here, or OldClientContext may fail an invariant. - if (*opType != 'c' && !NamespaceString(ns).isValid()) - return {ErrorCodes::InvalidNamespace, "invalid ns: " + ns}; + // Need to check this here, or OldClientContext may fail an invariant. + if (*opType != 'c' && !nss.isValid()) + return {ErrorCodes::InvalidNamespace, "invalid ns: " + nss.ns()}; - Status status(ErrorCodes::InternalError, ""); + Status status(ErrorCodes::InternalError, ""); - if (haveWrappingWUOW) { - invariant(*opType != 'c'); + if (haveWrappingWUOW) { + invariant(opCtx->lockState()->isW()); + invariant(*opType != 'c'); + auto db = dbHolder().get(opCtx, nss.ns()); + if (!db) { + throw DBException( + "cannot create a database in atomic applyOps mode; will retry without " + "atomicity", + ErrorCodes::NamespaceNotFound); + } - if (!dbHolder().get(txn, ns)) { - throw DBException( - "cannot create a database in atomic applyOps mode; will retry without " - "atomicity", - ErrorCodes::NamespaceNotFound); - } + // When processing an update on a non-existent collection, applyOperation_inlock() + // returns UpdateOperationFailed on updates and allows the collection to be + // implicitly created on upserts. We detect both cases here and fail early with + // NamespaceNotFound. + auto collection = db->getCollection(nss); + if (!collection && !nss.isSystemDotIndexes() && (*opType == 'i' || *opType == 'u')) { + throw DBException(str::stream() << "cannot apply insert or update operation on " + "a non-existent namespace " + << nss.ns() + << ": " + << redact(opObj), + ErrorCodes::NamespaceNotFound); + } - OldClientContext ctx(txn, ns); - status = repl::applyOperation_inlock(txn, ctx.db(), opObj, alwaysUpsert); - if (!status.isOK()) - return status; - logOpForDbHash(txn, ns.c_str()); - } else { - try { - // Run operations under a nested lock as a hack to prevent yielding. - // - // The list of operations is supposed to be applied atomically; yielding - // would break atomicity by allowing an interruption or a shutdown to occur - // after only some operations are applied. We are already locked globally - // at this point, so taking a DBLock on the namespace creates a nested lock, - // and yields are disallowed for operations that hold a nested lock. - // - // We do not have a wrapping WriteUnitOfWork so it is possible for a journal - // commit to happen with a subset of ops applied. - Lock::GlobalWrite globalWriteLockDisallowTempRelease(txn->lockState()); - - // Ensures that yielding will not happen (see the comment above). - DEV { - Locker::LockSnapshot lockSnapshot; - invariant(!txn->lockState()->saveLockStateAndUnlock(&lockSnapshot)); - }; - - MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN { - if (*opType == 'c') { - status = repl::applyCommand_inlock(txn, opObj, true); + OldClientContext ctx(opCtx, nss.ns()); + status = repl::applyOperation_inlock(opCtx, ctx.db(), opObj, alwaysUpsert); + if (!status.isOK()) + return status; + logOpForDbHash(opCtx, nss.ns().c_str()); + } else { + try { + MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN { + if (*opType == 'c') { + invariant(opCtx->lockState()->isW()); + uassertStatusOK(status = repl::applyCommand_inlock(opCtx, opObj, true)); + } else { + const char* names[] = {"o", "ns"}; + BSONElement fields[2]; + opObj.getFields(2, names, fields); + BSONElement& fieldO = fields[0]; + BSONElement& fieldNs = fields[1]; + const StringData ns = fieldNs.valueStringData(); + NamespaceString requestNss{ns}; + + if (nss.isSystemDotIndexes()) { + BSONObj indexSpec; + NamespaceString indexNss; + std::tie(indexSpec, indexNss) = + repl::prepForApplyOpsIndexInsert(fieldO, opObj, requestNss); + if (!indexSpec["collation"]) { + // If the index spec does not include a collation, explicitly + // specify the simple collation, so the index does not inherit the + // collection default collation. + auto indexVersion = indexSpec["v"]; + // The index version is populated by prepForApplyOpsIndexInsert(). + invariant(indexVersion); + if (indexVersion.isNumber() && + (indexVersion.numberInt() >= + static_cast<int>(IndexDescriptor::IndexVersion::kV2))) { + BSONObjBuilder bob; + bob.append("collation", CollationSpec::kSimpleSpec); + bob.appendElements(indexSpec); + indexSpec = bob.obj(); + } + } + BSONObjBuilder command; + command.append("createIndexes", indexNss.coll()); + { + BSONArrayBuilder indexes(command.subarrayStart("indexes")); + indexes.append(indexSpec); + indexes.doneFast(); + } + const BSONObj commandObj = command.done(); + + DBDirectClient client(opCtx); + BSONObj infoObj; + client.runCommand(nsToDatabase(ns), commandObj, infoObj); + status = getStatusFromCommandResult(infoObj); } else { - OldClientContext ctx(txn, ns); - - status = - repl::applyOperation_inlock(txn, ctx.db(), opObj, alwaysUpsert); + AutoGetCollection autoColl(opCtx, nss, MODE_IX); + if (!autoColl.getCollection() && !nss.isSystemDotIndexes()) { + // For idempotency reasons, return success on delete operations. + if (*opType == 'd') { + status = Status::OK(); + } else { + throw DBException( + str::stream() + << "cannot apply insert or update operation on" + " a non-existent namespace " + << nss.ns() + << ": " + << mongo::redact(opObj), + ErrorCodes::NamespaceNotFound); + } + } else { + OldClientContext ctx(opCtx, nss.ns()); + status = repl::applyOperation_inlock( + opCtx, ctx.db(), opObj, alwaysUpsert); + } } } - MONGO_WRITE_CONFLICT_RETRY_LOOP_END(txn, "applyOps", ns); - } catch (const DBException& ex) { - ab.append(false); - result->append("applied", ++(*numApplied)); - result->append("code", ex.getCode()); - result->append("codeName", - ErrorCodes::errorString(ErrorCodes::fromInt(ex.getCode()))); - result->append("errmsg", ex.what()); - result->append("results", ab.arr()); - return Status(ErrorCodes::UnknownError, ex.what()); } - WriteUnitOfWork wuow(txn); - logOpForDbHash(txn, ns.c_str()); - wuow.commit(); + MONGO_WRITE_CONFLICT_RETRY_LOOP_END(opCtx, "applyOps", nss.ns()); + } catch (const DBException& ex) { + ab.append(false); + result->append("applied", ++(*numApplied)); + result->append("code", ex.getCode()); + result->append("codeName", + ErrorCodes::errorString(ErrorCodes::fromInt(ex.getCode()))); + result->append("errmsg", ex.what()); + result->append("results", ab.arr()); + return ex.toStatus(); } - - ab.append(status.isOK()); - if (!status.isOK()) { - log() << "applyOps error applying: " << status; - errors++; - } - - (*numApplied)++; + WriteUnitOfWork wuow(opCtx); + logOpForDbHash(opCtx, nss.ns().c_str()); + wuow.commit(); } - result->append("applied", *numApplied); - result->append("results", ab.arr()); - } // set replicatedWrites back to original value - - if (txn->writesAreReplicated()) { - // We want this applied atomically on slaves - // so we re-wrap without the pre-condition for speed - - std::string tempNS = str::stream() << dbName << ".$cmd"; - - // TODO: possibly use mutable BSON to remove preCondition field - // once it is available - BSONObjBuilder cmdBuilder; - - for (auto elem : applyOpCmd) { - auto name = elem.fieldNameStringData(); - if (name == "preCondition") - continue; - if (name == "bypassDocumentValidation") - continue; - cmdBuilder.append(elem); + ab.append(status.isOK()); + if (!status.isOK()) { + log() << "applyOps error applying: " << status; + errors++; } - const BSONObj cmdRewritten = cmdBuilder.done(); - - auto opObserver = getGlobalServiceContext()->getOpObserver(); - invariant(opObserver); - if (haveWrappingWUOW) { - opObserver->onApplyOps(txn, tempNS, cmdRewritten); - } else { - // When executing applyOps outside of a wrapping WriteUnitOfWOrk, always logOp the - // command regardless of whether the individial ops succeeded and rely on any - // failures to also on secondaries. This isn't perfect, but it's what the command - // has always done and is part of its "correct" behavior. - while (true) { - try { - WriteUnitOfWork wunit(txn); - opObserver->onApplyOps(txn, tempNS, cmdRewritten); - - wunit.commit(); - break; - } catch (const WriteConflictException& wce) { - LOG(2) << "WriteConflictException while logging applyOps command, retrying."; - txn->recoveryUnit()->abandonSnapshot(); - continue; - } + (*numApplied)++; + + if (MONGO_FAIL_POINT(applyOpsPauseBetweenOperations)) { + // While holding a database lock under MMAPv1, we would be implicitly holding the + // flush lock here. This would prevent other threads from acquiring the global + // lock or any database locks. We release all locks temporarily while the fail + // point is enabled to allow other threads to make progress. + boost::optional<Lock::TempRelease> release; + auto storageEngine = opCtx->getServiceContext()->getGlobalStorageEngine(); + if (storageEngine->isMmapV1() && !opCtx->lockState()->isW()) { + release.emplace(opCtx->lockState()); } + MONGO_FAIL_POINT_PAUSE_WHILE_SET(applyOpsPauseBetweenOperations); } } + result->append("applied", *numApplied); + result->append("results", ab.arr()); + if (errors != 0) { return Status(ErrorCodes::UnknownError, "applyOps had one or more errors applying ops"); } @@ -256,94 +285,144 @@ Status _applyOps(OperationContext* txn, return Status::OK(); } -Status preconditionOK(OperationContext* txn, const BSONObj& applyOpCmd, BSONObjBuilder* result) { - dassert(txn->lockState()->isLockHeldForMode( - ResourceId(RESOURCE_GLOBAL, ResourceId::SINGLETON_GLOBAL), MODE_X)); - - if (applyOpCmd["preCondition"].type() == Array) { - BSONObjIterator i(applyOpCmd["preCondition"].Obj()); - while (i.more()) { - BSONObj preCondition = i.next().Obj(); - if (preCondition["ns"].type() != BSONType::String) { - return {ErrorCodes::InvalidNamespace, - str::stream() << "ns in preCondition must be a string, but found type: " - << typeName(preCondition["ns"].type())}; - } - const NamespaceString nss(preCondition["ns"].valueStringData()); - if (!nss.isValid()) { - return {ErrorCodes::InvalidNamespace, "invalid ns: " + nss.ns()}; - } +bool _hasPrecondition(const BSONObj& applyOpCmd) { + return applyOpCmd[kPreconditionFieldName].type() == Array; +} - DBDirectClient db(txn); - BSONObj realres = db.findOne(nss.ns(), preCondition["q"].Obj()); +Status _checkPrecondition(OperationContext* opCtx, + const BSONObj& applyOpCmd, + BSONObjBuilder* result) { + invariant(opCtx->lockState()->isW()); + invariant(_hasPrecondition(applyOpCmd)); + + for (auto elem : applyOpCmd[kPreconditionFieldName].Obj()) { + auto preCondition = elem.Obj(); + if (preCondition["ns"].type() != BSONType::String) { + return {ErrorCodes::InvalidNamespace, + str::stream() << "ns in preCondition must be a string, but found type: " + << typeName(preCondition["ns"].type())}; + } + const NamespaceString nss(preCondition["ns"].valueStringData()); + if (!nss.isValid()) { + return {ErrorCodes::InvalidNamespace, "invalid ns: " + nss.ns()}; + } - // Get collection default collation. - Database* database = dbHolder().get(txn, nss.db()); - if (!database) { - return {ErrorCodes::NamespaceNotFound, - "database in ns does not exist: " + nss.ns()}; - } - Collection* collection = database->getCollection(nss.ns()); - if (!collection) { - return {ErrorCodes::NamespaceNotFound, - "collection in ns does not exist: " + nss.ns()}; - } - const CollatorInterface* collator = collection->getDefaultCollator(); - - // Apply-ops would never have a $where/$text matcher. Using the "DisallowExtensions" - // callback ensures that parsing will throw an error if $where or $text are found. - Matcher matcher( - preCondition["res"].Obj(), ExtensionsCallbackDisallowExtensions(), collator); - if (!matcher.matches(realres)) { - result->append("got", realres); - result->append("whatFailed", preCondition); - return {ErrorCodes::BadValue, "preCondition failed"}; - } + DBDirectClient db(opCtx); + BSONObj realres = db.findOne(nss.ns(), preCondition["q"].Obj()); + + // Get collection default collation. + Database* database = dbHolder().get(opCtx, nss.db()); + if (!database) { + return {ErrorCodes::NamespaceNotFound, "database in ns does not exist: " + nss.ns()}; + } + Collection* collection = database->getCollection(nss.ns()); + if (!collection) { + return {ErrorCodes::NamespaceNotFound, "collection in ns does not exist: " + nss.ns()}; + } + const CollatorInterface* collator = collection->getDefaultCollator(); + + // Apply-ops would never have a $where/$text matcher. Using the "DisallowExtensions" + // callback ensures that parsing will throw an error if $where or $text are found. + Matcher matcher( + preCondition["res"].Obj(), ExtensionsCallbackDisallowExtensions(), collator); + if (!matcher.matches(realres)) { + result->append("got", realres); + result->append("whatFailed", preCondition); + return {ErrorCodes::BadValue, "preCondition failed"}; } } + return Status::OK(); } } // namespace -Status applyOps(OperationContext* txn, +Status applyOps(OperationContext* opCtx, const std::string& dbName, const BSONObj& applyOpCmd, BSONObjBuilder* result) { - ScopedTransaction scopedXact(txn, MODE_X); - Lock::GlobalWrite globalWriteLock(txn->lockState()); + bool allowAtomic = false; + uassertStatusOK( + bsonExtractBooleanFieldWithDefault(applyOpCmd, "allowAtomic", true, &allowAtomic)); + auto areOpsCrudOnly = _areOpsCrudOnly(applyOpCmd); + auto isAtomic = allowAtomic && areOpsCrudOnly; + auto hasPrecondition = _hasPrecondition(applyOpCmd); + + ScopedTransaction scopedXact(opCtx, MODE_X); + boost::optional<Lock::GlobalWrite> globalWriteLock; + boost::optional<Lock::DBLock> dbWriteLock; + + // There's only one case where we are allowed to take the database lock instead of the global + // lock - no preconditions; only CRUD ops; and non-atomic mode. + if (!hasPrecondition && areOpsCrudOnly && !allowAtomic) { + dbWriteLock.emplace(opCtx->lockState(), dbName, MODE_IX); + } else { + globalWriteLock.emplace(opCtx->lockState()); + } - bool userInitiatedWritesAndNotPrimary = txn->writesAreReplicated() && + bool userInitiatedWritesAndNotPrimary = opCtx->writesAreReplicated() && !repl::getGlobalReplicationCoordinator()->canAcceptWritesForDatabase(dbName); if (userInitiatedWritesAndNotPrimary) return Status(ErrorCodes::NotMaster, str::stream() << "Not primary while applying ops to database " << dbName); - Status preconditionStatus = preconditionOK(txn, applyOpCmd, result); - if (!preconditionStatus.isOK()) { - return preconditionStatus; + if (hasPrecondition) { + auto status = _checkPrecondition(opCtx, applyOpCmd, result); + if (!status.isOK()) { + return status; + } } int numApplied = 0; - if (!canBeAtomic(applyOpCmd)) - return _applyOps(txn, dbName, applyOpCmd, result, &numApplied); + if (!isAtomic) + return _applyOps(opCtx, dbName, applyOpCmd, result, &numApplied); // Perform write ops atomically + invariant(globalWriteLock); try { MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN { BSONObjBuilder intermediateResult; - WriteUnitOfWork wunit(txn); + WriteUnitOfWork wunit(opCtx); numApplied = 0; - uassertStatusOK(_applyOps(txn, dbName, applyOpCmd, &intermediateResult, &numApplied)); + { + // Suppress replication for atomic operations until end of applyOps. + repl::UnreplicatedWritesBlock uwb(opCtx); + uassertStatusOK( + _applyOps(opCtx, dbName, applyOpCmd, &intermediateResult, &numApplied)); + } + // Generate oplog entry for all atomic ops collectively. + if (opCtx->writesAreReplicated()) { + // We want this applied atomically on slaves so we rewrite the oplog entry without + // the pre-condition for speed. + + std::string tempNS = str::stream() << dbName << ".$cmd"; + + BSONObjBuilder cmdBuilder; + + for (auto elem : applyOpCmd) { + auto name = elem.fieldNameStringData(); + if (name == kPreconditionFieldName) + continue; + if (name == "bypassDocumentValidation") + continue; + cmdBuilder.append(elem); + } + + const BSONObj cmdRewritten = cmdBuilder.done(); + + auto opObserver = getGlobalServiceContext()->getOpObserver(); + invariant(opObserver); + opObserver->onApplyOps(opCtx, tempNS, cmdRewritten); + } wunit.commit(); result->appendElements(intermediateResult.obj()); } - MONGO_WRITE_CONFLICT_RETRY_LOOP_END(txn, "applyOps", dbName); + MONGO_WRITE_CONFLICT_RETRY_LOOP_END(opCtx, "applyOps", dbName); } catch (const DBException& ex) { if (ex.getCode() == ErrorCodes::NamespaceNotFound) { // Retry in non-atomic mode, since MMAP cannot implicitly create a new database // within an active WriteUnitOfWork. - return _applyOps(txn, dbName, applyOpCmd, result, &numApplied); + return _applyOps(opCtx, dbName, applyOpCmd, result, &numApplied); } BSONArrayBuilder ab; ++numApplied; diff --git a/src/mongo/db/catalog/apply_ops.h b/src/mongo/db/catalog/apply_ops.h index 588d3bb370b..3a742891573 100644 --- a/src/mongo/db/catalog/apply_ops.h +++ b/src/mongo/db/catalog/apply_ops.h @@ -37,7 +37,7 @@ class OperationContext; * Applies ops contained in "applyOpCmd" and populates fields in "result" to be returned to the * user. */ -Status applyOps(OperationContext* txn, +Status applyOps(OperationContext* opCtx, const std::string& dbName, const BSONObj& applyOpCmd, BSONObjBuilder* result); diff --git a/src/mongo/db/catalog/database.cpp b/src/mongo/db/catalog/database.cpp index 9e24afd5ffb..23fdfb84e95 100644 --- a/src/mongo/db/catalog/database.cpp +++ b/src/mongo/db/catalog/database.cpp @@ -475,8 +475,8 @@ Status Database::renameCollection(OperationContext* txn, Top::get(txn->getClient()->getServiceContext()).collectionDropped(fromNS.toString()); } - txn->recoveryUnit()->registerChange(new AddCollectionChange(txn, this, toNS)); Status s = _dbEntry->renameCollection(txn, fromNS, toNS, stayTemp); + txn->recoveryUnit()->registerChange(new AddCollectionChange(txn, this, toNS)); _collections[toNS] = _getOrCreateCollectionInstance(txn, toNS); return s; } diff --git a/src/mongo/db/catalog/rename_collection.cpp b/src/mongo/db/catalog/rename_collection.cpp index d9d25a951dc..f5f62ce9636 100644 --- a/src/mongo/db/catalog/rename_collection.cpp +++ b/src/mongo/db/catalog/rename_collection.cpp @@ -71,7 +71,15 @@ Status renameCollection(OperationContext* txn, DisableDocumentValidation validationDisabler(txn); ScopedTransaction transaction(txn, MODE_X); - Lock::GlobalWrite globalWriteLock(txn->lockState()); + boost::optional<Lock::GlobalWrite> globalWriteLock; + boost::optional<Lock::DBLock> dbWriteLock; + + // If the rename is known not to be a cross-database rename, just a database lock suffices. + if (source.db() == target.db()) + dbWriteLock.emplace(txn->lockState(), source.db(), MODE_X); + else + globalWriteLock.emplace(txn->lockState()); + // We stay in source context the whole time. This is mostly to set the CurOp namespace. OldClientContext ctx(txn, source.ns()); diff --git a/src/mongo/db/commands/apply_ops_cmd.cpp b/src/mongo/db/commands/apply_ops_cmd.cpp index c252b8fd073..262e8ef93d3 100644 --- a/src/mongo/db/commands/apply_ops_cmd.cpp +++ b/src/mongo/db/commands/apply_ops_cmd.cpp @@ -116,22 +116,17 @@ public: } } - auto client = txn->getClient(); - auto lastOpAtOperationStart = repl::ReplClientInfo::forClient(client).getLastOp(); - ScopeGuard lastOpSetterGuard = - MakeObjGuard(repl::ReplClientInfo::forClient(client), - &repl::ReplClientInfo::setLastOpToSystemLastOpTime, - txn); - + // TODO (SERVER-30217): When a write concern is provided to the applyOps command, we + // normally wait on the OpTime of whichever operation successfully completed last. This is + // erroneous, however, if the last operation in the array happens to be a write no-op and + // thus isn’t assigned an OpTime. Let the second to last operation in the applyOps be write + // A, the last operation in applyOps be write B. Let B do a no-op write and let the + // operation that caused B to be a no-op be C. If C has an OpTime after A but before B, + // then we won’t wait for C to be replicated and it could be rolled back, even though B + // was acknowledged. To fix this, we should wait for replication of the node’s last applied + // OpTime if the last write operation was a no-op write. auto applyOpsStatus = appendCommandStatus(result, applyOps(txn, dbname, cmdObj, &result)); - if (repl::ReplClientInfo::forClient(client).getLastOp() != lastOpAtOperationStart) { - // If this operation has already generated a new lastOp, don't bother setting it - // here. No-op applyOps will not generate a new lastOp, so we still need the guard to - // fire in that case. - lastOpSetterGuard.Dismiss(); - } - return applyOpsStatus; } diff --git a/src/mongo/db/commands/create_indexes.cpp b/src/mongo/db/commands/create_indexes.cpp index 489e080f7a6..ee0133da46b 100644 --- a/src/mongo/db/commands/create_indexes.cpp +++ b/src/mongo/db/commands/create_indexes.cpp @@ -290,12 +290,6 @@ public: const int numIndexesBefore = collection->getIndexCatalog()->numIndexesTotal(txn); result.append("numIndexesBefore", numIndexesBefore); - auto client = txn->getClient(); - ScopeGuard lastOpSetterGuard = - MakeObjGuard(repl::ReplClientInfo::forClient(client), - &repl::ReplClientInfo::setLastOpToSystemLastOpTime, - txn); - MultiIndexBlock indexer(txn, collection); indexer.allowBackgroundBuilding(); indexer.allowInterruption(); @@ -403,8 +397,6 @@ public: result.append("numIndexesAfter", collection->getIndexCatalog()->numIndexesTotal(txn)); - lastOpSetterGuard.Dismiss(); - return true; } diff --git a/src/mongo/db/commands/dbcommands.cpp b/src/mongo/db/commands/dbcommands.cpp index 37bcf783d47..38eae01ce9d 100644 --- a/src/mongo/db/commands/dbcommands.cpp +++ b/src/mongo/db/commands/dbcommands.cpp @@ -61,6 +61,7 @@ #include "mongo/db/commands.h" #include "mongo/db/commands/server_status.h" #include "mongo/db/commands/shutdown.h" +#include "mongo/db/concurrency/global_lock_acquisition_tracker.h" #include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/db_raii.h" #include "mongo/db/dbdirectclient.h" @@ -1256,13 +1257,21 @@ namespace { void _waitForWriteConcernAndAddToCommandResponse(OperationContext* opCtx, const std::string& commandName, + const repl::OpTime& lastOpBeforeRun, BSONObjBuilder* commandResponseBuilder) { + auto lastOpAfterRun = repl::ReplClientInfo::forClient(opCtx->getClient()).getLastOp(); + + // Ensures that if we tried to do a write, we wait for write concern, even if that write was + // a noop. + if ((lastOpAfterRun == lastOpBeforeRun) && + GlobalLockAcquisitionTracker::get(opCtx).getGlobalExclusiveLockTaken()) { + repl::ReplClientInfo::forClient(opCtx->getClient()).setLastOpToSystemLastOpTime(opCtx); + lastOpAfterRun = repl::ReplClientInfo::forClient(opCtx->getClient()).getLastOp(); + } + WriteConcernResult res; auto waitForWCStatus = - waitForWriteConcern(opCtx, - repl::ReplClientInfo::forClient(opCtx->getClient()).getLastOp(), - opCtx->getWriteConcern(), - &res); + waitForWriteConcern(opCtx, lastOpAfterRun, opCtx->getWriteConcern(), &res); Command::appendCommandWCStatus(*commandResponseBuilder, waitForWCStatus, res); // SERVER-22421: This code is to ensure error response backwards compatibility with the @@ -1558,13 +1567,17 @@ bool Command::run(OperationContext* txn, return result; } + auto lastOpBeforeRun = repl::ReplClientInfo::forClient(txn->getClient()).getLastOp(); + // Change the write concern while running the command. const auto oldWC = txn->getWriteConcern(); ON_BLOCK_EXIT([&] { txn->setWriteConcern(oldWC); }); txn->setWriteConcern(wcResult.getValue()); - ON_BLOCK_EXIT( - [&] { _waitForWriteConcernAndAddToCommandResponse(txn, getName(), &inPlaceReplyBob); }); + ON_BLOCK_EXIT([&] { + _waitForWriteConcernAndAddToCommandResponse( + txn, getName(), lastOpBeforeRun, &inPlaceReplyBob); + }); result = run(txn, db, cmd, 0, errmsg, inPlaceReplyBob); diff --git a/src/mongo/db/commands/feature_compatibility_version.cpp b/src/mongo/db/commands/feature_compatibility_version.cpp index f354efa88d9..00c6de52a5c 100644 --- a/src/mongo/db/commands/feature_compatibility_version.cpp +++ b/src/mongo/db/commands/feature_compatibility_version.cpp @@ -248,12 +248,8 @@ void FeatureCompatibilityVersion::set(OperationContext* txn, StringData version) client.runCommand(nss.db().toString(), makeUpdateCommand(version, WriteConcernOptions::Majority), updateResult); - uassertStatusOK(getStatusFromCommandResult(updateResult)); - uassertStatusOK(getWriteConcernStatusFromCommandResult(updateResult)); + uassertStatusOK(getStatusFromWriteCommandReply(updateResult)); - // We then update the value of the featureCompatibilityVersion server parameter. - serverGlobalParams.featureCompatibility.version.store( - ServerGlobalParams::FeatureCompatibility::Version::k34); } else if (version == FeatureCompatibilityVersionCommandParser::kVersion32) { // We update the featureCompatibilityVersion document stored in the "admin.system.version" // collection. We do this before dropping the v=2 index in order to maintain the invariant @@ -262,7 +258,7 @@ void FeatureCompatibilityVersion::set(OperationContext* txn, StringData version) // concern to this update because we're going to do so anyway for the "dropIndexes" command. BSONObj updateResult; client.runCommand(nss.db().toString(), makeUpdateCommand(version, BSONObj()), updateResult); - uassertStatusOK(getStatusFromCommandResult(updateResult)); + uassertStatusOK(getStatusFromWriteCommandReply(updateResult)); // We then drop the v=2 index on the "admin.system.version" collection to enable 3.2 // secondaries to sync from this mongod. @@ -278,10 +274,6 @@ void FeatureCompatibilityVersion::set(OperationContext* txn, StringData version) uassertStatusOK(status); } uassertStatusOK(getWriteConcernStatusFromCommandResult(dropIndexesResult)); - - // We then update the value of the featureCompatibilityVersion server parameter. - serverGlobalParams.featureCompatibility.version.store( - ServerGlobalParams::FeatureCompatibility::Version::k32); } } @@ -323,7 +315,8 @@ void FeatureCompatibilityVersion::setIfCleanStartup(OperationContext* txn, } // We then insert the featureCompatibilityVersion document into the "admin.system.version" - // collection. We do this after creating the v=2 index in order to maintain the invariant + // collection. The server parameter will be updated on commit by the op observer. + // We do this after creating the v=2 index in order to maintain the invariant // that if the featureCompatibilityVersion is 3.4, then 'k32IncompatibleIndexSpec' index // exists on the "admin.system.version" collection. If we happened to fail to insert the // document when starting up, then on a subsequent start-up we'd no longer consider the data @@ -334,14 +327,10 @@ void FeatureCompatibilityVersion::setIfCleanStartup(OperationContext* txn, BSON("_id" << FeatureCompatibilityVersion::kParameterName << FeatureCompatibilityVersion::kVersionField << FeatureCompatibilityVersionCommandParser::kVersion34))); - - // We then update the value of the featureCompatibilityVersion server parameter. - serverGlobalParams.featureCompatibility.version.store( - ServerGlobalParams::FeatureCompatibility::Version::k34); } } -void FeatureCompatibilityVersion::onInsertOrUpdate(const BSONObj& doc) { +void FeatureCompatibilityVersion::onInsertOrUpdate(OperationContext* opCtx, const BSONObj& doc) { auto idElement = doc["_id"]; if (idElement.type() != BSONType::String || idElement.String() != FeatureCompatibilityVersion::kParameterName) { @@ -350,10 +339,11 @@ void FeatureCompatibilityVersion::onInsertOrUpdate(const BSONObj& doc) { auto newVersion = uassertStatusOK(FeatureCompatibilityVersion::parse(doc)); log() << "setting featureCompatibilityVersion to " << getFeatureCompatibilityVersionString(newVersion); - serverGlobalParams.featureCompatibility.version.store(newVersion); + opCtx->recoveryUnit()->onCommit( + [newVersion]() { serverGlobalParams.featureCompatibility.version.store(newVersion); }); } -void FeatureCompatibilityVersion::onDelete(const BSONObj& doc) { +void FeatureCompatibilityVersion::onDelete(OperationContext* opCtx, const BSONObj& doc) { auto idElement = doc["_id"]; if (idElement.type() != BSONType::String || idElement.String() != FeatureCompatibilityVersion::kParameterName) { @@ -361,15 +351,19 @@ void FeatureCompatibilityVersion::onDelete(const BSONObj& doc) { } log() << "setting featureCompatibilityVersion to " << FeatureCompatibilityVersionCommandParser::kVersion32; - serverGlobalParams.featureCompatibility.version.store( - ServerGlobalParams::FeatureCompatibility::Version::k32); + opCtx->recoveryUnit()->onCommit([]() { + serverGlobalParams.featureCompatibility.version.store( + ServerGlobalParams::FeatureCompatibility::Version::k32); + }); } -void FeatureCompatibilityVersion::onDropCollection() { +void FeatureCompatibilityVersion::onDropCollection(OperationContext* opCtx) { log() << "setting featureCompatibilityVersion to " << FeatureCompatibilityVersionCommandParser::kVersion32; - serverGlobalParams.featureCompatibility.version.store( - ServerGlobalParams::FeatureCompatibility::Version::k32); + opCtx->recoveryUnit()->onCommit([]() { + serverGlobalParams.featureCompatibility.version.store( + ServerGlobalParams::FeatureCompatibility::Version::k32); + }); } /** diff --git a/src/mongo/db/commands/feature_compatibility_version.h b/src/mongo/db/commands/feature_compatibility_version.h index 4bcb4b56e55..8e7969c2e70 100644 --- a/src/mongo/db/commands/feature_compatibility_version.h +++ b/src/mongo/db/commands/feature_compatibility_version.h @@ -76,21 +76,22 @@ public: /** * Examines a document inserted or updated in admin.system.version. If it is the - * featureCompatibilityVersion document, validates the document and updates the server - * parameter. + * featureCompatibilityVersion document, validates the document and on commit, updates + * the server parameter. */ - static void onInsertOrUpdate(const BSONObj& doc); + static void onInsertOrUpdate(OperationContext* opCtx, const BSONObj& doc); /** * Examines the _id of a document removed from admin.system.version. If it is the * featureCompatibilityVersion document, resets the server parameter to its default value (3.2). + * on commit. */ - static void onDelete(const BSONObj& doc); + static void onDelete(OperationContext* opCtx, const BSONObj& doc); /** - * Resets the server parameter to its default value (3.2). + * Resets the server parameter to its default value (3.2) on commit. */ - static void onDropCollection(); + static void onDropCollection(OperationContext* opCtx); }; } // namespace mongo diff --git a/src/mongo/db/commands/find_and_modify.cpp b/src/mongo/db/commands/find_and_modify.cpp index 79ee34203ce..61dbc4f6509 100644 --- a/src/mongo/db/commands/find_and_modify.cpp +++ b/src/mongo/db/commands/find_and_modify.cpp @@ -364,18 +364,6 @@ public: if (shouldBypassDocumentValidationForCommand(cmdObj)) maybeDisableValidation.emplace(txn); - auto client = txn->getClient(); - auto lastOpAtOperationStart = repl::ReplClientInfo::forClient(client).getLastOp(); - ScopeGuard lastOpSetterGuard = - MakeObjGuard(repl::ReplClientInfo::forClient(client), - &repl::ReplClientInfo::setLastOpToSystemLastOpTime, - txn); - - // If this is the local database, don't set last op. - if (dbName == "local") { - lastOpSetterGuard.Dismiss(); - } - auto curOp = CurOp::get(txn); OpDebug* opDebug = &curOp->debug(); @@ -567,13 +555,6 @@ public: } MONGO_WRITE_CONFLICT_RETRY_LOOP_END(txn, "findAndModify", nsString.ns()); - if (repl::ReplClientInfo::forClient(client).getLastOp() != lastOpAtOperationStart) { - // If this operation has already generated a new lastOp, don't bother setting it here. - // No-op updates will not generate a new lastOp, so we still need the guard to fire in - // that case. - lastOpSetterGuard.Dismiss(); - } - return true; } diff --git a/src/mongo/db/commands/mr.cpp b/src/mongo/db/commands/mr.cpp index 4aeed6d5978..136656b8dd2 100644 --- a/src/mongo/db/commands/mr.cpp +++ b/src/mongo/db/commands/mr.cpp @@ -314,7 +314,7 @@ Config::Config(const string& _dbname, const BSONObj& cmdObj) { // scope and code if (cmdObj["scope"].type() == Object) - scopeSetup = cmdObj["scope"].embeddedObjectUserCheck(); + scopeSetup = cmdObj["scope"].embeddedObjectUserCheck().getOwned(); mapper.reset(new JSMapper(cmdObj["map"])); reducer.reset(new JSReducer(cmdObj["reduce"])); @@ -322,7 +322,7 @@ Config::Config(const string& _dbname, const BSONObj& cmdObj) { finalizer.reset(new JSFinalizer(cmdObj["finalize"])); if (cmdObj["mapparams"].type() == Array) { - mapParams = cmdObj["mapparams"].embeddedObjectUserCheck(); + mapParams = cmdObj["mapparams"].embeddedObjectUserCheck().getOwned(); } } @@ -836,6 +836,7 @@ void State::init() { AuthorizationSession::get(Client::getCurrent())->getAuthenticatedUserNamesToken(); _scope.reset(getGlobalScriptEngine()->newScopeForCurrentThread()); _scope->registerOperation(_txn); + _scope->requireOwnedObjects(); _scope->setLocalDB(_config.dbname); _scope->loadStored(_txn, true); @@ -1522,6 +1523,7 @@ public: BSONObj o; PlanExecutor::ExecState execState; while (PlanExecutor::ADVANCED == (execState = exec->getNext(&o, NULL))) { + o = o.getOwned(); // we will be accessing outside of the lock // check to see if this is a new object we don't own yet // because of a chunk migration if (collMetadata) { diff --git a/src/mongo/db/commands/pipeline_command.cpp b/src/mongo/db/commands/pipeline_command.cpp index 6fe6de34381..03f8ec87e30 100644 --- a/src/mongo/db/commands/pipeline_command.cpp +++ b/src/mongo/db/commands/pipeline_command.cpp @@ -407,10 +407,6 @@ public: return appendCommandStatus(result, resolvedView.getStatus()); } - auto collationSpec = ctx.getView()->defaultCollator() - ? ctx.getView()->defaultCollator()->getSpec().toBSON().getOwned() - : CollationSpec::kSimpleSpec; - // With the view & collation resolved, we can relinquish locks. ctx.releaseLocksForView(); @@ -424,7 +420,6 @@ public: if (!newRequest.isOK()) { return appendCommandStatus(result, newRequest.getStatus()); } - newRequest.getValue().setCollation(collationSpec); bool status = runParsed( txn, origNss, newRequest.getValue(), newCmd.getValue(), errmsg, result); diff --git a/src/mongo/db/concurrency/SConscript b/src/mongo/db/concurrency/SConscript index 942b7a4f7f1..023d3b6b09e 100644 --- a/src/mongo/db/concurrency/SConscript +++ b/src/mongo/db/concurrency/SConscript @@ -13,6 +13,17 @@ env.Library( ) env.Library( + target='global_lock_acquisition_tracker', + source=[ + 'global_lock_acquisition_tracker.cpp' + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/util/decorable', + ] +) + + +env.Library( target='lock_manager', source=[ 'd_concurrency.cpp', @@ -21,6 +32,7 @@ env.Library( 'lock_stats.cpp', ], LIBDEPS=[ + 'global_lock_acquisition_tracker', '$BUILD_DIR/mongo/util/background_job', '$BUILD_DIR/mongo/base', # Temporary crutch since the ssl cleanup is hard coded in background.cpp @@ -44,6 +56,7 @@ env.CppUnitTest( LIBDEPS=[ '$BUILD_DIR/mongo/db/service_context_noop_init', '$BUILD_DIR/mongo/util/progress_meter', + 'global_lock_acquisition_tracker', 'lock_manager', ] ) diff --git a/src/mongo/db/concurrency/d_concurrency.cpp b/src/mongo/db/concurrency/d_concurrency.cpp index f7dabcc2248..6e7bd1ee7d6 100644 --- a/src/mongo/db/concurrency/d_concurrency.cpp +++ b/src/mongo/db/concurrency/d_concurrency.cpp @@ -34,6 +34,7 @@ #include <string> +#include "mongo/db/concurrency/global_lock_acquisition_tracker.h" #include "mongo/db/namespace_string.h" #include "mongo/db/server_parameters.h" #include "mongo/db/service_context.h" @@ -91,6 +92,13 @@ void Lock::GlobalLock::waitForLock(unsigned timeoutMs) { if (_result != LOCK_OK && _locker->shouldConflictWithSecondaryBatchApplication()) { _pbwm.unlock(); } + + if (_locker->isWriteLocked() && haveClient()) { + auto opCtx = cc().getOperationContext(); + if (opCtx) { + GlobalLockAcquisitionTracker::get(opCtx).setGlobalExclusiveLockTaken(); + } + } } void Lock::GlobalLock::_unlock() { diff --git a/src/mongo/db/concurrency/d_concurrency.h b/src/mongo/db/concurrency/d_concurrency.h index 6274814ca1c..b8df869089e 100644 --- a/src/mongo/db/concurrency/d_concurrency.h +++ b/src/mongo/db/concurrency/d_concurrency.h @@ -163,6 +163,9 @@ public: /** * Enqueues lock but does not block on lock acquisition. * Call waitForLock() to complete locking process. + * + * Does not set that the global lock was taken on the GlobalLockAcquisitionTracker. Call + * waitForLock to do so. */ GlobalLock(Locker* locker, LockMode lockMode, unsigned timeoutMs, EnqueueOnly enqueueOnly); @@ -171,7 +174,8 @@ public: } /** - * Waits for lock to be granted. + * Waits for lock to be granted. Sets that the global lock was taken on the + * GlobalLockAcquisitionTracker. */ void waitForLock(unsigned timeoutMs); diff --git a/src/mongo/db/concurrency/d_concurrency_test.cpp b/src/mongo/db/concurrency/d_concurrency_test.cpp index 22aae759650..799aae8a7e3 100644 --- a/src/mongo/db/concurrency/d_concurrency_test.cpp +++ b/src/mongo/db/concurrency/d_concurrency_test.cpp @@ -34,6 +34,7 @@ #include <vector> #include "mongo/db/concurrency/d_concurrency.h" +#include "mongo/db/concurrency/global_lock_acquisition_tracker.h" #include "mongo/db/concurrency/lock_manager_test_help.h" #include "mongo/db/operation_context.h" #include "mongo/stdx/functional.h" @@ -80,12 +81,13 @@ private: class UseGlobalThrottling { public: explicit UseGlobalThrottling(OperationContext* opCtx, int numTickets) - : _opCtx(opCtx), _holder(1) { + : _opCtx(opCtx), _holder(numTickets) { _opCtx->lockState()->setGlobalThrottling(&_holder, &_holder); } - ~UseGlobalThrottling() { + ~UseGlobalThrottling() noexcept(false) { // Reset the global setting as we're about to destroy the ticket holder. _opCtx->lockState()->setGlobalThrottling(nullptr, nullptr); + ASSERT_EQ(_holder.used(), 0); } private: @@ -115,6 +117,16 @@ makeKClientsWithLockers(int k) { } /** + * Returns an operation context that has an MMAPV1 locker attached to it. + */ +ServiceContext::UniqueOperationContext makeMMAPOperationContext() { + auto opCtx = cc().makeOperationContext(); + opCtx->releaseLockState(); + opCtx->setLockState(stdx::make_unique<MMAPV1LockerImpl>()); + return opCtx; +} + +/** * Calls fn the given number of iterations, spread out over up to maxThreads threads. * The threadNr passed is an integer between 0 and maxThreads exclusive. Logs timing * statistics for for all power-of-two thread counts from 1 up to maxThreds. @@ -292,6 +304,86 @@ TEST(DConcurrency, GlobalLockX_Timeout) { } } +TEST(DConcurrency, GlobalLockXSetsGlobalLockTakenOnOperationContext) { + Client::initThreadIfNotAlready(); + auto opCtx = makeMMAPOperationContext(); + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); + + { + Lock::GlobalLock globalWrite(opCtx->lockState(), MODE_X, 0); + ASSERT(globalWrite.isLocked()); + } + ASSERT_TRUE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); +} + +TEST(DConcurrency, GlobalLockIXSetsGlobalLockTakenOnOperationContext) { + Client::initThreadIfNotAlready(); + auto opCtx = makeMMAPOperationContext(); + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); + { + Lock::GlobalLock globalWrite(opCtx->lockState(), MODE_IX, 0); + ASSERT(globalWrite.isLocked()); + } + ASSERT_TRUE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); +} + +TEST(DConcurrency, GlobalLockSDoesNotSetGlobalLockTakenOnOperationContext) { + Client::initThreadIfNotAlready(); + auto opCtx = makeMMAPOperationContext(); + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); + { + Lock::GlobalLock globalRead(opCtx->lockState(), MODE_S, 0); + ASSERT(globalRead.isLocked()); + } + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); +} + +TEST(DConcurrency, GlobalLockISDoesNotSetGlobalLockTakenOnOperationContext) { + Client::initThreadIfNotAlready(); + auto opCtx = makeMMAPOperationContext(); + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); + { + Lock::GlobalLock globalRead(opCtx->lockState(), MODE_IS, 0); + ASSERT(globalRead.isLocked()); + } + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); +} + +TEST(DConcurrency, DBLockXSetsGlobalLockTakenOnOperationContext) { + Client::initThreadIfNotAlready(); + auto opCtx = makeMMAPOperationContext(); + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); + + { Lock::DBLock dbWrite(opCtx->lockState(), "db", MODE_X); } + ASSERT_TRUE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); +} + +TEST(DConcurrency, DBLockSDoesNotSetGlobalLockTakenOnOperationContext) { + Client::initThreadIfNotAlready(); + auto opCtx = makeMMAPOperationContext(); + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); + + { Lock::DBLock dbRead(opCtx->lockState(), "db", MODE_S); } + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); +} + +TEST(DConcurrency, GlobalLockXDoesNotSetGlobalLockTakenWhenLockAcquisitionTimesOut) { + Client::initThreadIfNotAlready(); + auto clients = makeKClientsWithLockers<MMAPV1LockerImpl>(1); + + // Take a global lock so that the next one times out. + Lock::GlobalLock globalWrite0(clients[0].second.get()->lockState(), MODE_X, 0); + ASSERT(globalWrite0.isLocked()); + + auto opCtx = makeMMAPOperationContext(); + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); + { + Lock::GlobalLock globalWrite1(opCtx->lockState(), MODE_X, 1); + ASSERT_FALSE(globalWrite1.isLocked()); + } + ASSERT_FALSE(GlobalLockAcquisitionTracker::get(opCtx.get()).getGlobalExclusiveLockTaken()); +} + TEST(DConcurrency, GlobalLockS_NoTimeoutDueToGlobalLockS) { MMAPV1LockerImpl ls; Lock::GlobalRead globalRead(&ls); @@ -720,6 +812,231 @@ TEST(DConcurrency, Throttling) { ASSERT(!overlongWait); } +TEST(DConcurrency, NoThrottlingWhenNotAcquiringTickets) { + auto clientOpctxPairs = makeKClientsWithLockers<DefaultLockerImpl>(2); + auto opctx1 = clientOpctxPairs[0].second.get(); + auto opctx2 = clientOpctxPairs[1].second.get(); + // Limit the locker to 1 ticket at a time. + UseGlobalThrottling throttle(opctx1, 1); + + // Prevent the enforcement of ticket throttling. + opctx1->lockState()->setShouldAcquireTicket(false); + + // Both locks should be acquired immediately because there is no throttling. + Lock::GlobalRead R1(opctx1->lockState(), 0); + ASSERT(R1.isLocked()); + + Lock::GlobalRead R2(opctx2->lockState(), 0); + ASSERT(R2.isLocked()); +} + +TEST(DConcurrency, CompatibleFirstWithSXIS) { + auto clientOpctxPairs = makeKClientsWithLockers<DefaultLockerImpl>(3); + auto opctx1 = clientOpctxPairs[0].second.get(); + auto opctx2 = clientOpctxPairs[1].second.get(); + auto opctx3 = clientOpctxPairs[2].second.get(); + + // Build a queue of MODE_S <- MODE_X <- MODE_IS, with MODE_S granted. + Lock::GlobalRead lockS(opctx1->lockState()); + ASSERT(lockS.isLocked()); + Lock::GlobalLock lockX(opctx2->lockState(), MODE_X, UINT_MAX, Lock::GlobalLock::EnqueueOnly()); + ASSERT(!lockX.isLocked()); + + // A MODE_IS should be granted due to compatibleFirst policy. + Lock::GlobalLock lockIS(opctx3->lockState(), MODE_IS, 0); + ASSERT(lockIS.isLocked()); + + lockX.waitForLock(0); + ASSERT(!lockX.isLocked()); +} + + +TEST(DConcurrency, CompatibleFirstWithXSIXIS) { + auto clientOpctxPairs = makeKClientsWithLockers<DefaultLockerImpl>(4); + auto opctx1 = clientOpctxPairs[0].second.get(); + auto opctx2 = clientOpctxPairs[1].second.get(); + auto opctx3 = clientOpctxPairs[2].second.get(); + auto opctx4 = clientOpctxPairs[3].second.get(); + + // Build a queue of MODE_X <- MODE_S <- MODE_IX <- MODE_IS, with MODE_X granted. + boost::optional<Lock::GlobalWrite> lockX; + lockX.emplace(opctx1->lockState()); + ASSERT(lockX->isLocked()); + boost::optional<Lock::GlobalLock> lockS; + lockS.emplace(opctx2->lockState(), MODE_S, UINT_MAX, Lock::GlobalLock::EnqueueOnly()); + ASSERT(!lockS->isLocked()); + Lock::GlobalLock lockIX( + opctx3->lockState(), MODE_IX, UINT_MAX, Lock::GlobalLock::EnqueueOnly()); + ASSERT(!lockIX.isLocked()); + Lock::GlobalLock lockIS( + opctx4->lockState(), MODE_IS, UINT_MAX, Lock::GlobalLock::EnqueueOnly()); + ASSERT(!lockIS.isLocked()); + + + // Now release the MODE_X and ensure that MODE_S will switch policy to compatibleFirst + lockX.reset(); + lockS->waitForLock(0); + ASSERT(lockS->isLocked()); + ASSERT(!lockIX.isLocked()); + lockIS.waitForLock(0); + ASSERT(lockIS.isLocked()); + + // Now release the MODE_S and ensure that MODE_IX gets locked. + lockS.reset(); + lockIX.waitForLock(0); + ASSERT(lockIX.isLocked()); +} + +TEST(DConcurrency, CompatibleFirstWithXSXIXIS) { + auto clientOpctxPairs = makeKClientsWithLockers<DefaultLockerImpl>(5); + auto opctx1 = clientOpctxPairs[0].second.get(); + auto opctx2 = clientOpctxPairs[1].second.get(); + auto opctx3 = clientOpctxPairs[2].second.get(); + auto opctx4 = clientOpctxPairs[3].second.get(); + auto opctx5 = clientOpctxPairs[4].second.get(); + + // Build a queue of MODE_X <- MODE_S <- MODE_X <- MODE_IX <- MODE_IS, with the first MODE_X + // granted and check that releasing it will result in the MODE_IS being granted. + boost::optional<Lock::GlobalWrite> lockXgranted; + lockXgranted.emplace(opctx1->lockState()); + ASSERT(lockXgranted->isLocked()); + + boost::optional<Lock::GlobalLock> lockX; + lockX.emplace(opctx3->lockState(), MODE_X, UINT_MAX, Lock::GlobalLock::EnqueueOnly()); + ASSERT(!lockX->isLocked()); + + // Now request MODE_S: it will be first in the pending list due to EnqueueAtFront policy. + boost::optional<Lock::GlobalLock> lockS; + lockS.emplace(opctx2->lockState(), MODE_S, UINT_MAX, Lock::GlobalLock::EnqueueOnly()); + ASSERT(!lockS->isLocked()); + + Lock::GlobalLock lockIX( + opctx4->lockState(), MODE_IX, UINT_MAX, Lock::GlobalLock::EnqueueOnly()); + ASSERT(!lockIX.isLocked()); + Lock::GlobalLock lockIS( + opctx5->lockState(), MODE_IS, UINT_MAX, Lock::GlobalLock::EnqueueOnly()); + ASSERT(!lockIS.isLocked()); + + + // Now release the granted MODE_X and ensure that MODE_S will switch policy to compatibleFirst, + // not locking the MODE_X or MODE_IX, but instead granting the final MODE_IS. + lockXgranted.reset(); + lockS->waitForLock(0); + ASSERT(lockS->isLocked()); + + lockX->waitForLock(0); + ASSERT(!lockX->isLocked()); + lockIX.waitForLock(0); + ASSERT(!lockIX.isLocked()); + + lockIS.waitForLock(0); + ASSERT(lockIS.isLocked()); +} + +TEST(DConcurrency, CompatibleFirstStress) { + int numThreads = 8; + int testMicros = 500000; + AtomicUInt64 readOnlyInterval{0}; + AtomicBool done{false}; + std::vector<uint64_t> acquisitionCount(numThreads); + std::vector<uint64_t> timeoutCount(numThreads); + std::vector<uint64_t> busyWaitCount(numThreads); + auto clientOpctxPairs = makeKClientsWithLockers<DefaultLockerImpl>(numThreads); + + // Do some busy waiting to trigger different timings. The atomic load prevents compilers + // from optimizing the loop away. + auto busyWait = [&done, &busyWaitCount](int threadId, long long iters) { + while (iters-- > 0) { + for (int i = 0; i < 100 && !done.load(); i++) { + busyWaitCount[threadId]++; + } + } + }; + + std::vector<stdx::thread> threads; + + // Thread putting state in/out of read-only CompatibleFirst mode. + threads.emplace_back([&]() { + Timer t; + auto endTime = t.micros() + testMicros; + uint64_t readOnlyIntervalCount = 0; + OperationContext* opCtx = clientOpctxPairs[0].second.get(); + for (int iters = 0; (t.micros() < endTime); iters++) { + busyWait(0, iters % 20); + Lock::GlobalRead readLock(opCtx->lockState(), iters % 2); + if (!readLock.isLocked()) { + timeoutCount[0]++; + continue; + } + acquisitionCount[0]++; + readOnlyInterval.store(++readOnlyIntervalCount); + busyWait(0, iters % 200); + readOnlyInterval.store(0); + }; + done.store(true); + }); + + for (int threadId = 1; threadId < numThreads; threadId++) { + threads.emplace_back([&, threadId]() { + Timer t; + for (int iters = 0; !done.load(); iters++) { + OperationContext* opCtx = clientOpctxPairs[threadId].second.get(); + boost::optional<Lock::GlobalLock> lock; + switch (threadId) { + case 1: + case 2: + case 3: + case 4: { + // Here, actually try to acquire a lock without waiting, and check whether + // we should have gotten the lock or not. Use MODE_IS in 95% of the cases, + // and MODE_S in only 5, as that stressing the partitioning scheme and + // policy changes more as thread 0 acquires/releases its MODE_S lock. + busyWait(threadId, iters % 100); + auto interval = readOnlyInterval.load(); + lock.emplace(opCtx->lockState(), + iters % 20 ? MODE_IS : MODE_S, + 0, + Lock::GlobalLock::EnqueueOnly()); + // If thread 0 is holding the MODE_S lock while we tried to acquire a + // MODE_IS or MODE_S lock, the CompatibleFirst policy guarantees success. + auto newInterval = readOnlyInterval.load(); + invariant(!interval || interval != newInterval || lock->isLocked()); + lock->waitForLock(0); + break; + } + case 5: + busyWait(threadId, iters % 150); + lock.emplace(opCtx->lockState(), MODE_X, iters % 2); + busyWait(threadId, iters % 10); + break; + case 6: + lock.emplace(opCtx->lockState(), iters % 25 ? MODE_IX : MODE_S, iters % 2); + busyWait(threadId, iters % 100); + break; + case 7: + busyWait(threadId, iters % 100); + lock.emplace(opCtx->lockState(), iters % 20 ? MODE_IS : MODE_X, 0); + break; + default: + MONGO_UNREACHABLE; + } + if (lock->isLocked()) + acquisitionCount[threadId]++; + else + timeoutCount[threadId]++; + }; + }); + } + + for (auto& thread : threads) + thread.join(); + for (int threadId = 0; threadId < numThreads; threadId++) { + log() << "thread " << threadId << " stats: " << acquisitionCount[threadId] + << " acquisitions, " << timeoutCount[threadId] << " timeouts, " + << busyWaitCount[threadId] / 1000000 << "M busy waits"; + } +} + // These tests exercise single- and multi-threaded performance of uncontended lock acquisition. It // is neither practical nor useful to run them on debug builds. diff --git a/src/mongo/db/concurrency/global_lock_acquisition_tracker.cpp b/src/mongo/db/concurrency/global_lock_acquisition_tracker.cpp new file mode 100644 index 00000000000..21ac9272c93 --- /dev/null +++ b/src/mongo/db/concurrency/global_lock_acquisition_tracker.cpp @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/concurrency/global_lock_acquisition_tracker.h" + +namespace mongo { + +const OperationContext::Decoration<GlobalLockAcquisitionTracker> GlobalLockAcquisitionTracker::get = + OperationContext::declareDecoration<GlobalLockAcquisitionTracker>(); + +bool GlobalLockAcquisitionTracker::getGlobalExclusiveLockTaken() const { + return _globalExclusiveLockTaken; +} + +void GlobalLockAcquisitionTracker::setGlobalExclusiveLockTaken() { + _globalExclusiveLockTaken = true; +} +} // namespace mongo diff --git a/src/mongo/db/s/collection_range_deleter.h b/src/mongo/db/concurrency/global_lock_acquisition_tracker.h index 9d2e1ae8cb4..5bd68453f57 100644 --- a/src/mongo/db/s/collection_range_deleter.h +++ b/src/mongo/db/concurrency/global_lock_acquisition_tracker.h @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 MongoDB Inc. + * Copyright (C) 2017 MongoDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, @@ -25,53 +25,38 @@ * exception statement from all source files in the program, then also delete * it in the license file. */ + #pragma once -#include "mongo/base/disallow_copying.h" -#include "mongo/db/namespace_string.h" -#include "mongo/s/catalog/type_chunk.h" +#include "mongo/db/operation_context.h" namespace mongo { -class BSONObj; -class Collection; -class OperationContext; - -class CollectionRangeDeleter { - MONGO_DISALLOW_COPYING(CollectionRangeDeleter); - +/** + * The GlobalLockAcquisitionTracker keeps track of if the global lock has ever been taken in X or + * IX mode. This class is used to track if we ever did a transaction with the intent to do a + * write, so that we can enforce write concern on noop writes. + */ +class GlobalLockAcquisitionTracker { public: - CollectionRangeDeleter(NamespaceString nss); + static const OperationContext::Decoration<GlobalLockAcquisitionTracker> get; - /** - * Starts deleting ranges and cleans up this object when it is finished. - */ - void run(); + // Decoration requires a default constructor. + GlobalLockAcquisitionTracker() = default; /** - * Acquires the collection IX lock and checks whether there are new entries for the collection's - * rangesToClean structure. If there are, deletes some small amount of entries and yields using - * the standard query yielding logic. - * - * Returns true if there are more entries in rangesToClean, false if there is no more progress - * to be made. + * Returns whether we have ever taken a global lock in X or IX mode in this operation. */ - bool cleanupNextRange(OperationContext* txn); + bool getGlobalExclusiveLockTaken() const; -private: /** - * Performs the deletion of a small amount of entries within the range in progress. - * This function will invariant if called while _rangeInProgress is not set. - * - * Returns the number of documents deleted (0 if deletion is finished), or -1 for error. + * Sets that we have ever taken a global lock in X or IX mode in this operation. */ - int _doDeletion(OperationContext* txn, Collection* collection, const BSONObj& keyPattern); + void setGlobalExclusiveLockTaken(); - NamespaceString _nss; - - // Holds a range for which deletion has begun. If empty, then a new range - // must be requested from rangesToClean - boost::optional<ChunkRange> _rangeInProgress; +private: + // Set to true when the global lock is first taken in X or IX mode. Never set back to false. + bool _globalExclusiveLockTaken = false; }; } // namespace mongo diff --git a/src/mongo/db/concurrency/lock_manager.cpp b/src/mongo/db/concurrency/lock_manager.cpp index f90141e79be..8ae7919012e 100644 --- a/src/mongo/db/concurrency/lock_manager.cpp +++ b/src/mongo/db/concurrency/lock_manager.cpp @@ -592,7 +592,9 @@ bool LockManager::unlock(LockRequest* request) { lock->decGrantedModeCount(request->mode); if (request->compatibleFirst) { + invariant(lock->compatibleFirstCount > 0); lock->compatibleFirstCount--; + invariant(lock->compatibleFirstCount == 0 || !lock->grantedList.empty()); } _onLockModeChanged(lock, lock->grantedCounts[request->mode] == 0); diff --git a/src/mongo/db/concurrency/lock_state.cpp b/src/mongo/db/concurrency/lock_state.cpp index 45ea279c137..3625cef24f7 100644 --- a/src/mongo/db/concurrency/lock_state.cpp +++ b/src/mongo/db/concurrency/lock_state.cpp @@ -311,7 +311,7 @@ LockResult LockerImpl<IsForMMAPV1>::_lockGlobalBegin(LockMode mode, Milliseconds dassert(isLocked() == (_modeForTicket != MODE_NONE)); if (_modeForTicket == MODE_NONE) { const bool reader = isSharedLockMode(mode); - auto holder = ticketHolders[mode]; + auto holder = shouldAcquireTicket() ? ticketHolders[mode] : nullptr; if (holder) { _clientState.store(reader ? kQueuedReader : kQueuedWriter); if (timeout == Milliseconds::max()) { @@ -787,7 +787,10 @@ LockResult LockerImpl<IsForMMAPV1>::lockComplete(ResourceId resId, } } - // Cleanup the state, since this is an unused lock now + // Cleanup the state, since this is an unused lock now. + // Note: in case of the _notify object returning LOCK_TIMEOUT, it is possible to find that the + // lock was still granted after all, but we don't try to take advantage of that and will return + // a timeout. if (result != LOCK_OK) { LockRequestsMap::Iterator it = _requests.find(resId); _unlockImpl(&it); @@ -807,7 +810,7 @@ bool LockerImpl<IsForMMAPV1>::_unlockImpl(LockRequestsMap::Iterator* it) { if (globalLockManager.unlock(it->objAddr())) { if (it->key() == resourceIdGlobal) { invariant(_modeForTicket != MODE_NONE); - auto holder = ticketHolders[_modeForTicket]; + auto holder = shouldAcquireTicket() ? ticketHolders[_modeForTicket] : nullptr; _modeForTicket = MODE_NONE; if (holder) { holder->release(); diff --git a/src/mongo/db/concurrency/locker.h b/src/mongo/db/concurrency/locker.h index 37af569124f..c1443865326 100644 --- a/src/mongo/db/concurrency/locker.h +++ b/src/mongo/db/concurrency/locker.h @@ -334,11 +334,25 @@ public: return _shouldConflictWithSecondaryBatchApplication; } + /** + * If set to false, this opts out of the ticket mechanism. This should be used sparingly + * for special purpose threads, such as FTDC. + */ + void setShouldAcquireTicket(bool newValue) { + invariant(!isLocked()); + _shouldAcquireTicket = newValue; + } + bool shouldAcquireTicket() const { + return _shouldAcquireTicket; + } + + protected: Locker() {} private: bool _shouldConflictWithSecondaryBatchApplication = true; + bool _shouldAcquireTicket = true; }; } // namespace mongo diff --git a/src/mongo/db/db.cpp b/src/mongo/db/db.cpp index 2dbb2b1065e..c6272425bf5 100644 --- a/src/mongo/db/db.cpp +++ b/src/mongo/db/db.cpp @@ -103,10 +103,10 @@ #include "mongo/db/startup_warnings_mongod.h" #include "mongo/db/stats/counters.h" #include "mongo/db/stats/snapshots.h" +#include "mongo/db/storage/encryption_hooks.h" #include "mongo/db/storage/mmap_v1/mmap_v1_options.h" #include "mongo/db/storage/storage_engine.h" #include "mongo/db/storage/storage_options.h" -#include "mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.h" #include "mongo/db/ttl.h" #include "mongo/db/wire_version.h" #include "mongo/executor/network_interface_factory.h" @@ -596,7 +596,7 @@ ExitCode _initAndListen(int listenPort) { getGlobalServiceContext()->initializeGlobalStorageEngine(); #ifdef MONGO_CONFIG_WIREDTIGER_ENABLED - if (WiredTigerCustomizationHooks::get(getGlobalServiceContext())->restartRequired()) { + if (EncryptionHooks::get(getGlobalServiceContext())->restartRequired()) { exitCleanly(EXIT_CLEAN); } #endif @@ -704,7 +704,11 @@ ExitCode _initAndListen(int listenPort) { Status status = authindex::verifySystemIndexes(startupOpCtx.get()); if (!status.isOK()) { log() << redact(status); - exitCleanly(EXIT_NEED_UPGRADE); + if (status.code() == ErrorCodes::AuthSchemaIncompatible) { + exitCleanly(EXIT_NEED_UPGRADE); + } else { + quickExit(EXIT_FAILURE); + } } // SERVER-14090: Verify that auth schema version is schemaVersion26Final. @@ -752,7 +756,7 @@ ExitCode _initAndListen(int listenPort) { if (!storageGlobalParams.readOnly) { logStartup(startupOpCtx.get()); - startFTDC(); + startMongoDFTDC(); getDeleter()->startWorkers(); @@ -1108,7 +1112,7 @@ static void shutdownTask() { #endif // Shutdown Full-Time Data Capture - stopFTDC(); + stopMongoDFTDC(); if (txn) { ShardingState::get(txn)->shutDown(txn); diff --git a/src/mongo/db/dbhelpers.cpp b/src/mongo/db/dbhelpers.cpp index ebed6807c7e..96eb011018d 100644 --- a/src/mongo/db/dbhelpers.cpp +++ b/src/mongo/db/dbhelpers.cpp @@ -62,8 +62,8 @@ #include "mongo/db/s/sharding_state.h" #include "mongo/db/service_context.h" #include "mongo/db/storage/data_protector.h" +#include "mongo/db/storage/encryption_hooks.h" #include "mongo/db/storage/storage_options.h" -#include "mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.h" #include "mongo/db/write_concern.h" #include "mongo/db/write_concern_options.h" #include "mongo/s/shard_key_pattern.h" @@ -270,6 +270,7 @@ long long Helpers::removeRange(OperationContext* txn, const KeyRange& range, BoundInclusion boundInclusion, const WriteConcernOptions& writeConcern, + Milliseconds& replWaitDuration, RemoveSaver* callback, bool fromMigrate, bool onlyRemoveOrphanedDocs) { @@ -283,6 +284,7 @@ long long Helpers::removeRange(OperationContext* txn, BSONObj max; { + ScopedTransaction scopedXact(txn, MODE_IS); AutoGetCollectionForRead ctx(txn, ns); Collection* collection = ctx.getCollection(); if (!collection) { @@ -326,11 +328,12 @@ long long Helpers::removeRange(OperationContext* txn, long long numDeleted = 0; - Milliseconds millisWaitingForReplication{0}; + replWaitDuration = Milliseconds::zero(); while (1) { // Scoping for write lock. { + ScopedTransaction scopedXact(txn, MODE_IX); AutoGetCollection ctx(txn, NamespaceString(ns), MODE_IX, MODE_IX); Collection* collection = ctx.getCollection(); if (!collection) @@ -438,20 +441,21 @@ long long Helpers::removeRange(OperationContext* txn, txn, repl::ReplClientInfo::forClient(txn->getClient()).getLastOp(), writeConcern); - if (replStatus.status.code() == ErrorCodes::ExceededTimeLimit) { + if (replStatus.status.code() == ErrorCodes::ExceededTimeLimit || + replStatus.status.code() == ErrorCodes::WriteConcernFailed) { warning(LogComponent::kSharding) << "replication to secondaries for removeRange at " "least 60 seconds behind"; } else { uassertStatusOK(replStatus.status); } - millisWaitingForReplication += replStatus.duration; + replWaitDuration += replStatus.duration; } } if (writeConcern.shouldWaitForOtherNodes()) log(LogComponent::kSharding) << "Helpers::removeRangeUnlocked time spent waiting for replication: " - << durationCount<Milliseconds>(millisWaitingForReplication) << "ms" << endl; + << durationCount<Milliseconds>(replWaitDuration) << "ms" << endl; MONGO_LOG_COMPONENT(1, LogComponent::kSharding) << "end removal of " << min << " to " << max << " in " << ns << " (took " @@ -485,19 +489,19 @@ Helpers::RemoveSaver::RemoveSaver(const string& a, const string& b, const string ss << why << "." << terseCurrentTime(false) << "." << NUM++ << ".bson"; _file /= ss.str(); - auto hooks = WiredTigerCustomizationHooks::get(getGlobalServiceContext()); - if (hooks->enabled()) { - _protector = hooks->getDataProtector(); - _file += hooks->getProtectedPathSuffix(); + auto encryptionHooks = EncryptionHooks::get(getGlobalServiceContext()); + if (encryptionHooks->enabled()) { + _protector = encryptionHooks->getDataProtector(); + _file += encryptionHooks->getProtectedPathSuffix(); } } Helpers::RemoveSaver::~RemoveSaver() { if (_protector && _out) { - auto hooks = WiredTigerCustomizationHooks::get(getGlobalServiceContext()); - invariant(hooks->enabled()); + auto encryptionHooks = EncryptionHooks::get(getGlobalServiceContext()); + invariant(encryptionHooks->enabled()); - size_t protectedSizeMax = hooks->additionalBytesForProtectedBuffer(); + size_t protectedSizeMax = encryptionHooks->additionalBytesForProtectedBuffer(); std::unique_ptr<uint8_t[]> protectedBuffer(new uint8_t[protectedSizeMax]); size_t resultLen; @@ -560,10 +564,10 @@ Status Helpers::RemoveSaver::goingToDelete(const BSONObj& o) { std::unique_ptr<uint8_t[]> protectedBuffer; if (_protector) { - auto hooks = WiredTigerCustomizationHooks::get(getGlobalServiceContext()); - invariant(hooks->enabled()); + auto encryptionHooks = EncryptionHooks::get(getGlobalServiceContext()); + invariant(encryptionHooks->enabled()); - size_t protectedSizeMax = dataSize + hooks->additionalBytesForProtectedBuffer(); + size_t protectedSizeMax = dataSize + encryptionHooks->additionalBytesForProtectedBuffer(); protectedBuffer.reset(new uint8_t[protectedSizeMax]); size_t resultLen; diff --git a/src/mongo/db/dbhelpers.h b/src/mongo/db/dbhelpers.h index 650f265265d..59a4a317db7 100644 --- a/src/mongo/db/dbhelpers.h +++ b/src/mongo/db/dbhelpers.h @@ -141,11 +141,13 @@ struct Helpers { /** * Takes a namespace range, specified by a min and max and qualified by an index pattern, * and removes all the documents in that range found by iterating - * over the given index. Caller is responsible for insuring that min/max are + * over the given index. Caller is responsible for ensuring that min/max are * compatible with the given keyPattern (e.g min={a:100} is compatible with * keyPattern={a:1,b:1} since it can be extended to {a:100,b:minKey}, but * min={b:100} is not compatible). * + * Returns time spent waiting for majority replication in replWaitDuration. + * * Caller must hold a write lock on 'ns' * * Returns -1 when no usable index exists @@ -157,6 +159,7 @@ struct Helpers { const KeyRange& range, BoundInclusion boundInclusion, const WriteConcernOptions& secondaryThrottle, + Milliseconds& replWaitDuration, RemoveSaver* callback = NULL, bool fromMigrate = false, bool onlyRemoveOrphanedDocs = false); diff --git a/src/mongo/db/dbwebserver.cpp b/src/mongo/db/dbwebserver.cpp index b202e0500b9..50fa68a8176 100644 --- a/src/mongo/db/dbwebserver.cpp +++ b/src/mongo/db/dbwebserver.cpp @@ -65,6 +65,7 @@ namespace mongo { using std::map; +using std::string; using std::stringstream; using std::vector; diff --git a/src/mongo/db/ftdc/SConscript b/src/mongo/db/ftdc/SConscript index 0d16aec93f2..81df9ad0f4d 100644 --- a/src/mongo/db/ftdc/SConscript +++ b/src/mongo/db/ftdc/SConscript @@ -21,7 +21,6 @@ ftdcEnv.Library( LIBDEPS=[ '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/bson/util/bson_extract', - '$BUILD_DIR/mongo/db/server_options_core', '$BUILD_DIR/mongo/db/service_context', '$BUILD_DIR/third_party/s2/s2', # For VarInt '$BUILD_DIR/third_party/shim_zlib', @@ -40,24 +39,44 @@ elif env.TargetOSIs('windows'): ] env.Library( - target='ftdc_mongod', + target='ftdc_server', source=[ - 'ftdc_commands.cpp', - 'ftdc_mongod.cpp', + 'ftdc_server.cpp', 'ftdc_system_stats.cpp', 'ftdc_system_stats_${TARGET_OS}.cpp', ], LIBDEPS=[ '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/db/commands', - '$BUILD_DIR/mongo/db/repl/repl_coordinator_global', '$BUILD_DIR/mongo/db/server_parameters', - '$BUILD_DIR/mongo/db/storage/storage_options', '$BUILD_DIR/mongo/util/processinfo', 'ftdc' ] + platform_libs, ) +env.Library( + target='ftdc_mongod', + source=[ + 'ftdc_commands.cpp', + 'ftdc_mongod.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/db/repl/repl_coordinator_global', + '$BUILD_DIR/mongo/db/storage/storage_options', + 'ftdc_server' + ], +) + +env.Library( + target='ftdc_mongos', + source=[ + 'ftdc_mongos.cpp', + ], + LIBDEPS=[ + 'ftdc_server' + ], +) + env.CppUnitTest( target='ftdc_test', source=[ diff --git a/src/mongo/db/ftdc/collector.cpp b/src/mongo/db/ftdc/collector.cpp index 611f12dff5a..7c35fb12bb6 100644 --- a/src/mongo/db/ftdc/collector.cpp +++ b/src/mongo/db/ftdc/collector.cpp @@ -67,6 +67,7 @@ std::tuple<BSONObj, Date_t> FTDCCollectorCollection::collect(Client* client) { // batches that are taking a long time. auto txn = client->makeOperationContext(); txn->lockState()->setShouldConflictWithSecondaryBatchApplication(false); + txn->lockState()->setShouldAcquireTicket(false); for (auto& collector : _collectors) { BSONObjBuilder subObjBuilder(builder.subobjStart(collector->name())); diff --git a/src/mongo/db/ftdc/compressor_test.cpp b/src/mongo/db/ftdc/compressor_test.cpp index 688197a392a..0c01bd58040 100644 --- a/src/mongo/db/ftdc/compressor_test.cpp +++ b/src/mongo/db/ftdc/compressor_test.cpp @@ -368,7 +368,7 @@ TEST(FTDCCompressor, TestNumbersCompat) { } // Test various date time types -TEST(AFTDCCompressor, TestDateTimeTypes) { +TEST(FTDCCompressor, TestDateTimeTypes) { TestTie c; for (int i = 0; i < 10; i++) { BSONObjBuilder builder1; diff --git a/src/mongo/db/ftdc/constants.h b/src/mongo/db/ftdc/constants.h index a989fd35b64..3fe5191ff44 100644 --- a/src/mongo/db/ftdc/constants.h +++ b/src/mongo/db/ftdc/constants.h @@ -26,6 +26,10 @@ * then also delete it in the license file. */ +#pragma once + +#include "mongo/base/string_data.h" + namespace mongo { extern const char kFTDCInterimFile[]; @@ -42,4 +46,6 @@ extern const char kFTDCDocsField[]; extern const char kFTDCCollectStartField[]; extern const char kFTDCCollectEndField[]; +constexpr StringData kFTDCDefaultDirectory = "diagnostic.data"_sd; + } // namespace mongo diff --git a/src/mongo/db/ftdc/controller.cpp b/src/mongo/db/ftdc/controller.cpp index 9e2987db0f9..bc3d4df444c 100644 --- a/src/mongo/db/ftdc/controller.cpp +++ b/src/mongo/db/ftdc/controller.cpp @@ -47,9 +47,19 @@ namespace mongo { -void FTDCController::setEnabled(bool enabled) { +Status FTDCController::setEnabled(bool enabled) { stdx::lock_guard<stdx::mutex> lock(_mutex); + + if (_path.empty()) { + return Status(ErrorCodes::FTDCPathNotSet, + str::stream() << "FTDC cannot be enabled without setting the set parameter " + "'diagnosticDataCollectionDirectoryPath' first."); + } + _configTemp.enabled = enabled; + _condvar.notify_one(); + + return Status::OK(); } void FTDCController::setPeriod(Milliseconds millis) { @@ -82,6 +92,23 @@ void FTDCController::setMaxSamplesPerInterimMetricChunk(size_t size) { _condvar.notify_one(); } +Status FTDCController::setDirectory(const boost::filesystem::path& path) { + stdx::lock_guard<stdx::mutex> lock(_mutex); + + if (!_path.empty()) { + return Status(ErrorCodes::FTDCPathAlreadySet, + str::stream() << "FTDC path has already been set to '" << _path.string() + << "'. It cannot be changed."); + } + + _path = path; + + // Do not notify for the change since it has to be enabled via setEnabled. + + return Status::OK(); +} + + void FTDCController::addPeriodicCollector(std::unique_ptr<FTDCCollectorInterface> collector) { { stdx::lock_guard<stdx::mutex> lock(_mutex); @@ -203,7 +230,7 @@ void FTDCController::doLoop() { } // TODO: consider only running this thread if we are enabled - // for now, we just keep an idle thread as it is simplier + // for now, we just keep an idle thread as it is simpler if (_config.enabled) { // Delay initialization of FTDCFileManager until we are sure the user has enabled // FTDC diff --git a/src/mongo/db/ftdc/controller.h b/src/mongo/db/ftdc/controller.h index 44b5e838506..e73d570598d 100644 --- a/src/mongo/db/ftdc/controller.h +++ b/src/mongo/db/ftdc/controller.h @@ -62,8 +62,12 @@ public: /* * Set whether the controller is enabled, and collects data. + * + * Returns ErrorCodes::FTDCPathNotSet if no log path has been specified for FTDC. This occurs + * in MongoS in some situations since MongoS is not required to have a storage directory like + * MongoD does. */ - void setEnabled(bool enabled); + Status setEnabled(bool enabled); /** * Set the period for data collection. @@ -94,6 +98,13 @@ public: */ void setMaxSamplesPerInterimMetricChunk(size_t size); + /* + * Set the path to store FTDC files if not already set. + * + * Returns ErrorCodes::FTDCPathAlreadySet if the path has already been set. + */ + Status setDirectory(const boost::filesystem::path& path); + /** * Add a metric collector to collect periodically. i.e., serverStatus */ @@ -172,7 +183,7 @@ private: State _state{State::kNotStarted}; // Directory to store files - const boost::filesystem::path _path; + boost::filesystem::path _path; // Mutex to protect the condvar, configuration changes, and most recent periodic document. stdx::mutex _mutex; diff --git a/src/mongo/db/ftdc/controller_test.cpp b/src/mongo/db/ftdc/controller_test.cpp index 365f06580cd..c75a3fe44d6 100644 --- a/src/mongo/db/ftdc/controller_test.cpp +++ b/src/mongo/db/ftdc/controller_test.cpp @@ -254,7 +254,7 @@ TEST(FTDCControllerTest, TestStartAsDisabled) { ASSERT_EQUALS(files0.size(), 0UL); - c.setEnabled(true); + ASSERT_OK(c.setEnabled(true)); c1Ptr->setSignalOnCount(50); diff --git a/src/mongo/db/ftdc/ftdc_mongod.cpp b/src/mongo/db/ftdc/ftdc_mongod.cpp index 094a2b05d06..1f53ec345ea 100644 --- a/src/mongo/db/ftdc/ftdc_mongod.cpp +++ b/src/mongo/db/ftdc/ftdc_mongod.cpp @@ -31,295 +31,18 @@ #include "mongo/db/ftdc/ftdc_mongod.h" #include <boost/filesystem.hpp> -#include <fstream> -#include <memory> -#include "mongo/base/init.h" -#include "mongo/base/status.h" -#include "mongo/bson/bsonobjbuilder.h" -#include "mongo/db/commands.h" -#include "mongo/db/ftdc/collector.h" -#include "mongo/db/ftdc/config.h" +#include "mongo/db/ftdc/constants.h" #include "mongo/db/ftdc/controller.h" -#include "mongo/db/ftdc/ftdc_system_stats.h" -#include "mongo/db/jsobj.h" +#include "mongo/db/ftdc/ftdc_server.h" #include "mongo/db/repl/replication_coordinator.h" #include "mongo/db/repl/replication_coordinator_global.h" -#include "mongo/db/server_parameters.h" -#include "mongo/db/service_context.h" #include "mongo/db/storage/storage_options.h" namespace mongo { namespace { - -const auto getFTDCController = ServiceContext::declareDecoration<std::unique_ptr<FTDCController>>(); - -FTDCController* getGlobalFTDCController() { - if (!hasGlobalServiceContext()) { - return nullptr; - } - - return getFTDCController(getGlobalServiceContext()).get(); -} - -std::atomic<bool> localEnabledFlag(FTDCConfig::kEnabledDefault); // NOLINT - -class ExportedFTDCEnabledParameter - : public ExportedServerParameter<bool, ServerParameterType::kStartupAndRuntime> { -public: - ExportedFTDCEnabledParameter() - : ExportedServerParameter<bool, ServerParameterType::kStartupAndRuntime>( - ServerParameterSet::getGlobal(), - "diagnosticDataCollectionEnabled", - &localEnabledFlag) {} - - virtual Status validate(const bool& potentialNewValue) { - auto controller = getGlobalFTDCController(); - if (controller) { - controller->setEnabled(potentialNewValue); - } - - return Status::OK(); - } - -} exportedFTDCEnabledParameter; - -std::atomic<std::int32_t> localPeriodMillis(FTDCConfig::kPeriodMillisDefault); // NOLINT - -class ExportedFTDCPeriodParameter - : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { -public: - ExportedFTDCPeriodParameter() - : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( - ServerParameterSet::getGlobal(), - "diagnosticDataCollectionPeriodMillis", - &localPeriodMillis) {} - - virtual Status validate(const std::int32_t& potentialNewValue) { - if (potentialNewValue < 100) { - return Status( - ErrorCodes::BadValue, - "diagnosticDataCollectionPeriodMillis must be greater than or equal to 100ms"); - } - - auto controller = getGlobalFTDCController(); - if (controller) { - controller->setPeriod(Milliseconds(potentialNewValue)); - } - - return Status::OK(); - } - -} exportedFTDCPeriodParameter; - -// Scale the values down since are defaults are in bytes, but the user interface is MB -std::atomic<std::int32_t> localMaxDirectorySizeMB( // NOLINT - FTDCConfig::kMaxDirectorySizeBytesDefault / (1024 * 1024)); - -std::atomic<std::int32_t> localMaxFileSizeMB(FTDCConfig::kMaxFileSizeBytesDefault / // NOLINT - (1024 * 1024)); - -class ExportedFTDCDirectorySizeParameter - : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { -public: - ExportedFTDCDirectorySizeParameter() - : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( - ServerParameterSet::getGlobal(), - "diagnosticDataCollectionDirectorySizeMB", - &localMaxDirectorySizeMB) {} - - virtual Status validate(const std::int32_t& potentialNewValue) { - if (potentialNewValue < 10) { - return Status( - ErrorCodes::BadValue, - "diagnosticDataCollectionDirectorySizeMB must be greater than or equal to 10"); - } - - if (potentialNewValue < localMaxFileSizeMB) { - return Status( - ErrorCodes::BadValue, - str::stream() - << "diagnosticDataCollectionDirectorySizeMB must be greater than or equal to '" - << localMaxFileSizeMB - << "' which is the current value of diagnosticDataCollectionFileSizeMB."); - } - - auto controller = getGlobalFTDCController(); - if (controller) { - controller->setMaxDirectorySizeBytes(potentialNewValue * 1024 * 1024); - } - - return Status::OK(); - } - -} exportedFTDCDirectorySizeParameter; - -class ExportedFTDCFileSizeParameter - : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { -public: - ExportedFTDCFileSizeParameter() - : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( - ServerParameterSet::getGlobal(), - "diagnosticDataCollectionFileSizeMB", - &localMaxFileSizeMB) {} - - virtual Status validate(const std::int32_t& potentialNewValue) { - if (potentialNewValue < 1) { - return Status(ErrorCodes::BadValue, - "diagnosticDataCollectionFileSizeMB must be greater than or equal to 1"); - } - - if (potentialNewValue > localMaxDirectorySizeMB) { - return Status( - ErrorCodes::BadValue, - str::stream() - << "diagnosticDataCollectionFileSizeMB must be less than or equal to '" - << localMaxDirectorySizeMB - << "' which is the current value of diagnosticDataCollectionDirectorySizeMB."); - } - - auto controller = getGlobalFTDCController(); - if (controller) { - controller->setMaxFileSizeBytes(potentialNewValue * 1024 * 1024); - } - - return Status::OK(); - } - -} exportedFTDCFileSizeParameter; - -std::atomic<std::int32_t> localMaxSamplesPerArchiveMetricChunk( // NOLINT - FTDCConfig::kMaxSamplesPerArchiveMetricChunkDefault); - -class ExportedFTDCArchiveChunkSizeParameter - : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { -public: - ExportedFTDCArchiveChunkSizeParameter() - : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( - ServerParameterSet::getGlobal(), - "diagnosticDataCollectionSamplesPerChunk", - &localMaxSamplesPerArchiveMetricChunk) {} - - virtual Status validate(const std::int32_t& potentialNewValue) { - if (potentialNewValue < 2) { - return Status( - ErrorCodes::BadValue, - "diagnosticDataCollectionSamplesPerChunk must be greater than or equal to 2"); - } - - auto controller = getGlobalFTDCController(); - if (controller) { - controller->setMaxSamplesPerArchiveMetricChunk(potentialNewValue); - } - - return Status::OK(); - } - -} exportedFTDCArchiveChunkSizeParameter; - -std::atomic<std::int32_t> localMaxSamplesPerInterimMetricChunk( // NOLINT - FTDCConfig::kMaxSamplesPerInterimMetricChunkDefault); - -class ExportedFTDCInterimChunkSizeParameter - : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { -public: - ExportedFTDCInterimChunkSizeParameter() - : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( - ServerParameterSet::getGlobal(), - "diagnosticDataCollectionSamplesPerInterimUpdate", - &localMaxSamplesPerInterimMetricChunk) {} - - virtual Status validate(const std::int32_t& potentialNewValue) { - if (potentialNewValue < 2) { - return Status(ErrorCodes::BadValue, - "diagnosticDataCollectionSamplesPerInterimUpdate must be greater than or " - "equal to 2"); - } - - auto controller = getGlobalFTDCController(); - if (controller) { - controller->setMaxSamplesPerInterimMetricChunk(potentialNewValue); - } - - return Status::OK(); - } - -} exportedFTDCInterimChunkSizeParameter; - -class FTDCSimpleInternalCommandCollector final : public FTDCCollectorInterface { -public: - FTDCSimpleInternalCommandCollector(StringData command, - StringData name, - StringData ns, - BSONObj cmdObj) - : _name(name.toString()), _ns(ns.toString()), _cmdObj(std::move(cmdObj)) { - _command = Command::findCommand(command); - invariant(_command); - } - - void collect(OperationContext* txn, BSONObjBuilder& builder) override { - std::string errmsg; - - bool ret = _command->run(txn, _ns, _cmdObj, 0, errmsg, builder); - - // Some commands return errmsgs when they return false (collstats) - // Some commands return bson objs when they return false (replGetStatus) - // We append the status as needed to ensure readers of the collected data can check the - // status of any individual command. - _command->appendCommandStatus(builder, ret, errmsg); - } - - std::string name() const override { - return _name; - } - -private: - std::string _name; - std::string _ns; - BSONObj _cmdObj; - - // Not owned - Command* _command; -}; - -} // namespace - -// Register the FTDC system -// Note: This must be run before the server parameters are parsed during startup -// so that the FTDCController is initialized. -// -void startFTDC() { - boost::filesystem::path dir(storageGlobalParams.dbpath); - dir /= "diagnostic.data"; - - - FTDCConfig config; - config.period = Milliseconds(localPeriodMillis.load()); - config.enabled = localEnabledFlag; - config.maxFileSizeBytes = localMaxFileSizeMB * 1024 * 1024; - config.maxDirectorySizeBytes = localMaxDirectorySizeMB * 1024 * 1024; - config.maxSamplesPerArchiveMetricChunk = localMaxSamplesPerArchiveMetricChunk; - config.maxSamplesPerInterimMetricChunk = localMaxSamplesPerInterimMetricChunk; - - auto controller = stdx::make_unique<FTDCController>(dir, config); - - // Install periodic collectors - // These are collected on the period interval in FTDCConfig. - // NOTE: For each command here, there must be an equivalent privilege check in - // GetDiagnosticDataCommand - - // CmdServerStatus - // The "sharding" section is filtered out because at this time it only consists of strings in - // migration status. This section triggers too many schema changes in the serverStatus which - // hurt ftdc compression efficiency, because its output varies depending on the list of active - // migrations. - controller->addPeriodicCollector(stdx::make_unique<FTDCSimpleInternalCommandCollector>( - "serverStatus", - "serverStatus", - "", - BSON("serverStatus" << 1 << "tcMalloc" << true << "sharding" << false))); - +void registerMongoDCollectors(FTDCController* controller) { // These metrics are only collected if replication is enabled if (repl::getGlobalReplicationCoordinator()->getReplicationMode() != repl::ReplicationCoordinator::modeNone) { @@ -335,43 +58,19 @@ void startFTDC() { BSON("collStats" << "oplog.rs"))); } - - // Install System Metric Collector as a periodic collector - installSystemMetricsCollector(controller.get()); - - // Install file rotation collectors - // These are collected on each file rotation. - - // CmdBuildInfo - controller->addOnRotateCollector(stdx::make_unique<FTDCSimpleInternalCommandCollector>( - "buildInfo", "buildInfo", "", BSON("buildInfo" << 1))); - - // CmdGetCmdLineOpts - controller->addOnRotateCollector(stdx::make_unique<FTDCSimpleInternalCommandCollector>( - "getCmdLineOpts", "getCmdLineOpts", "", BSON("getCmdLineOpts" << 1))); - - // HostInfoCmd - controller->addOnRotateCollector(stdx::make_unique<FTDCSimpleInternalCommandCollector>( - "hostInfo", "hostInfo", "", BSON("hostInfo" << 1))); - - // Install the new controller - auto& staticFTDC = getFTDCController(getGlobalServiceContext()); - - staticFTDC = std::move(controller); - - staticFTDC->start(); } -void stopFTDC() { - auto controller = getGlobalFTDCController(); +} // namespace - if (controller) { - controller->stop(); - } +void startMongoDFTDC() { + boost::filesystem::path dir(storageGlobalParams.dbpath); + dir /= kFTDCDefaultDirectory.toString(); + + startFTDC(dir, FTDCStartMode::kStart, registerMongoDCollectors); } -FTDCController* FTDCController::get(ServiceContext* serviceContext) { - return getFTDCController(serviceContext).get(); +void stopMongoDFTDC() { + stopFTDC(); } } // namespace mongo diff --git a/src/mongo/db/ftdc/ftdc_mongod.h b/src/mongo/db/ftdc/ftdc_mongod.h index 1e4f20b8b17..b4409dde902 100644 --- a/src/mongo/db/ftdc/ftdc_mongod.h +++ b/src/mongo/db/ftdc/ftdc_mongod.h @@ -1,30 +1,30 @@ /** -* Copyright (C) 2015 MongoDB Inc. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License, version 3, -* as published by the Free Software Foundation. -* -* 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 -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see <http://www.gnu.org/licenses/>. -* -* 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 GNU Affero General 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. -*/ + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ #pragma once @@ -34,11 +34,11 @@ namespace mongo { * Start Full Time Data Capture * Starts 1 thread. */ -void startFTDC(); +void startMongoDFTDC(); /** * Stop Full Time Data Capture */ -void stopFTDC(); +void stopMongoDFTDC(); } // namespace mongo diff --git a/src/mongo/db/ftdc/ftdc_mongos.cpp b/src/mongo/db/ftdc/ftdc_mongos.cpp new file mode 100644 index 00000000000..bdacaa7b6e1 --- /dev/null +++ b/src/mongo/db/ftdc/ftdc_mongos.cpp @@ -0,0 +1,153 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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::kFTDC + +#include "mongo/platform/basic.h" + +#include "mongo/db/ftdc/ftdc_mongos.h" + +#include <boost/filesystem.hpp> + +#include "mongo/db/ftdc/controller.h" +#include "mongo/db/ftdc/ftdc_server.h" +#include "mongo/db/server_parameters.h" +#include "mongo/stdx/thread.h" +#include "mongo/util/log.h" + +namespace mongo { + +namespace { + +/** + * Expose diagnosticDataCollectionDirectoryPath set parameter to specify the MongoS FTDC path. + */ +class ExportedFTDCDirectoryPathParameter : public ServerParameter { +public: + ExportedFTDCDirectoryPathParameter() + : ServerParameter(ServerParameterSet::getGlobal(), + "diagnosticDataCollectionDirectoryPath", + true, + true) {} + + + void append(OperationContext* opCtx, BSONObjBuilder& b, const std::string& name) final { + stdx::lock_guard<stdx::mutex> guard(_lock); + b.append(name, _path.generic_string()); + } + + Status set(const BSONElement& newValueElement) { + if (newValueElement.type() != String) { + return Status(ErrorCodes::BadValue, + "diagnosticDataCollectionDirectoryPath only supports type string"); + } + + std::string str = newValueElement.str(); + return setFromString(str); + } + + Status setFromString(const std::string& str) final { + stdx::lock_guard<stdx::mutex> guard(_lock); + + FTDCController* controller = nullptr; + + if (hasGlobalServiceContext()) { + controller = FTDCController::get(getGlobalServiceContext()); + } + + if (controller) { + Status s = controller->setDirectory(str); + if (!s.isOK()) { + return s; + } + } + + _path = str; + + return Status::OK(); + } + + boost::filesystem::path getDirectory() { + stdx::lock_guard<stdx::mutex> guard(_lock); + return _path; + } + + void setDirectory(boost::filesystem::path& path) { + stdx::lock_guard<stdx::mutex> guard(_lock); + _path = path; + } + +private: + // Lock to guard _path + stdx::mutex _lock; + + // Directory location of ftdc files, guarded by _lock + boost::filesystem::path _path; +} exportedFTDCDirectoryPathParameter; + +void registerMongoSCollectors(FTDCController* controller) { + // PoolStats + controller->addPeriodicCollector(stdx::make_unique<FTDCSimpleInternalCommandCollector>( + "connPoolStats", "connPoolStats", "", BSON("connPoolStats" << 1))); +} + +} // namespace + +void startMongoSFTDC() { + // Get the path to use for FTDC: + // 1. Check if the user set one. + // 2. If not, check if the user has a logpath and derive one. + // 3. Otherwise, tell the user FTDC cannot run. + + // Only attempt to enable FTDC if we have a path to log files to. + FTDCStartMode startMode = FTDCStartMode::kStart; + auto directory = exportedFTDCDirectoryPathParameter.getDirectory(); + + if (directory.empty()) { + if (serverGlobalParams.logpath.empty()) { + warning() << "FTDC is disabled because neither '--logpath' nor set parameter " + "'diagnosticDataCollectionDirectoryPath' are specified."; + startMode = FTDCStartMode::kSkipStart; + } else { + directory = FTDCUtil::getMongoSPath(serverGlobalParams.logpath); + + // Update the server parameter with the computed path. + // Note: If the computed FTDC directory conflicts with an existing file, then FTDC will + // warn about the conflict, and not startup. It will not terminate MongoS in this + // situation. + exportedFTDCDirectoryPathParameter.setDirectory(directory); + } + } + + startFTDC(directory, startMode, registerMongoSCollectors); +} + +void stopMongoSFTDC() { + stopFTDC(); +} + +} // namespace mongo diff --git a/src/mongo/db/ftdc/ftdc_mongos.h b/src/mongo/db/ftdc/ftdc_mongos.h new file mode 100644 index 00000000000..4d8ff8fc08b --- /dev/null +++ b/src/mongo/db/ftdc/ftdc_mongos.h @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#pragma once + +namespace mongo { + +/** + * Start Full Time Data Capture + */ +void startMongoSFTDC(); + +/** + * Stop Full Time Data Capture + */ +void stopMongoSFTDC(); + +} // namespace mongo diff --git a/src/mongo/db/ftdc/ftdc_server.cpp b/src/mongo/db/ftdc/ftdc_server.cpp new file mode 100644 index 00000000000..167587a8722 --- /dev/null +++ b/src/mongo/db/ftdc/ftdc_server.cpp @@ -0,0 +1,350 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/ftdc/ftdc_server.h" + +#include <boost/filesystem.hpp> +#include <fstream> +#include <memory> + +#include "mongo/base/status.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/db/commands.h" +#include "mongo/db/ftdc/collector.h" +#include "mongo/db/ftdc/config.h" +#include "mongo/db/ftdc/controller.h" +#include "mongo/db/ftdc/ftdc_system_stats.h" +#include "mongo/db/jsobj.h" +#include "mongo/db/server_parameters.h" +#include "mongo/db/service_context.h" +#include "mongo/stdx/memory.h" + +namespace mongo { + +namespace { + +const auto getFTDCController = ServiceContext::declareDecoration<std::unique_ptr<FTDCController>>(); + +FTDCController* getGlobalFTDCController() { + if (!hasGlobalServiceContext()) { + return nullptr; + } + + return getFTDCController(getGlobalServiceContext()).get(); +} + +std::atomic<bool> localEnabledFlag(FTDCConfig::kEnabledDefault); // NOLINT + +class ExportedFTDCEnabledParameter + : public ExportedServerParameter<bool, ServerParameterType::kStartupAndRuntime> { +public: + ExportedFTDCEnabledParameter() + : ExportedServerParameter<bool, ServerParameterType::kStartupAndRuntime>( + ServerParameterSet::getGlobal(), + "diagnosticDataCollectionEnabled", + &localEnabledFlag) {} + + virtual Status validate(const bool& potentialNewValue) { + auto controller = getGlobalFTDCController(); + if (controller) { + return controller->setEnabled(potentialNewValue); + } + + return Status::OK(); + } + +} exportedFTDCEnabledParameter; + +std::atomic<std::int32_t> localPeriodMillis(FTDCConfig::kPeriodMillisDefault); // NOLINT + +class ExportedFTDCPeriodParameter + : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { +public: + ExportedFTDCPeriodParameter() + : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( + ServerParameterSet::getGlobal(), + "diagnosticDataCollectionPeriodMillis", + &localPeriodMillis) {} + + virtual Status validate(const std::int32_t& potentialNewValue) { + if (potentialNewValue < 100) { + return Status( + ErrorCodes::BadValue, + "diagnosticDataCollectionPeriodMillis must be greater than or equal to 100ms"); + } + + auto controller = getGlobalFTDCController(); + if (controller) { + controller->setPeriod(Milliseconds(potentialNewValue)); + } + + return Status::OK(); + } + +} exportedFTDCPeriodParameter; + +// Scale the values down since are defaults are in bytes, but the user interface is MB +std::atomic<std::int32_t> localMaxDirectorySizeMB(FTDCConfig::kMaxDirectorySizeBytesDefault / + (1024 * 1024)); // NOLINT + +std::atomic<std::int32_t> localMaxFileSizeMB(FTDCConfig::kMaxFileSizeBytesDefault / + (1024 * 1024)); // NOLINT + +class ExportedFTDCDirectorySizeParameter + : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { +public: + ExportedFTDCDirectorySizeParameter() + : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( + ServerParameterSet::getGlobal(), + "diagnosticDataCollectionDirectorySizeMB", + &localMaxDirectorySizeMB) {} + + virtual Status validate(const std::int32_t& potentialNewValue) { + if (potentialNewValue < 10) { + return Status( + ErrorCodes::BadValue, + "diagnosticDataCollectionDirectorySizeMB must be greater than or equal to 10"); + } + + if (potentialNewValue < localMaxFileSizeMB.load()) { + return Status( + ErrorCodes::BadValue, + str::stream() + << "diagnosticDataCollectionDirectorySizeMB must be greater than or equal to '" + << localMaxFileSizeMB.load() + << "' which is the current value of diagnosticDataCollectionFileSizeMB."); + } + + auto controller = getGlobalFTDCController(); + if (controller) { + controller->setMaxDirectorySizeBytes(potentialNewValue * 1024 * 1024); + } + + return Status::OK(); + } + +} exportedFTDCDirectorySizeParameter; + +class ExportedFTDCFileSizeParameter + : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { +public: + ExportedFTDCFileSizeParameter() + : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( + ServerParameterSet::getGlobal(), + "diagnosticDataCollectionFileSizeMB", + &localMaxFileSizeMB) {} + + virtual Status validate(const std::int32_t& potentialNewValue) { + if (potentialNewValue < 1) { + return Status(ErrorCodes::BadValue, + "diagnosticDataCollectionFileSizeMB must be greater than or equal to 1"); + } + + if (potentialNewValue > localMaxDirectorySizeMB.load()) { + return Status( + ErrorCodes::BadValue, + str::stream() + << "diagnosticDataCollectionFileSizeMB must be less than or equal to '" + << localMaxDirectorySizeMB.load() + << "' which is the current value of diagnosticDataCollectionDirectorySizeMB."); + } + + auto controller = getGlobalFTDCController(); + if (controller) { + controller->setMaxFileSizeBytes(potentialNewValue * 1024 * 1024); + } + + return Status::OK(); + } + +} exportedFTDCFileSizeParameter; + +std::atomic<std::int32_t> localMaxSamplesPerArchiveMetricChunk( // NOLINT + FTDCConfig::kMaxSamplesPerArchiveMetricChunkDefault); + +class ExportedFTDCArchiveChunkSizeParameter + : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { +public: + ExportedFTDCArchiveChunkSizeParameter() + : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( + ServerParameterSet::getGlobal(), + "diagnosticDataCollectionSamplesPerChunk", + &localMaxSamplesPerArchiveMetricChunk) {} + + virtual Status validate(const std::int32_t& potentialNewValue) { + if (potentialNewValue < 2) { + return Status( + ErrorCodes::BadValue, + "diagnosticDataCollectionSamplesPerChunk must be greater than or equal to 2"); + } + + auto controller = getGlobalFTDCController(); + if (controller) { + controller->setMaxSamplesPerArchiveMetricChunk(potentialNewValue); + } + + return Status::OK(); + } + +} exportedFTDCArchiveChunkSizeParameter; + +std::atomic<std::int32_t> localMaxSamplesPerInterimMetricChunk( // NOLINT + FTDCConfig::kMaxSamplesPerInterimMetricChunkDefault); + +class ExportedFTDCInterimChunkSizeParameter + : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { +public: + ExportedFTDCInterimChunkSizeParameter() + : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( + ServerParameterSet::getGlobal(), + "diagnosticDataCollectionSamplesPerInterimUpdate", + &localMaxSamplesPerInterimMetricChunk) {} + + virtual Status validate(const std::int32_t& potentialNewValue) { + if (potentialNewValue < 2) { + return Status(ErrorCodes::BadValue, + "diagnosticDataCollectionSamplesPerInterimUpdate must be greater than or " + "equal to 2"); + } + + auto controller = getGlobalFTDCController(); + if (controller) { + controller->setMaxSamplesPerInterimMetricChunk(potentialNewValue); + } + + return Status::OK(); + } + +} exportedFTDCInterimChunkSizeParameter; +} // namespace + +FTDCSimpleInternalCommandCollector::FTDCSimpleInternalCommandCollector(StringData command, + StringData name, + StringData ns, + BSONObj cmdObj) + : _name(name.toString()), _ns(ns.toString()), _cmdObj(std::move(cmdObj)) { + _command = Command::findCommand(command); + invariant(_command); +} + +void FTDCSimpleInternalCommandCollector::collect(OperationContext* opCtx, BSONObjBuilder& builder) { + std::string errmsg; + + bool ret = _command->run(opCtx, _ns, _cmdObj, 0, errmsg, builder); + + // Some commands return errmsgs when they return false (collstats) + // Some commands return bson objs when they return false (replGetStatus) + // We append the status as needed to ensure readers of the collected data can check the + // status of any individual command. + _command->appendCommandStatus(builder, ret, errmsg); +} + +std::string FTDCSimpleInternalCommandCollector::name() const { + return _name; +} + +// Register the FTDC system +// Note: This must be run before the server parameters are parsed during startup +// so that the FTDCController is initialized. +// +void startFTDC(boost::filesystem::path& path, + FTDCStartMode startupMode, + RegisterCollectorsFunction registerCollectors) { + FTDCConfig config; + config.period = Milliseconds(localPeriodMillis.load()); + // Only enable FTDC if our caller says to enable FTDC, MongoS may not have a valid path to write + // files to so update the diagnosticDataCollectionEnabled set parameter to reflect that. + localEnabledFlag.store(startupMode == FTDCStartMode::kStart && localEnabledFlag.load()); + config.enabled = localEnabledFlag.load(); + config.maxFileSizeBytes = localMaxFileSizeMB.load() * 1024 * 1024; + config.maxDirectorySizeBytes = localMaxDirectorySizeMB.load() * 1024 * 1024; + config.maxSamplesPerArchiveMetricChunk = localMaxSamplesPerArchiveMetricChunk.load(); + config.maxSamplesPerInterimMetricChunk = localMaxSamplesPerInterimMetricChunk.load(); + + auto controller = stdx::make_unique<FTDCController>(path, config); + + // Install periodic collectors + // These are collected on the period interval in FTDCConfig. + // NOTE: For each command here, there must be an equivalent privilege check in + // GetDiagnosticDataCommand + + // CmdServerStatus + // The "sharding" section is filtered out because at this time it only consists of strings in + // migration status. This section triggers too many schema changes in the serverStatus which + // hurt ftdc compression efficiency, because its output varies depending on the list of active + // migrations. + // TODO: do we need to enable "sharding" on MongoS? + controller->addPeriodicCollector(stdx::make_unique<FTDCSimpleInternalCommandCollector>( + "serverStatus", + "serverStatus", + "", + BSON("serverStatus" << 1 << "tcMalloc" << true << "sharding" << false))); + + registerCollectors(controller.get()); + + // Install System Metric Collector as a periodic collector + installSystemMetricsCollector(controller.get()); + + // Install file rotation collectors + // These are collected on each file rotation. + + // CmdBuildInfo + controller->addOnRotateCollector(stdx::make_unique<FTDCSimpleInternalCommandCollector>( + "buildInfo", "buildInfo", "", BSON("buildInfo" << 1))); + + // CmdGetCmdLineOpts + controller->addOnRotateCollector(stdx::make_unique<FTDCSimpleInternalCommandCollector>( + "getCmdLineOpts", "getCmdLineOpts", "", BSON("getCmdLineOpts" << 1))); + + // HostInfoCmd + controller->addOnRotateCollector(stdx::make_unique<FTDCSimpleInternalCommandCollector>( + "hostInfo", "hostInfo", "", BSON("hostInfo" << 1))); + + // Install the new controller + auto& staticFTDC = getFTDCController(getGlobalServiceContext()); + + staticFTDC = std::move(controller); + + staticFTDC->start(); +} + +void stopFTDC() { + auto controller = getGlobalFTDCController(); + + if (controller) { + controller->stop(); + } +} + +FTDCController* FTDCController::get(ServiceContext* serviceContext) { + return getFTDCController(serviceContext).get(); +} + +} // namespace mongo diff --git a/src/mongo/db/ftdc/ftdc_server.h b/src/mongo/db/ftdc/ftdc_server.h new file mode 100644 index 00000000000..4b4583ae153 --- /dev/null +++ b/src/mongo/db/ftdc/ftdc_server.h @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#pragma once + +#include <string> + +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/db/commands.h" +#include "mongo/db/ftdc/collector.h" +#include "mongo/db/ftdc/controller.h" +#include "mongo/db/jsobj.h" +#include "mongo/db/operation_context.h" +#include "mongo/stdx/functional.h" + +namespace mongo { + +/** + * Function that allows FTDC server components to register their own collectors as needed. + */ +using RegisterCollectorsFunction = stdx::function<void(FTDCController*)>; + +/** + * An enum that decides whether FTDC will startup as part of startup or if its deferred to later. + */ +enum class FTDCStartMode { + + /** + * Skip starting FTDC since it missing a file storage location. + */ + kSkipStart, + + /** + * Start FTDC because it has a path to store files. + */ + kStart, +}; + +/** + * Start Full Time Data Capture + * Starts 1 thread. + * + * See MongoD and MongoS specific functions. + */ +void startFTDC(boost::filesystem::path& path, + FTDCStartMode startupMode, + RegisterCollectorsFunction registerCollectors); + +/** + * Stop Full Time Data Capture + * + * See MongoD and MongoS specific functions. + */ +void stopFTDC(); + +/** + * A simple FTDC Collector that runs Commands. + */ +class FTDCSimpleInternalCommandCollector final : public FTDCCollectorInterface { +public: + FTDCSimpleInternalCommandCollector(StringData command, + StringData name, + StringData ns, + BSONObj cmdObj); + + void collect(OperationContext* opCtx, BSONObjBuilder& builder) override; + std::string name() const override; + +private: + std::string _name; + std::string _ns; + BSONObj _cmdObj; + + // Not owned + Command* _command; +}; + +} // namespace mongo diff --git a/src/mongo/db/ftdc/ftdc_system_stats.h b/src/mongo/db/ftdc/ftdc_system_stats.h index 20d21ef4f39..97bc0a67cf5 100644 --- a/src/mongo/db/ftdc/ftdc_system_stats.h +++ b/src/mongo/db/ftdc/ftdc_system_stats.h @@ -25,6 +25,7 @@ * delete this exception statement from all source files in the program, * then also delete it in the license file. */ +#pragma once #include <string> diff --git a/src/mongo/db/ftdc/util.cpp b/src/mongo/db/ftdc/util.cpp index d56eb8ca380..243ba90b666 100644 --- a/src/mongo/db/ftdc/util.cpp +++ b/src/mongo/db/ftdc/util.cpp @@ -103,6 +103,19 @@ Date_t roundTime(Date_t now, Milliseconds period) { return Date_t::fromMillisSinceEpoch(next_time); } +boost::filesystem::path getMongoSPath(const boost::filesystem::path& logFile) { + auto base = logFile; + + // Keep stripping file extensions until we are only left with the file name + while (base.has_extension()) { + auto full_path = base.generic_string(); + base = full_path.substr(0, full_path.size() - base.extension().size()); + } + + base += "." + kFTDCDefaultDirectory.toString(); + return base; +} + } // namespace FTDCUtil diff --git a/src/mongo/db/ftdc/util.h b/src/mongo/db/ftdc/util.h index 0cb4d19ce7f..4816c534f96 100644 --- a/src/mongo/db/ftdc/util.h +++ b/src/mongo/db/ftdc/util.h @@ -190,6 +190,11 @@ boost::filesystem::path getInterimTempFile(const boost::filesystem::path& file); */ Date_t roundTime(Date_t now, Milliseconds period); +/** + * Get the storage path for MongoS from the log file path. + */ +boost::filesystem::path getMongoSPath(const boost::filesystem::path& logFile); + } // namespace FTDCUtil } // namespace mongo diff --git a/src/mongo/db/ftdc/util_test.cpp b/src/mongo/db/ftdc/util_test.cpp index aa99006c73a..0adf0f73c15 100644 --- a/src/mongo/db/ftdc/util_test.cpp +++ b/src/mongo/db/ftdc/util_test.cpp @@ -46,4 +46,25 @@ TEST(FTDCUtilTest, TestRoundTime) { checkTime(14, 13, 7); } +// Validate the MongoS FTDC path is computed correctly from a log file path. +TEST(FTDCUtilTest, TestMongoSPath) { + + std::vector<std::pair<std::string, std::string>> testCases = { + {"/var/log/mongos.log", "/var/log/mongos.diagnostic.data"}, + {"/var/log/mongos.foo.log", "/var/log/mongos.diagnostic.data"}, + {"/var/log/log_file", "/var/log/log_file.diagnostic.data"}, + {"./mongos.log", "./mongos.diagnostic.data"}, + {"../mongos.log", "../mongos.diagnostic.data"}, + {"c:\\var\\log\\mongos.log", "c:\\var\\log\\mongos.diagnostic.data"}, + {"c:\\var\\log\\mongos.foo.log", "c:\\var\\log\\mongos.diagnostic.data"}, + {"c:\\var\\log\\log_file", "c:\\var\\log\\log_file.diagnostic.data"}, + {"/var/some.log/mongos.log", "/var/some.log/mongos.diagnostic.data"}, + {"/var/some.log/log_file", "/var/some.log/log_file.diagnostic.data"}, + }; + + for (const auto& p : testCases) { + ASSERT_EQUALS(FTDCUtil::getMongoSPath(p.first), p.second); + } +} + } // namespace mongo diff --git a/src/mongo/db/fts/unicode/string.cpp b/src/mongo/db/fts/unicode/string.cpp index 10737acc3ed..8c1890f3669 100644 --- a/src/mongo/db/fts/unicode/string.cpp +++ b/src/mongo/db/fts/unicode/string.cpp @@ -30,6 +30,7 @@ #include <algorithm> #include <boost/algorithm/searching/boyer_moore.hpp> +#include <boost/version.hpp> #include "mongo/db/fts/unicode/byte_vector.h" #include "mongo/platform/bits.h" @@ -272,9 +273,15 @@ bool String::substrMatch(const std::string& str, auto haystack = caseFoldAndStripDiacritics(&haystackBuf, str, options, cfMode); auto needle = caseFoldAndStripDiacritics(&needleBuf, find, options, cfMode); - // Case sensitive and diacritic sensitive. +// Case sensitive and diacritic sensitive. +#if BOOST_VERSION < 106200 return boost::algorithm::boyer_moore_search( haystack.begin(), haystack.end(), needle.begin(), needle.end()) != haystack.end(); +#else + return boost::algorithm::boyer_moore_search( + haystack.begin(), haystack.end(), needle.begin(), needle.end()) != + std::make_pair(haystack.end(), haystack.end()); +#endif } } // namespace unicode diff --git a/src/mongo/db/index/expression_keys_private.cpp b/src/mongo/db/index/expression_keys_private.cpp index d58c5ce1dc1..9fe3365f393 100644 --- a/src/mongo/db/index/expression_keys_private.cpp +++ b/src/mongo/db/index/expression_keys_private.cpp @@ -32,6 +32,7 @@ #include <utility> +#include "mongo/bson/bsonelement_comparator_interface.h" #include "mongo/bson/simple_bsonobj_comparator.h" #include "mongo/db/bson/dotted_path_support.h" #include "mongo/db/field_ref.h" @@ -247,7 +248,7 @@ void ExpressionKeysPrivate::get2DKeys(const BSONObj& obj, const TwoDIndexingParams& params, BSONObjSet* keys, std::vector<BSONObj>* locs) { - BSONElementMSet bSet; + BSONElementMultiSet bSet; // Get all the nested location fields, but don't return individual elements from // the last array, if it exists. @@ -256,7 +257,7 @@ void ExpressionKeysPrivate::get2DKeys(const BSONObj& obj, if (bSet.empty()) return; - for (BSONElementMSet::iterator setI = bSet.begin(); setI != bSet.end(); ++setI) { + for (BSONElementMultiSet::iterator setI = bSet.begin(); setI != bSet.end(); ++setI) { BSONElement geo = *setI; if (geo.eoo() || !geo.isABSONObj()) diff --git a/src/mongo/db/instance.cpp b/src/mongo/db/instance.cpp index c10d0a3174b..5bbf3339a37 100644 --- a/src/mongo/db/instance.cpp +++ b/src/mongo/db/instance.cpp @@ -206,14 +206,15 @@ void receivedCommand(OperationContext* txn, const int32_t responseToMsgId = message.header().getId(); - DbMessage dbMessage(message); - QueryMessage queryMessage(dbMessage); - CurOp* op = CurOp::get(txn); - rpc::LegacyReplyBuilder builder{}; try { + DbMessage dbMessage(message); + + // Can throw, so make sure it's under the try statement. + QueryMessage queryMessage(dbMessage); + // This will throw if the request is on an invalid namespace. rpc::LegacyRequest request{&message}; // Auth checking for Commands happens later. diff --git a/src/mongo/db/log_process_details.cpp b/src/mongo/db/log_process_details.cpp index 125106fd331..ea6e9d945a8 100644 --- a/src/mongo/db/log_process_details.cpp +++ b/src/mongo/db/log_process_details.cpp @@ -34,6 +34,9 @@ #include "mongo/db/log_process_details.h" +#include "mongo/db/repl/repl_set_config.h" +#include "mongo/db/repl/replication_coordinator.h" +#include "mongo/db/repl/replication_coordinator_global.h" #include "mongo/db/server_options.h" #include "mongo/db/server_options_helpers.h" #include "mongo/util/log.h" @@ -51,6 +54,7 @@ void logProcessDetails() { auto&& vii = VersionInfoInterface::instance(); log() << mongodVersion(vii); vii.logBuildInfo(); + printCommandLineOpts(); } @@ -59,6 +63,19 @@ void logProcessDetailsForLogRotate() { << (is32bit() ? " 32" : " 64") << "-bit " << "host=" << getHostNameCached(); + auto replCoord = repl::getGlobalReplicationCoordinator(); + if (replCoord != nullptr && + replCoord->getReplicationMode() == repl::ReplicationCoordinator::modeReplSet) { + auto rsConfig = replCoord->getConfig(); + + if (rsConfig.isInitialized()) { + log() << "Replica Set Config: " << rsConfig.toBSON(); + log() << "Replica Set Member State: " << (replCoord->getMemberState()).toString(); + } else { + log() << "Node currently has no Replica Set Config."; + } + } + logProcessDetails(); } diff --git a/src/mongo/db/matcher/expression_leaf.cpp b/src/mongo/db/matcher/expression_leaf.cpp index d562bff141e..0bf8d8685bc 100644 --- a/src/mongo/db/matcher/expression_leaf.cpp +++ b/src/mongo/db/matcher/expression_leaf.cpp @@ -202,7 +202,7 @@ void ComparisonMatchExpression::debugString(StringBuilder& debug, int level) con } void ComparisonMatchExpression::serialize(BSONObjBuilder* out) const { - string opString = ""; + std::string opString = ""; switch (matchType()) { case LT: opString = "$lt"; @@ -656,29 +656,31 @@ bool InMatchExpression::equivalent(const MatchExpression* other) const { void InMatchExpression::_doSetCollator(const CollatorInterface* collator) { _collator = collator; + _eltCmp = BSONElementComparator(BSONElementComparator::FieldNamesMode::kIgnore, _collator); // We need to re-compute '_equalitySet', since our set comparator has changed. - BSONElementSet equalitiesWithNewComparator( - _originalEqualityVector.begin(), _originalEqualityVector.end(), collator); - _equalitySet = std::move(equalitiesWithNewComparator); + _equalitySet = _eltCmp.makeBSONEltFlatSet(_originalEqualityVector); } -Status InMatchExpression::addEquality(const BSONElement& elt) { - if (elt.type() == BSONType::RegEx) { - return Status(ErrorCodes::BadValue, "InMatchExpression equality cannot be a regex"); - } - if (elt.type() == BSONType::Undefined) { - return Status(ErrorCodes::BadValue, "InMatchExpression equality cannot be undefined"); - } +Status InMatchExpression::setEqualities(std::vector<BSONElement> equalities) { + for (auto&& equality : equalities) { + if (equality.type() == BSONType::RegEx) { + return Status(ErrorCodes::BadValue, "InMatchExpression equality cannot be a regex"); + } + if (equality.type() == BSONType::Undefined) { + return Status(ErrorCodes::BadValue, "InMatchExpression equality cannot be undefined"); + } - if (elt.type() == BSONType::jstNULL) { - _hasNull = true; - } - if (elt.type() == BSONType::Array && elt.Obj().isEmpty()) { - _hasEmptyArray = true; + if (equality.type() == BSONType::jstNULL) { + _hasNull = true; + } else if (equality.type() == BSONType::Array && equality.Obj().isEmpty()) { + _hasEmptyArray = true; + } } - _equalitySet.insert(elt); - _originalEqualityVector.push_back(elt); + _originalEqualityVector = std::move(equalities); + + _equalitySet = _eltCmp.makeBSONEltFlatSet(_originalEqualityVector); + return Status::OK(); } @@ -884,7 +886,7 @@ void BitTestMatchExpression::debugString(StringBuilder& debug, int level) const } void BitTestMatchExpression::serialize(BSONObjBuilder* out) const { - string opString = ""; + std::string opString = ""; switch (matchType()) { case BITS_ALL_SET: diff --git a/src/mongo/db/matcher/expression_leaf.h b/src/mongo/db/matcher/expression_leaf.h index 128d3070737..5761f2ca2a2 100644 --- a/src/mongo/db/matcher/expression_leaf.h +++ b/src/mongo/db/matcher/expression_leaf.h @@ -30,9 +30,11 @@ #pragma once +#include "mongo/bson/bsonelement_comparator.h" #include "mongo/bson/bsonmisc.h" #include "mongo/bson/bsonobj.h" #include "mongo/db/matcher/expression.h" +#include "mongo/db/query/collation/collator_interface.h" #include "mongo/stdx/memory.h" #include "mongo/stdx/unordered_map.h" @@ -330,7 +332,10 @@ public: */ class InMatchExpression : public LeafMatchExpression { public: - InMatchExpression() : LeafMatchExpression(MATCH_IN) {} + InMatchExpression() + : LeafMatchExpression(MATCH_IN), + _eltCmp(BSONElementComparator::FieldNamesMode::kIgnore, _collator), + _equalitySet(_eltCmp.makeBSONEltFlatSet(_originalEqualityVector)) {} Status init(StringData path); @@ -349,11 +354,11 @@ public: */ virtual void _doSetCollator(const CollatorInterface* collator); - Status addEquality(const BSONElement& elt); + Status setEqualities(std::vector<BSONElement> equalities); Status addRegex(std::unique_ptr<RegexMatchExpression> expr); - const BSONElementSet& getEqualities() const { + const BSONEltFlatSet& getEqualities() const { return _equalitySet; } @@ -380,17 +385,20 @@ private: // Whether or not '_equalities' has an empty array element in it. bool _hasEmptyArray = false; - // Collator used to compare elements. By default, simple binary comparison will be used. + // Collator used to construct '_eltCmp'; const CollatorInterface* _collator = nullptr; - // Set of equality elements associated with this expression. '_collator' is used as a comparator - // for this set. - BSONElementSet _equalitySet; + // Comparator used to compare elements. By default, simple binary comparison will be used. + BSONElementComparator _eltCmp; // Original container of equality elements, including duplicates. Needed for re-computing // '_equalitySet' in case '_collator' changes after elements have been added. std::vector<BSONElement> _originalEqualityVector; + // Set of equality elements associated with this expression. '_eltCmp' is used as a comparator + // for this set. + BSONEltFlatSet _equalitySet; + // Container of regex elements this object owns. std::vector<std::unique_ptr<RegexMatchExpression>> _regexes; }; diff --git a/src/mongo/db/matcher/expression_leaf_test.cpp b/src/mongo/db/matcher/expression_leaf_test.cpp index 293ed59c85c..1c3f7795a95 100644 --- a/src/mongo/db/matcher/expression_leaf_test.cpp +++ b/src/mongo/db/matcher/expression_leaf_test.cpp @@ -1501,7 +1501,8 @@ TEST(InMatchExpression, MatchesElementSingle) { BSONObj match = BSON("a" << 1); BSONObj notMatch = BSON("a" << 2); InMatchExpression in; - in.addEquality(operand.firstElement()); + std::vector<BSONElement> equalities{operand.firstElement()}; + ASSERT_OK(in.setEqualities(std::move(equalities))); ASSERT(in.matchesSingleElement(match["a"])); ASSERT(!in.matchesSingleElement(notMatch["a"])); } @@ -1519,10 +1520,8 @@ TEST(InMatchExpression, MatchesEmpty) { TEST(InMatchExpression, MatchesElementMultiple) { BSONObj operand = BSON_ARRAY(1 << "r" << true << 1); InMatchExpression in; - in.addEquality(operand[0]); - in.addEquality(operand[1]); - in.addEquality(operand[2]); - in.addEquality(operand[3]); + std::vector<BSONElement> equalities{operand[0], operand[1], operand[2], operand[3]}; + ASSERT_OK(in.setEqualities(std::move(equalities))); BSONObj matchFirst = BSON("a" << 1); BSONObj matchSecond = BSON("a" @@ -1540,7 +1539,8 @@ TEST(InMatchExpression, MatchesScalar) { BSONObj operand = BSON_ARRAY(5); InMatchExpression in; in.init("a"); - in.addEquality(operand.firstElement()); + std::vector<BSONElement> equalities{operand.firstElement()}; + ASSERT_OK(in.setEqualities(std::move(equalities))); ASSERT(in.matchesBSON(BSON("a" << 5.0), NULL)); ASSERT(!in.matchesBSON(BSON("a" << 4), NULL)); @@ -1550,7 +1550,8 @@ TEST(InMatchExpression, MatchesArrayValue) { BSONObj operand = BSON_ARRAY(5); InMatchExpression in; in.init("a"); - in.addEquality(operand.firstElement()); + std::vector<BSONElement> equalities{operand.firstElement()}; + ASSERT_OK(in.setEqualities(std::move(equalities))); ASSERT(in.matchesBSON(BSON("a" << BSON_ARRAY(5.0 << 6)), NULL)); ASSERT(!in.matchesBSON(BSON("a" << BSON_ARRAY(6 << 7)), NULL)); @@ -1562,7 +1563,8 @@ TEST(InMatchExpression, MatchesNull) { InMatchExpression in; in.init("a"); - in.addEquality(operand.firstElement()); + std::vector<BSONElement> equalities{operand.firstElement()}; + ASSERT_OK(in.setEqualities(std::move(equalities))); ASSERT(in.matchesBSON(BSONObj(), NULL)); ASSERT(in.matchesBSON(BSON("a" << BSONNULL), NULL)); @@ -1576,15 +1578,16 @@ TEST(InMatchExpression, MatchesUndefined) { InMatchExpression in; in.init("a"); - Status s = in.addEquality(operand.firstElement()); - ASSERT_NOT_OK(s); + std::vector<BSONElement> equalities{operand.firstElement()}; + ASSERT_NOT_OK(in.setEqualities(std::move(equalities))); } TEST(InMatchExpression, MatchesMinKey) { BSONObj operand = BSON_ARRAY(MinKey); InMatchExpression in; in.init("a"); - in.addEquality(operand.firstElement()); + std::vector<BSONElement> equalities{operand.firstElement()}; + ASSERT_OK(in.setEqualities(std::move(equalities))); ASSERT(in.matchesBSON(BSON("a" << MinKey), NULL)); ASSERT(!in.matchesBSON(BSON("a" << MaxKey), NULL)); @@ -1595,7 +1598,8 @@ TEST(InMatchExpression, MatchesMaxKey) { BSONObj operand = BSON_ARRAY(MaxKey); InMatchExpression in; in.init("a"); - in.addEquality(operand.firstElement()); + std::vector<BSONElement> equalities{operand.firstElement()}; + ASSERT_OK(in.setEqualities(std::move(equalities))); ASSERT(in.matchesBSON(BSON("a" << MaxKey), NULL)); ASSERT(!in.matchesBSON(BSON("a" << MinKey), NULL)); @@ -1606,9 +1610,8 @@ TEST(InMatchExpression, MatchesFullArray) { BSONObj operand = BSON_ARRAY(BSON_ARRAY(1 << 2) << 4 << 5); InMatchExpression in; in.init("a"); - in.addEquality(operand[0]); - in.addEquality(operand[1]); - in.addEquality(operand[2]); + std::vector<BSONElement> equalities{operand[0], operand[1], operand[2]}; + ASSERT_OK(in.setEqualities(std::move(equalities))); ASSERT(in.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2)), NULL)); ASSERT(!in.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2 << 3)), NULL)); @@ -1620,8 +1623,8 @@ TEST(InMatchExpression, ElemMatchKey) { BSONObj operand = BSON_ARRAY(5 << 2); InMatchExpression in; in.init("a"); - in.addEquality(operand[0]); - in.addEquality(operand[1]); + std::vector<BSONElement> equalities{operand[0], operand[1]}; + ASSERT_OK(in.setEqualities(std::move(equalities))); MatchDetails details; details.requestElemMatchKey(); @@ -1639,7 +1642,8 @@ TEST(InMatchExpression, InMatchExpressionsWithDifferentNumbersOfElementsAreUnequ << "string"); InMatchExpression eq1; InMatchExpression eq2; - eq1.addEquality(obj.firstElement()); + std::vector<BSONElement> equalities{obj.firstElement()}; + ASSERT_OK(eq1.setEqualities(std::move(equalities))); ASSERT(!eq1.equivalent(&eq2)); } @@ -1675,8 +1679,12 @@ TEST(InMatchExpression, InMatchExpressionsWithCollationEquivalentElementsAreEqua InMatchExpression eq2; eq2.setCollator(&collator2); - eq1.addEquality(obj1.firstElement()); - eq2.addEquality(obj2.firstElement()); + std::vector<BSONElement> equalities1{obj1.firstElement()}; + ASSERT_OK(eq1.setEqualities(std::move(equalities1))); + + std::vector<BSONElement> equalities2{obj2.firstElement()}; + ASSERT_OK(eq2.setEqualities(std::move(equalities2))); + ASSERT(eq1.equivalent(&eq2)); } @@ -1692,8 +1700,12 @@ TEST(InMatchExpression, InMatchExpressionsWithCollationNonEquivalentElementsAreU InMatchExpression eq2; eq2.setCollator(&collator2); - eq1.addEquality(obj1.firstElement()); - eq2.addEquality(obj2.firstElement()); + std::vector<BSONElement> equalities1{obj1.firstElement()}; + ASSERT_OK(eq1.setEqualities(std::move(equalities1))); + + std::vector<BSONElement> equalities2{obj2.firstElement()}; + ASSERT_OK(eq2.setEqualities(std::move(equalities2))); + ASSERT(!eq1.equivalent(&eq2)); } @@ -1702,7 +1714,8 @@ TEST(InMatchExpression, StringMatchingWithNullCollatorUsesBinaryComparison) { BSONObj notMatch = BSON("a" << "string2"); InMatchExpression in; - in.addEquality(operand.firstElement()); + std::vector<BSONElement> equalities{operand.firstElement()}; + ASSERT_OK(in.setEqualities(std::move(equalities))); ASSERT(!in.matchesSingleElement(notMatch["a"])); } @@ -1713,7 +1726,8 @@ TEST(InMatchExpression, StringMatchingRespectsCollation) { CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kAlwaysEqual); InMatchExpression in; in.setCollator(&collator); - in.addEquality(operand.firstElement()); + std::vector<BSONElement> equalities{operand.firstElement()}; + ASSERT_OK(in.setEqualities(std::move(equalities))); ASSERT(in.matchesSingleElement(match["a"])); } @@ -1726,8 +1740,8 @@ TEST(InMatchExpression, ChangingCollationAfterAddingEqualitiesPreservesEqualitie CollatorInterfaceMock collatorReverseString(CollatorInterfaceMock::MockType::kReverseString); InMatchExpression in; in.setCollator(&collatorAlwaysEqual); - in.addEquality(obj1.firstElement()); - in.addEquality(obj2.firstElement()); + std::vector<BSONElement> equalities{obj1.firstElement(), obj2.firstElement()}; + ASSERT_OK(in.setEqualities(std::move(equalities))); ASSERT(in.getEqualities().size() == 1); in.setCollator(&collatorReverseString); ASSERT(in.getEqualities().size() == 2); diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp index 2d1399edcf9..9bfc2d75f3e 100644 --- a/src/mongo/db/matcher/expression_parser.cpp +++ b/src/mongo/db/matcher/expression_parser.cpp @@ -588,6 +588,7 @@ Status MatchExpressionParser::_parseInExpression(InMatchExpression* inExpression const BSONObj& theArray, const CollatorInterface* collator) { inExpression->setCollator(collator); + std::vector<BSONElement> equalities; BSONObjIterator i(theArray); while (i.more()) { BSONElement e = i.next(); @@ -606,12 +607,10 @@ Status MatchExpressionParser::_parseInExpression(InMatchExpression* inExpression if (!s.isOK()) return s; } else { - Status s = inExpression->addEquality(e); - if (!s.isOK()) - return s; + equalities.push_back(e); } } - return Status::OK(); + return inExpression->setEqualities(std::move(equalities)); } StatusWithMatchExpression MatchExpressionParser::_parseType(const char* name, diff --git a/src/mongo/db/mongod_options.cpp b/src/mongo/db/mongod_options.cpp index 307d6ace41a..c4b2283ef40 100644 --- a/src/mongo/db/mongod_options.cpp +++ b/src/mongo/db/mongod_options.cpp @@ -58,6 +58,7 @@ using std::cout; using std::endl; using std::string; + MongodGlobalParams mongodGlobalParams; extern DiagLog _diaglog; @@ -685,14 +686,13 @@ Status validateMongodOptions(const moe::Environment& params) { } } - bool isClusterRoleShard = false; + bool isClusterRoleShard = params.count("shardsvr"); if (params.count("sharding.clusterRole")) { auto clusterRole = params["sharding.clusterRole"].as<std::string>(); - isClusterRoleShard = (clusterRole == "shardsvr"); + isClusterRoleShard = isClusterRoleShard || (clusterRole == "shardsvr"); } - if ((isClusterRoleShard || params.count("shardsvr")) && - !params.count("sharding._overrideShardIdentity")) { + if (isClusterRoleShard && !params.count("sharding._overrideShardIdentity")) { return Status( ErrorCodes::BadValue, "shardsvr cluster role with queryableBackupMode requires _overrideShardIdentity"); @@ -1328,6 +1328,21 @@ Status storeMongodOptions(const moe::Environment& params) { log() << endl; } + bool isClusterRoleShard = params.count("shardsvr"); + bool isClusterRoleConfig = params.count("configsvr"); + if (params.count("sharding.clusterRole")) { + auto clusterRole = params["sharding.clusterRole"].as<std::string>(); + isClusterRoleShard = isClusterRoleShard || (clusterRole == "shardsvr"); + isClusterRoleConfig = isClusterRoleConfig || (clusterRole == "configsvr"); + } + + if ((isClusterRoleShard || isClusterRoleConfig) && skipShardingConfigurationChecks) { + auto clusterRoleStr = isClusterRoleConfig ? "--configsvr" : "--shardsvr"; + return Status(ErrorCodes::BadValue, + str::stream() << "Can not specify " << clusterRoleStr + << " and set skipShardingConfigurationChecks=true"); + } + setGlobalReplSettings(replSettings); return Status::OK(); } diff --git a/src/mongo/db/mongod_options.h b/src/mongo/db/mongod_options.h index 27fb778cfdf..481edacc96f 100644 --- a/src/mongo/db/mongod_options.h +++ b/src/mongo/db/mongod_options.h @@ -50,6 +50,8 @@ struct MongodGlobalParams { MongodGlobalParams() : scriptingEnabled(true) {} }; +extern bool skipShardingConfigurationChecks; + extern MongodGlobalParams mongodGlobalParams; Status addMongodOptions(moe::OptionSection* options); diff --git a/src/mongo/db/namespace_string-inl.h b/src/mongo/db/namespace_string-inl.h index 0ff45fca39e..7ad830f2481 100644 --- a/src/mongo/db/namespace_string-inl.h +++ b/src/mongo/db/namespace_string-inl.h @@ -118,7 +118,7 @@ inline bool NamespaceString::validCollectionName(StringData coll) { return true; } -inline NamespaceString::NamespaceString() : _ns(), _dotIndex(0) {} +inline NamespaceString::NamespaceString() : _ns(), _dotIndex(std::string::npos) {} inline NamespaceString::NamespaceString(StringData nsIn) { _ns = nsIn.toString(); // copy to our buffer _dotIndex = _ns.find('.'); diff --git a/src/mongo/db/namespace_string_test.cpp b/src/mongo/db/namespace_string_test.cpp index 69884bd5ca9..69742c797bf 100644 --- a/src/mongo/db/namespace_string_test.cpp +++ b/src/mongo/db/namespace_string_test.cpp @@ -272,4 +272,16 @@ TEST(NamespaceStringTest, makeListIndexesNSIsCorrect) { ASSERT(ns.isListIndexesCursorNS()); ASSERT_EQUALS(NamespaceString("DB.COLL"), ns.getTargetNSForListIndexes()); } + +TEST(NamespaceStringTest, EmptyNSStringReturnsEmptyColl) { + NamespaceString nss{}; + ASSERT_TRUE(nss.toString().empty()); + ASSERT_EQ(nss.coll(), StringData{}); +} + +TEST(NamespaceStringTest, EmptyNSStringReturnsEmptyDb) { + NamespaceString nss{}; + ASSERT_TRUE(nss.toString().empty()); + ASSERT_EQ(nss.db(), StringData{}); +} } diff --git a/src/mongo/db/op_observer_impl.cpp b/src/mongo/db/op_observer_impl.cpp index 00560851a7a..91bc3317d48 100644 --- a/src/mongo/db/op_observer_impl.cpp +++ b/src/mongo/db/op_observer_impl.cpp @@ -42,9 +42,12 @@ #include "mongo/db/server_options.h" #include "mongo/db/views/durable_view_catalog.h" #include "mongo/scripting/engine.h" +#include "mongo/util/fail_point_service.h" namespace mongo { +MONGO_FP_DECLARE(failCollectionUpdates); + void OpObserverImpl::onCreateIndex(OperationContext* txn, const std::string& ns, BSONObj indexDoc, @@ -80,7 +83,7 @@ void OpObserverImpl::onInserts(OperationContext* txn, if (nss.ns() == FeatureCompatibilityVersion::kCollection) { for (auto it = begin; it != end; it++) { - FeatureCompatibilityVersion::onInsertOrUpdate(*it); + FeatureCompatibilityVersion::onInsertOrUpdate(txn, *it); } } @@ -94,6 +97,20 @@ void OpObserverImpl::onInserts(OperationContext* txn, } void OpObserverImpl::onUpdate(OperationContext* txn, const OplogUpdateEntryArgs& args) { + MONGO_FAIL_POINT_BLOCK(failCollectionUpdates, extraData) { + auto collElem = extraData.getData()["collectionNS"]; + // If the failpoint specifies no collection or matches the existing one, fail. + if (!collElem || args.ns == collElem.String()) { + uasserted(40654, + str::stream() << "failCollectionUpdates failpoint enabled, namespace: " + << args.ns + << ", update: " + << args.update + << " on document with " + << args.criteria); + } + } + // Do not log a no-op operation; see SERVER-21738 if (args.update.isEmpty()) { return; @@ -119,7 +136,7 @@ void OpObserverImpl::onUpdate(OperationContext* txn, const OplogUpdateEntryArgs& } if (args.ns == FeatureCompatibilityVersion::kCollection) { - FeatureCompatibilityVersion::onInsertOrUpdate(args.updatedDoc); + FeatureCompatibilityVersion::onInsertOrUpdate(txn, args.updatedDoc); } } @@ -162,7 +179,7 @@ void OpObserverImpl::onDelete(OperationContext* txn, DurableViewCatalog::onExternalChange(txn, ns); } if (ns.ns() == FeatureCompatibilityVersion::kCollection) { - FeatureCompatibilityVersion::onDelete(deleteState.idDoc); + FeatureCompatibilityVersion::onDelete(txn, deleteState.idDoc); } } @@ -221,7 +238,7 @@ void OpObserverImpl::onDropDatabase(OperationContext* txn, const std::string& db repl::logOp(txn, "c", dbName.c_str(), cmdObj, nullptr, false); if (NamespaceString(dbName).db() == FeatureCompatibilityVersion::kDatabase) { - FeatureCompatibilityVersion::onDropCollection(); + FeatureCompatibilityVersion::onDropCollection(txn); } getGlobalAuthorizationManager()->logOp(txn, "c", dbName.c_str(), cmdObj, nullptr); @@ -243,7 +260,7 @@ void OpObserverImpl::onDropCollection(OperationContext* txn, } if (collectionName.ns() == FeatureCompatibilityVersion::kCollection) { - FeatureCompatibilityVersion::onDropCollection(); + FeatureCompatibilityVersion::onDropCollection(txn); } getGlobalAuthorizationManager()->logOp(txn, "c", dbName.c_str(), cmdObj, nullptr); @@ -276,11 +293,10 @@ void OpObserverImpl::onRenameCollection(OperationContext* txn, << dropTarget); repl::logOp(txn, "c", dbName.c_str(), cmdObj, nullptr, false); - if (fromCollection.coll() == DurableViewCatalog::viewsCollectionName() || - toCollection.coll() == DurableViewCatalog::viewsCollectionName()) { - DurableViewCatalog::onExternalChange( - txn, NamespaceString(DurableViewCatalog::viewsCollectionName())); - } + if (fromCollection.isSystemDotViews()) + DurableViewCatalog::onExternalChange(txn, fromCollection); + if (toCollection.isSystemDotViews()) + DurableViewCatalog::onExternalChange(txn, toCollection); getGlobalAuthorizationManager()->logOp(txn, "c", dbName.c_str(), cmdObj, nullptr); logOpForDbHash(txn, dbName.c_str()); diff --git a/src/mongo/db/operation_context.cpp b/src/mongo/db/operation_context.cpp index 8151fe97dfa..41b54eb6611 100644 --- a/src/mongo/db/operation_context.cpp +++ b/src/mongo/db/operation_context.cpp @@ -325,7 +325,7 @@ StatusWith<stdx::cv_status> OperationContext::waitForConditionOrInterruptNoAsser deadline = std::min(deadline, getDeadline()); } - const auto waitStatus = [&] { + const auto waitStatus = [&]() -> stdx::cv_status { if (Date_t::max() == deadline) { cv.wait(m); return stdx::cv_status::no_timeout; diff --git a/src/mongo/db/operation_context.h b/src/mongo/db/operation_context.h index 2c8be360a77..79455e47309 100644 --- a/src/mongo/db/operation_context.h +++ b/src/mongo/db/operation_context.h @@ -202,6 +202,40 @@ public: } /** + * Waits on condition "cv" for "pred" until "pred" returns true, or the given "deadline" + * expires, or this operation is interrupted, or this operation's own deadline expires. + * + * + * If the operation deadline expires or the operation is interrupted, throws a DBException. If + * the given "deadline" expires, returns pred. Otherwise, returns true. + */ + template <typename Pred> + bool waitForConditionOrInterruptUntilPred(stdx::condition_variable& cv, + stdx::unique_lock<stdx::mutex>& m, + Date_t deadline, + Pred pred) { + while (!pred()) { + if (stdx::cv_status::timeout == waitForConditionOrInterruptUntil(cv, m, deadline)) { + return pred(); + } + } + return true; + } + + /** + * Same as the predicate form of waitForConditionOrInterruptUntilPred, but takes a relative + * amount of time to wait instead of an absolute time point. + */ + template <typename Pred> + bool waitForConditionOrInterruptFor(stdx::condition_variable& cv, + stdx::unique_lock<stdx::mutex>& m, + Milliseconds duration, + Pred pred) { + return waitForConditionOrInterruptUntilPred( + cv, m, getServiceContext()->getPreciseClockSource()->now() + duration, pred); + } + + /** * Same as waitForConditionOrInterruptUntil, except returns StatusWith<stdx::cv_status> and * non-ok status indicates the error instead of a DBException. */ diff --git a/src/mongo/db/operation_context_test.cpp b/src/mongo/db/operation_context_test.cpp index 06c6575208a..ca77b8c316b 100644 --- a/src/mongo/db/operation_context_test.cpp +++ b/src/mongo/db/operation_context_test.cpp @@ -231,7 +231,7 @@ public: Date_t maxTime) { auto barrier = std::make_shared<unittest::Barrier>(2); - auto task = stdx::packaged_task<stdx::cv_status()>([=] { + auto task = stdx::packaged_task<stdx::cv_status()>([=]() -> stdx::cv_status { if (maxTime < Date_t::max()) { txn->setDeadlineByDate(maxTime); } diff --git a/src/mongo/db/ops/delete.cpp b/src/mongo/db/ops/delete.cpp index 7f509308ad4..6a7da15ee74 100644 --- a/src/mongo/db/ops/delete.cpp +++ b/src/mongo/db/ops/delete.cpp @@ -63,19 +63,11 @@ long long deleteObjects(OperationContext* txn, ParsedDelete parsedDelete(txn, &request); uassertStatusOK(parsedDelete.parseRequest()); - auto client = txn->getClient(); - auto lastOpAtOperationStart = repl::ReplClientInfo::forClient(client).getLastOp(); - std::unique_ptr<PlanExecutor> exec = uassertStatusOK( getExecutorDelete(txn, &CurOp::get(txn)->debug(), collection, &parsedDelete)); uassertStatusOK(exec->executePlan()); - // No-ops need to reset lastOp in the client, for write concern. - if (repl::ReplClientInfo::forClient(client).getLastOp() == lastOpAtOperationStart) { - repl::ReplClientInfo::forClient(client).setLastOpToSystemLastOpTime(txn); - } - return DeleteStage::getNumDeleted(*exec); } diff --git a/src/mongo/db/ops/modifier_add_to_set.cpp b/src/mongo/db/ops/modifier_add_to_set.cpp index a565800d749..416d487e0f4 100644 --- a/src/mongo/db/ops/modifier_add_to_set.cpp +++ b/src/mongo/db/ops/modifier_add_to_set.cpp @@ -225,6 +225,8 @@ Status ModifierAddToSet::prepare(mb::Element root, StringData matchedField, Exec // Locate the field name in 'root'. Status status = pathsupport::findLongestPrefix( _fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound); + const auto elemFoundIsArray = + _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array; // FindLongestPrefix may say the path does not exist at all, which is fine here, or // that the path was not viable or otherwise wrong, in which case, the mod cannot @@ -248,6 +250,12 @@ Status ModifierAddToSet::prepare(mb::Element root, StringData matchedField, Exec if (!_preparedState->elemFound.ok() || _preparedState->idxFound < (_fieldRef.numParts() - 1)) { // If no target element exists, we will simply be creating a new array. _preparedState->addAll = true; + + if (elemFoundIsArray) { + // Report that an existing array will gain a new element as a result of this mod. + execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound; + } + return Status::OK(); } diff --git a/src/mongo/db/ops/modifier_add_to_set_test.cpp b/src/mongo/db/ops/modifier_add_to_set_test.cpp index 235a65239e6..63d5a7136d4 100644 --- a/src/mongo/db/ops/modifier_add_to_set_test.cpp +++ b/src/mongo/db/ops/modifier_add_to_set_test.cpp @@ -459,4 +459,41 @@ TEST(Collation, AddToSetWithEachRespectsCollation) { ASSERT_EQUALS(doc, fromjson("{ a : ['abc', 'bdc'] }")); } + +TEST(IndexedMod, PrepareReportCreatedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$addToSet: {'a.1.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$addToSet: {'a.0.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) { + Document doc(fromjson("{a: {'0': {b: 0}}}")); + Mod mod(fromjson("{$addToSet: {'a.1.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} } // namespace diff --git a/src/mongo/db/ops/modifier_bit.cpp b/src/mongo/db/ops/modifier_bit.cpp index fb7ae3f45eb..99296911e95 100644 --- a/src/mongo/db/ops/modifier_bit.cpp +++ b/src/mongo/db/ops/modifier_bit.cpp @@ -164,6 +164,8 @@ Status ModifierBit::prepare(mutablebson::Element root, // Locate the field name in 'root'. Status status = pathsupport::findLongestPrefix( _fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound); + const auto elemFoundIsArray = + _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array; // FindLongestPrefix may say the path does not exist at all, which is fine here, or @@ -189,6 +191,12 @@ Status ModifierBit::prepare(mutablebson::Element root, // If no target element exists, the value we will write is the result of applying // the operation to a zero-initialized integer element. _preparedState->newValue = apply(SafeNum(static_cast<int32_t>(0))); + + if (elemFoundIsArray) { + // Report that an existing array will gain a new element as a result of this mod. + execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound; + } + return Status::OK(); } diff --git a/src/mongo/db/ops/modifier_bit_test.cpp b/src/mongo/db/ops/modifier_bit_test.cpp index dbc4d7ba0b2..5b8b0131ea7 100644 --- a/src/mongo/db/ops/modifier_bit_test.cpp +++ b/src/mongo/db/ops/modifier_bit_test.cpp @@ -755,4 +755,41 @@ TEST(DbUpdateTests, Bit1_4_Combined) { ASSERT_EQUALS(BSON("$set" << BSON("x" << ((3 | 2) & 8))), logDoc); } +TEST(IndexedMod, PrepareReportCreatedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$bit: {'a.1.c': {and: NumberInt(1)}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) { + Document doc(fromjson("{a: [{b: NumberInt(0)}]}")); + Mod mod(fromjson("{$bit: {'a.0.c': {or: NumberInt(1)}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) { + Document doc(fromjson("{a: {'0': {b: 0}}}")); + Mod mod(fromjson("{$bit: {'a.1.c': {and: NumberInt(1)}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + } // namespace diff --git a/src/mongo/db/ops/modifier_compare.cpp b/src/mongo/db/ops/modifier_compare.cpp index 4e366f90ee2..7db7abd09ca 100644 --- a/src/mongo/db/ops/modifier_compare.cpp +++ b/src/mongo/db/ops/modifier_compare.cpp @@ -110,6 +110,8 @@ Status ModifierCompare::prepare(mutablebson::Element root, // be created during the apply. Status status = pathsupport::findLongestPrefix( _updatePath, root, &_preparedState->idxFound, &_preparedState->elemFound); + const auto elemFoundIsArray = + _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array; // FindLongestPrefix may say the path does not exist at all, which is fine here, or // that the path was not viable or otherwise wrong, in which case, the mod cannot @@ -128,6 +130,11 @@ Status ModifierCompare::prepare(mutablebson::Element root, _preparedState->idxFound == (_updatePath.numParts() - 1)); if (!destExists) { execInfo->noOp = false; + + if (elemFoundIsArray) { + // Report that an existing array will gain a new element as a result of this mod. + execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound; + } } else { const int compareVal = _preparedState->elemFound.compareWithBSONElement(_val, _collator, false); diff --git a/src/mongo/db/ops/modifier_compare_test.cpp b/src/mongo/db/ops/modifier_compare_test.cpp index 9bc036b776b..edaba6a2a68 100644 --- a/src/mongo/db/ops/modifier_compare_test.cpp +++ b/src/mongo/db/ops/modifier_compare_test.cpp @@ -341,4 +341,41 @@ TEST(Collation, MaxRespectsCollationFromSetCollator) { ASSERT_OK(mod.apply()); ASSERT_EQUALS(fromjson("{a : 'abd'}"), doc); } + +TEST(IndexedMod, PrepareReportCreatedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$min: {'a.1.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$min: {'a.0.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) { + Document doc(fromjson("{a: {'0': {b: 0}}}")); + Mod mod(fromjson("{$min: {'a.1.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} } // namespace diff --git a/src/mongo/db/ops/modifier_current_date.cpp b/src/mongo/db/ops/modifier_current_date.cpp index cd328f5fe94..4496e0cd2cd 100644 --- a/src/mongo/db/ops/modifier_current_date.cpp +++ b/src/mongo/db/ops/modifier_current_date.cpp @@ -161,6 +161,8 @@ Status ModifierCurrentDate::prepare(mutablebson::Element root, // be created during the apply. Status status = pathsupport::findLongestPrefix( _updatePath, root, &_preparedState->idxFound, &_preparedState->elemFound); + const auto elemFoundIsArray = + _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array; // FindLongestPrefix may say the path does not exist at all, which is fine here, or // that the path was not viable or otherwise wrong, in which case, the mod cannot @@ -175,6 +177,14 @@ Status ModifierCurrentDate::prepare(mutablebson::Element root, // there is any conflict among mods. execInfo->fieldRef[0] = &_updatePath; + if (!_preparedState->elemFound.ok() || + _preparedState->idxFound < (_updatePath.numParts() - 1)) { + if (elemFoundIsArray) { + // Report that an existing array will gain a new element as a result of this mod. + execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound; + } + } + return Status::OK(); } diff --git a/src/mongo/db/ops/modifier_current_date_test.cpp b/src/mongo/db/ops/modifier_current_date_test.cpp index 81bcd4d04e0..a3746e445da 100644 --- a/src/mongo/db/ops/modifier_current_date_test.cpp +++ b/src/mongo/db/ops/modifier_current_date_test.cpp @@ -359,4 +359,41 @@ TEST(DottedTimestampInput, EmptyStartDoc) { validateOplogEntry(oplogFormat, logDoc); } +TEST(BoolInput, PrepareReportCreatedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$currentDate: {'a.1.c': true}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(BoolInput, PrepareDoNotReportModifiedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$currentDate: {'a.0.c': true}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(BoolInput, PrepareDoNotReportCreatedNumericObjectField) { + Document doc(fromjson("{a: {'0': {b: 0}}}")); + Mod mod(fromjson("{$currentDate: {'a.1.c': true}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + } // namespace diff --git a/src/mongo/db/ops/modifier_inc.cpp b/src/mongo/db/ops/modifier_inc.cpp index 314ac6a5024..f2d51b390da 100644 --- a/src/mongo/db/ops/modifier_inc.cpp +++ b/src/mongo/db/ops/modifier_inc.cpp @@ -135,6 +135,8 @@ Status ModifierInc::prepare(mutablebson::Element root, // be created during the apply. Status status = pathsupport::findLongestPrefix( _fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound); + const auto elemFoundIsArray = + _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array; // FindLongestPrefix may say the path does not exist at all, which is fine here, or // that the path was not viable or otherwise wrong, in which case, the mod cannot @@ -166,6 +168,11 @@ Status ModifierInc::prepare(mutablebson::Element root, if (_mode == MODE_MUL) _preparedState->newValue *= SafeNum(static_cast<int32_t>(0)); + if (elemFoundIsArray) { + // Report that an existing array will gain a new element as a result of this mod. + execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound; + } + return Status::OK(); } diff --git a/src/mongo/db/ops/modifier_inc_test.cpp b/src/mongo/db/ops/modifier_inc_test.cpp index 02fbbd4474a..3784abbcdff 100644 --- a/src/mongo/db/ops/modifier_inc_test.cpp +++ b/src/mongo/db/ops/modifier_inc_test.cpp @@ -630,4 +630,41 @@ TEST(Multiplication, ApplyMissingElementDouble) { ASSERT_EQUALS(mongo::NumberDouble, doc.root().rightChild().getType()); } +TEST(IndexedMod, PrepareReportCreatedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$inc: {'a.1.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$inc: {'a.0.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) { + Document doc(fromjson("{a: {'0': {b: 0}}}")); + Mod mod(fromjson("{$inc: {'a.1.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + } // namespace diff --git a/src/mongo/db/ops/modifier_interface.h b/src/mongo/db/ops/modifier_interface.h index d833dcf07a1..2294802e18d 100644 --- a/src/mongo/db/ops/modifier_interface.h +++ b/src/mongo/db/ops/modifier_interface.h @@ -205,11 +205,17 @@ struct ModifierInterface::ExecInfo { ExecInfo() : noOp(false), context(ANY_CONTEXT) { for (int i = 0; i < MAX_NUM_FIELDS; i++) { fieldRef[i] = NULL; + indexOfArrayWithNewElement[i] = boost::none; } } // The fields of concern to the driver: no other op may modify the fields listed here. FieldRef* fieldRef[MAX_NUM_FIELDS]; // not owned here + + // For each modified field ref, the index of the path component representing an existing array + // that gained a new element. + boost::optional<size_t> indexOfArrayWithNewElement[MAX_NUM_FIELDS]; + bool noOp; UpdateContext context; }; diff --git a/src/mongo/db/ops/modifier_push.cpp b/src/mongo/db/ops/modifier_push.cpp index 8bc671ecf46..ef19e45c8a2 100644 --- a/src/mongo/db/ops/modifier_push.cpp +++ b/src/mongo/db/ops/modifier_push.cpp @@ -434,6 +434,8 @@ Status ModifierPush::prepare(mutablebson::Element root, // be created during the apply. Status status = pathsupport::findLongestPrefix( _fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound); + const auto elemFoundIsArray = + _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array; // FindLongestPrefix may say the path does not exist at all, which is fine here, or // that the path was not viable or otherwise wrong, in which case, the mod cannot @@ -463,6 +465,13 @@ Status ModifierPush::prepare(mutablebson::Element root, // there is any conflict among mods. execInfo->fieldRef[0] = &_fieldRef; + if (!_preparedState->elemFound.ok() || _preparedState->idxFound < (_fieldRef.numParts() - 1)) { + if (elemFoundIsArray) { + // Report that an existing array will gain a new element as a result of this mod. + execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound; + } + } + return Status::OK(); } diff --git a/src/mongo/db/ops/modifier_push_test.cpp b/src/mongo/db/ops/modifier_push_test.cpp index 49b9d01b002..a7de5d9f0f0 100644 --- a/src/mongo/db/ops/modifier_push_test.cpp +++ b/src/mongo/db/ops/modifier_push_test.cpp @@ -1473,4 +1473,87 @@ TEST(ToPosition, Back) { ASSERT_EQUALS(fromjson("{$set: {'a.1':1}}"), logDoc); } +TEST(IndexedMod, PrepareReportCreatedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$push: {'a.1.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareReportCreatedArrayElementPushAll) { + Document doc(fromjson("{a: [{b: 0}]}")); + auto modObj = fromjson("{$pushAll: {'a.1.c': [2]}}"); + ModifierPush mod(ModifierPush::PUSH_ALL); + ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod mod(fromjson("{$push: {'a.0.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportModifiedArrayElementPushAll) { + Document doc(fromjson("{a: [{b: 0}]}")); + auto modObj = fromjson("{$pushAll: {'a.0.c': [2]}}"); + ModifierPush mod(ModifierPush::PUSH_ALL); + ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) { + Document doc(fromjson("{a: {'0': {b: 0}}}")); + Mod mod(fromjson("{$push: {'a.1.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectFieldPushAll) { + Document doc(fromjson("{a: {'0': {b: 0}}}")); + auto modObj = fromjson("{$pushAll: {'a.1.c': [2]}}"); + ModifierPush mod(ModifierPush::PUSH_ALL); + ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + } // unnamed namespace diff --git a/src/mongo/db/ops/modifier_set.cpp b/src/mongo/db/ops/modifier_set.cpp index 59f59c555ce..03b703b860c 100644 --- a/src/mongo/db/ops/modifier_set.cpp +++ b/src/mongo/db/ops/modifier_set.cpp @@ -127,6 +127,8 @@ Status ModifierSet::prepare(mutablebson::Element root, // be created during the apply. Status status = pathsupport::findLongestPrefix( _fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound); + const auto elemFoundIsArray = + _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array; // FindLongestPrefix may say the path does not exist at all, which is fine here, or // that the path was not viable or otherwise wrong, in which case, the mod cannot @@ -155,6 +157,10 @@ Status ModifierSet::prepare(mutablebson::Element root, // If the field path is not fully present, then this mod cannot be in place, nor is it a noOp. if (!_preparedState->elemFound.ok() || _preparedState->idxFound < (_fieldRef.numParts() - 1)) { + if (elemFoundIsArray) { + // Report that an existing array will gain a new element as a result of this mod. + execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound; + } return Status::OK(); } diff --git a/src/mongo/db/ops/modifier_set_test.cpp b/src/mongo/db/ops/modifier_set_test.cpp index 1579b096722..b83270da957 100644 --- a/src/mongo/db/ops/modifier_set_test.cpp +++ b/src/mongo/db/ops/modifier_set_test.cpp @@ -429,6 +429,43 @@ TEST(IndexedMod, PrepareNonViablePath) { ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); } +TEST(IndexedMod, PrepareReportCreatedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod setMod(fromjson("{$set: {'a.1.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod setMod(fromjson("{$set: {'a.0.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) { + Document doc(fromjson("{a: {'0': {b: 0}}}")); + Mod setMod(fromjson("{$set: {'a.1.c': 2}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c"); + ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]); + ASSERT_FALSE(execInfo.noOp); +} + TEST(IndexedMod, PrepareApplyInPlace) { Document doc(fromjson("{a: [{b: 0},{b: 1},{b: 1}]}")); Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); diff --git a/src/mongo/db/ops/update.cpp b/src/mongo/db/ops/update.cpp index 5e0763f9eac..bc3c595dc6c 100644 --- a/src/mongo/db/ops/update.cpp +++ b/src/mongo/db/ops/update.cpp @@ -63,20 +63,9 @@ UpdateResult update(OperationContext* txn, Database* db, const UpdateRequest& re // Explain should never use this helper. invariant(!request.isExplain()); - auto client = txn->getClient(); - auto lastOpAtOperationStart = repl::ReplClientInfo::forClient(client).getLastOp(); - ScopeGuard lastOpSetterGuard = MakeObjGuard(repl::ReplClientInfo::forClient(client), - &repl::ReplClientInfo::setLastOpToSystemLastOpTime, - txn); - const NamespaceString& nsString = request.getNamespaceString(); Collection* collection = db->getCollection(nsString.ns()); - // If this is the local database, don't set last op. - if (db->name() == "local") { - lastOpSetterGuard.Dismiss(); - } - // The update stage does not create its own collection. As such, if the update is // an upsert, create the collection that the update stage inserts into beforehand. if (!collection && request.isUpsert()) { @@ -116,12 +105,6 @@ UpdateResult update(OperationContext* txn, Database* db, const UpdateRequest& re uassertStatusOK(getExecutorUpdate(txn, nullOpDebug, collection, &parsedUpdate)); uassertStatusOK(exec->executePlan()); - if (repl::ReplClientInfo::forClient(client).getLastOp() != lastOpAtOperationStart) { - // If this operation has already generated a new lastOp, don't bother setting it here. - // No-op updates will not generate a new lastOp, so we still need the guard to fire in that - // case. - lastOpSetterGuard.Dismiss(); - } const UpdateStats* updateStats = UpdateStage::getUpdateStats(exec.get()); diff --git a/src/mongo/db/ops/update_driver.cpp b/src/mongo/db/ops/update_driver.cpp index e5a63d64ccb..dc47ca4175f 100644 --- a/src/mongo/db/ops/update_driver.cpp +++ b/src/mongo/db/ops/update_driver.cpp @@ -291,11 +291,21 @@ Status UpdateDriver::update(StringData matchedField, // is not a no-op and it is in a valid context -- then we switch back to a // non-in-place mode. // - // TODO: make mightBeIndexed and fieldRef like each other. - if (!_affectIndices && !execInfo.noOp && _indexedFields && - _indexedFields->mightBeIndexed(execInfo.fieldRef[i]->dottedField())) { - _affectIndices = true; - doc->disableInPlaceUpdates(); + // To determine if indexes are affected: If we did not create a new element in an array, + // check whether the full path affects indexes. If we did create a new element in an + // array, check whether the array itself might affect any indexes. This is necessary + // because if there is an index {"a.b": 1}, and we set "a.1.c" and implicitly create an + // array element in "a", then we may need to add a null key to the index {"a.b": 1}, + // even though "a.1.c" does not appear to affect the index. + if (!_affectIndices && !execInfo.noOp && _indexedFields) { + auto pathLengthForIndexCheck = execInfo.indexOfArrayWithNewElement[i] + ? *execInfo.indexOfArrayWithNewElement[i] + 1 + : execInfo.fieldRef[i]->numParts(); + if (_indexedFields->mightBeIndexed( + execInfo.fieldRef[i]->dottedSubstring(0, pathLengthForIndexCheck))) { + _affectIndices = true; + doc->disableInPlaceUpdates(); + } } } diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index 98a66a02f48..ca8617f3d3d 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -252,8 +252,8 @@ docSourceEnv.Library( '$BUILD_DIR/mongo/db/matcher/expression_algo', '$BUILD_DIR/mongo/db/service_context', '$BUILD_DIR/mongo/db/stats/top', + '$BUILD_DIR/mongo/db/storage/encryption_hooks', '$BUILD_DIR/mongo/db/storage/storage_options', - '$BUILD_DIR/mongo/db/storage/wiredtiger/storage_wiredtiger_customization_hooks', '$BUILD_DIR/third_party/shim_snappy', ], LIBDEPS_TAGS=[ diff --git a/src/mongo/db/pipeline/document_source_geo_near.cpp b/src/mongo/db/pipeline/document_source_geo_near.cpp index 17e9997bbf5..a385eeeb07e 100644 --- a/src/mongo/db/pipeline/document_source_geo_near.cpp +++ b/src/mongo/db/pipeline/document_source_geo_near.cpp @@ -150,10 +150,12 @@ BSONObj DocumentSourceGeoNear::buildGeoNearCmd() const { geoNear.append("minDistance", minDistance); geoNear.append("query", query); - if (pExpCtx->getCollator()) { - geoNear.append("collation", pExpCtx->getCollator()->getSpec().toBSON()); - } else { - geoNear.append("collation", CollationSpec::kSimpleSpec); + if (!pExpCtx->collation.isEmpty()) { + if (pExpCtx->getCollator()) { + geoNear.append("collation", pExpCtx->getCollator()->getSpec().toBSON()); + } else { + geoNear.append("collation", CollationSpec::kSimpleSpec); + } } geoNear.append("spherical", spherical); diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection.cpp b/src/mongo/db/pipeline/parsed_aggregation_projection.cpp index 69b196faab3..c8d7bf06d7f 100644 --- a/src/mongo/db/pipeline/parsed_aggregation_projection.cpp +++ b/src/mongo/db/pipeline/parsed_aggregation_projection.cpp @@ -46,6 +46,8 @@ namespace mongo { namespace parsed_aggregation_projection { +using expression::isPathPrefixOf; + // // ProjectionSpecValidator // @@ -54,22 +56,36 @@ Status ProjectionSpecValidator::validate(const BSONObj& spec) { return ProjectionSpecValidator(spec).validate(); } -Status ProjectionSpecValidator::ensurePathDoesNotConflictOrThrow(StringData path) { - for (auto&& seenPath : _seenPaths) { - if ((path == seenPath) || (expression::isPathPrefixOf(path, seenPath)) || - (expression::isPathPrefixOf(seenPath, path))) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "specification contains two conflicting paths. " - "Cannot specify both '" - << path - << "' and '" - << seenPath - << "': " - << _rawObj.toString(), - 40176); - } +Status ProjectionSpecValidator::ensurePathDoesNotConflictOrThrow(const std::string& path) { + auto result = _seenPaths.emplace(path); + auto pos = result.first; + + // Check whether the path was a duplicate of an existing path. + auto conflictingPath = boost::make_optional(!result.second, *pos); + + // Check whether the preceding path prefixes this path. + if (!conflictingPath && pos != _seenPaths.begin()) { + conflictingPath = + boost::make_optional(isPathPrefixOf(*std::prev(pos), path), *std::prev(pos)); + } + + // Check whether this path prefixes the subsequent path. + if (!conflictingPath && std::next(pos) != _seenPaths.end()) { + conflictingPath = + boost::make_optional(isPathPrefixOf(path, *std::next(pos)), *std::next(pos)); + } + + if (conflictingPath) { + return Status(ErrorCodes::FailedToParse, + str::stream() << "specification contains two conflicting paths. " + "Cannot specify both '" + << path + << "' and '" + << *conflictingPath + << "': " + << _rawObj.toString(), + 40176); } - _seenPaths.emplace_back(path.toString()); return Status::OK(); } diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection.h b/src/mongo/db/pipeline/parsed_aggregation_projection.h index 8d92e4ab768..4fd9731774e 100644 --- a/src/mongo/db/pipeline/parsed_aggregation_projection.h +++ b/src/mongo/db/pipeline/parsed_aggregation_projection.h @@ -69,7 +69,7 @@ private: * For example, a user is not allowed to specify {'a': 1, 'a.b': 1}, or some similar conflicting * paths. */ - Status ensurePathDoesNotConflictOrThrow(StringData path); + Status ensurePathDoesNotConflictOrThrow(const std::string& path); /** * Returns the relevant error if an invalid projection specification is detected. @@ -99,9 +99,40 @@ private: // The original object. Used to generate more helpful error messages. const BSONObj& _rawObj; + // Custom comparator that orders fieldpath strings by path prefix first, then by field. + struct PathPrefixComparator { + static constexpr char dot = '.'; + + // Returns true if the lhs value should sort before the rhs, false otherwise. + bool operator()(const std::string& lhs, const std::string& rhs) const { + for (size_t pos = 0, len = std::min(lhs.size(), rhs.size()); pos < len; ++pos) { + auto &lchar = lhs[pos], &rchar = rhs[pos]; + if (lchar == rchar) { + continue; + } + + // Consider the path delimiter '.' as being less than all other characters, so that + // paths sort directly before any paths they prefix and directly after any paths + // which prefix them. + if (lchar == dot) { + return true; + } else if (rchar == dot) { + return false; + } + + // Otherwise, default to normal character comparison. + return lchar < rchar; + } + + // If we get here, then we have reached the end of lhs and/or rhs and all of their path + // segments up to this point match. If lhs is shorter than rhs, then lhs prefixes rhs + // and should sort before it. + return lhs.size() < rhs.size(); + } + }; + // Tracks which paths we've seen to ensure no two paths conflict with each other. - // Can be a vector since we iterate through it. - std::vector<std::string> _seenPaths; + std::set<std::string, PathPrefixComparator> _seenPaths; }; /** diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp b/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp index 71dd1c2378f..cb5d50b2771 100644 --- a/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp +++ b/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp @@ -126,6 +126,75 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectFieldsWithSharedPrefix) { UserException); } +TEST(ParsedAggregationProjectionErrors, ShouldRejectPathConflictsWithNonAlphaNumericCharacters) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + // Include/exclude non-alphanumeric fields with a shared prefix. First assert that the non- + // alphanumeric fields are accepted when no prefixes are present. + ASSERT(ParsedAggregationProjection::create( + expCtx, BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true << "a.b c" << true))); + ASSERT(ParsedAggregationProjection::create( + expCtx, BSON("a.b c" << false << "a.b?c" << false << "a.b" << false << "a.b-c" << false))); + + // Then assert that we throw when we introduce a prefixed field. + ASSERT_THROWS( + ParsedAggregationProjection::create( + expCtx, + BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true << "a.b c" << true << "a.b.d" + << true)), + AssertionException); + ASSERT_THROWS( + ParsedAggregationProjection::create( + expCtx, + BSON("a.b.d" << false << "a.b c" << false << "a.b?c" << false << "a.b" << false + << "a.b-c" + << false)), + AssertionException); + + // Adding the same field twice. + ASSERT_THROWS(ParsedAggregationProjection::create( + expCtx, BSON("a.b?c" << wrapInLiteral(1) << "a.b?c" << wrapInLiteral(0))), + AssertionException); + ASSERT_THROWS(ParsedAggregationProjection::create( + expCtx, BSON("a.b c" << wrapInLiteral(0) << "a.b c" << wrapInLiteral(1))), + AssertionException); + + // Mix of include/exclude and adding a shared prefix. + ASSERT_THROWS( + ParsedAggregationProjection::create( + expCtx, + BSON("a.b-c" << true << "a.b" << wrapInLiteral(1) << "a.b?c" << true << "a.b c" << true + << "a.b.d" + << true)), + AssertionException); + ASSERT_THROWS(ParsedAggregationProjection::create( + expCtx, + BSON("a.b.d" << false << "a.b c" << false << "a.b?c" << false << "a.b" + << wrapInLiteral(0) + << "a.b-c" + << false)), + AssertionException); + + // Adding a shared prefix twice. + ASSERT_THROWS(ParsedAggregationProjection::create( + expCtx, + BSON("a.b-c" << wrapInLiteral(1) << "a.b" << wrapInLiteral(1) << "a.b?c" + << wrapInLiteral(1) + << "a.b c" + << wrapInLiteral(1) + << "a.b.d" + << wrapInLiteral(0))), + AssertionException); + ASSERT_THROWS(ParsedAggregationProjection::create( + expCtx, + BSON("a.b.d" << wrapInLiteral(1) << "a.b c" << wrapInLiteral(1) << "a.b?c" + << wrapInLiteral(1) + << "a.b" + << wrapInLiteral(0) + << "a.b-c" + << wrapInLiteral(1))), + AssertionException); +} + TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfIdAndSubFieldsOfId) { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // Include/exclude _id twice. diff --git a/src/mongo/db/query/SConscript b/src/mongo/db/query/SConscript index a6c14a5f7dc..521f44cecb8 100644 --- a/src/mongo/db/query/SConscript +++ b/src/mongo/db/query/SConscript @@ -364,47 +364,11 @@ env.CppUnitTest( env.CppUnitTest( target="query_planner_test", source=[ - "query_planner_test.cpp" - ], - LIBDEPS=[ - "query_planner_test_fixture", - ], -) - -env.CppUnitTest( - target="query_planner_array_test", - source=[ - "query_planner_array_test.cpp" - ], - LIBDEPS=[ - "query_planner_test_fixture", - ], -) - -env.CppUnitTest( - target="query_planner_geo_test", - source=[ - "query_planner_geo_test.cpp" - ], - LIBDEPS=[ - "query_planner_test_fixture", - ], -) - -env.CppUnitTest( - target="query_planner_partialidx_test", - source=[ - "query_planner_partialidx_test.cpp" - ], - LIBDEPS=[ - "query_planner_test_fixture", - ], -) - -env.CppUnitTest( - target="query_planner_collation_test", - source=[ + "query_planner_array_test.cpp", "query_planner_collation_test.cpp", + "query_planner_geo_test.cpp", + "query_planner_partialidx_test.cpp", + "query_planner_test.cpp", ], LIBDEPS=[ "collation/collator_interface_mock", diff --git a/src/mongo/db/query/canonical_query_test.cpp b/src/mongo/db/query/canonical_query_test.cpp index c7a1f2a05a2..31ba2737ee9 100644 --- a/src/mongo/db/query/canonical_query_test.cpp +++ b/src/mongo/db/query/canonical_query_test.cpp @@ -548,7 +548,8 @@ TEST(CanonicalQueryTest, NormalizeWithInPreservesCollator) { BSONObj obj = fromjson("{'': 'string'}"); auto inMatchExpression = stdx::make_unique<InMatchExpression>(); inMatchExpression->setCollator(&collator); - inMatchExpression->addEquality(obj.firstElement()); + std::vector<BSONElement> equalities{obj.firstElement()}; + ASSERT_OK(inMatchExpression->setEqualities(std::move(equalities))); unique_ptr<MatchExpression> matchExpression( CanonicalQuery::normalizeTree(inMatchExpression.release())); ASSERT(matchExpression->matchType() == MatchExpression::MatchType::EQ); diff --git a/src/mongo/db/query/index_bounds_builder.cpp b/src/mongo/db/query/index_bounds_builder.cpp index b16ad43dc5e..eeb1c2f1114 100644 --- a/src/mongo/db/query/index_bounds_builder.cpp +++ b/src/mongo/db/query/index_bounds_builder.cpp @@ -249,6 +249,14 @@ bool typeMatch(const BSONObj& obj) { return first.canonicalType() == second.canonicalType(); } +bool IndexBoundsBuilder::canUseCoveredMatching(const MatchExpression* expr, + const IndexEntry& index) { + IndexBoundsBuilder::BoundsTightness tightness; + OrderedIntervalList oil; + translate(expr, BSONElement{}, index, &oil, &tightness); + return tightness >= IndexBoundsBuilder::INEXACT_COVERED; +} + // static void IndexBoundsBuilder::translate(const MatchExpression* expr, const BSONElement& elt, diff --git a/src/mongo/db/query/index_bounds_builder.h b/src/mongo/db/query/index_bounds_builder.h index b397f81b295..311b3ee61f6 100644 --- a/src/mongo/db/query/index_bounds_builder.h +++ b/src/mongo/db/query/index_bounds_builder.h @@ -70,6 +70,17 @@ public: static void allValuesForField(const BSONElement& elt, OrderedIntervalList* out); /** + * Returns true if 'expr' can correctly be assigned as an INEXACT_COVERED predicate to an index + * scan over 'index'. + * + * The result of this function is not meaningful when the predicate applies to special fields + * such as "hashed", "2d", or "2dsphere". That is, the caller is responsible for ensuring that + * 'expr' is a candidate for covered matching over a regular ascending/descending field of the + * index. + */ + static bool canUseCoveredMatching(const MatchExpression* expr, const IndexEntry& index); + + /** * Turn the MatchExpression in 'expr' into a set of index bounds. The field that 'expr' is * concerned with is indexed according to the keypattern element 'elt' from index 'index'. * diff --git a/src/mongo/db/query/index_bounds_builder_test.cpp b/src/mongo/db/query/index_bounds_builder_test.cpp index 45f11a2cc1d..a8ae3651d32 100644 --- a/src/mongo/db/query/index_bounds_builder_test.cpp +++ b/src/mongo/db/query/index_bounds_builder_test.cpp @@ -2137,4 +2137,54 @@ TEST(IndexBoundsBuilderTest, InWithStringAgainstHashedIndexWithCollatorUsesHashO ASSERT_EQUALS(tightness, IndexBoundsBuilder::INEXACT_FETCH); } +TEST(IndexBoundsBuilderTest, CanUseCoveredMatchingForEqualityPredicate) { + IndexEntry testIndex = IndexEntry(BSONObj()); + BSONObj obj = fromjson("{a: {$eq: 3}}"); + unique_ptr<MatchExpression> expr(parseMatchExpression(obj)); + ASSERT_TRUE(IndexBoundsBuilder::canUseCoveredMatching(expr.get(), testIndex)); +} + +TEST(IndexBoundsBuilderTest, CannotUseCoveredMatchingForEqualityToArrayPredicate) { + IndexEntry testIndex = IndexEntry(BSONObj()); + BSONObj obj = fromjson("{a: {$eq: [1, 2, 3]}}"); + unique_ptr<MatchExpression> expr(parseMatchExpression(obj)); + ASSERT_FALSE(IndexBoundsBuilder::canUseCoveredMatching(expr.get(), testIndex)); +} + +TEST(IndexBoundsBuilderTest, CannotUseCoveredMatchingForEqualityToNullPredicate) { + IndexEntry testIndex = IndexEntry(BSONObj()); + BSONObj obj = fromjson("{a: null}"); + unique_ptr<MatchExpression> expr(parseMatchExpression(obj)); + ASSERT_FALSE(IndexBoundsBuilder::canUseCoveredMatching(expr.get(), testIndex)); +} + +TEST(IndexBoundsBuilderTest, CannotUseCoveredMatchingForTypeArrayPredicate) { + IndexEntry testIndex = IndexEntry(BSONObj()); + BSONObj obj = fromjson("{a: {$type: 'array'}}"); + unique_ptr<MatchExpression> expr(parseMatchExpression(obj)); + ASSERT_FALSE(IndexBoundsBuilder::canUseCoveredMatching(expr.get(), testIndex)); +} + +TEST(IndexBoundsBuilderTest, CannotUseCoveredMatchingForExistsTruePredicate) { + IndexEntry testIndex = IndexEntry(BSONObj()); + BSONObj obj = fromjson("{a: {$exists: true}}"); + unique_ptr<MatchExpression> expr(parseMatchExpression(obj)); + ASSERT_FALSE(IndexBoundsBuilder::canUseCoveredMatching(expr.get(), testIndex)); +} + +TEST(IndexBoundsBuilderTest, CannotUseCoveredMatchingForExistsFalsePredicate) { + IndexEntry testIndex = IndexEntry(BSONObj()); + BSONObj obj = fromjson("{a: {$exists: false}}"); + unique_ptr<MatchExpression> expr(parseMatchExpression(obj)); + ASSERT_FALSE(IndexBoundsBuilder::canUseCoveredMatching(expr.get(), testIndex)); +} + +TEST(IndexBoundsBuilderTest, CanUseCoveredMatchingForExistsTrueWithSparseIndex) { + IndexEntry testIndex = IndexEntry(BSONObj()); + testIndex.sparse = true; + BSONObj obj = fromjson("{a: {$exists: true}}"); + unique_ptr<MatchExpression> expr(parseMatchExpression(obj)); + ASSERT_TRUE(IndexBoundsBuilder::canUseCoveredMatching(expr.get(), testIndex)); +} + } // namespace diff --git a/src/mongo/db/query/plan_enumerator.cpp b/src/mongo/db/query/plan_enumerator.cpp index 789cd405607..de80606df8d 100644 --- a/src/mongo/db/query/plan_enumerator.cpp +++ b/src/mongo/db/query/plan_enumerator.cpp @@ -579,6 +579,32 @@ bool PlanEnumerator::prepMemo(MatchExpression* node, PrepMemoContext context) { return false; } +void PlanEnumerator::assignToNonMultikeyMandatoryIndex( + const IndexEntry& index, + const std::vector<MatchExpression*>& predsOverLeadingField, + const IndexToPredMap& idxToNotFirst, + OneIndexAssignment* indexAssign) { + // Text indexes are typically multikey because there is an index key for each token in the + // source text. However, the leading and trailing non-text fields of the index cannot be + // multikey. As a result, we should use non-multikey predicate assignment rules for such + // indexes. + invariant(!index.multikey || index.type == IndexType::INDEX_TEXT); + + // Since the index is not multikey, all predicates over the leading field can be assigned. + indexAssign->preds = predsOverLeadingField; + + // Since everything in assign.preds prefixes the index, they all go at position '0' in the + // index, the first position. + indexAssign->positions.resize(indexAssign->preds.size(), 0); + + // And now we begin compound analysis. Find everything that could use assign.index but isn't a + // pred over the first field of that index. + auto compIt = idxToNotFirst.find(indexAssign->index); + if (compIt != idxToNotFirst.end()) { + compound(compIt->second, index, indexAssign); + } +} + bool PlanEnumerator::enumerateMandatoryIndex(const IndexToPredMap& idxToFirst, const IndexToPredMap& idxToNotFirst, MatchExpression* mandatoryPred, @@ -613,7 +639,12 @@ bool PlanEnumerator::enumerateMandatoryIndex(const IndexToPredMap& idxToFirst, const vector<MatchExpression*>& predsOverLeadingField = it->second; - if (thisIndex.multikey && !thisIndex.multikeyPaths.empty()) { + // Text indexes should be treated like non-multikey indexes, since the non-text fields are + // prohibited from containing arrays. + if (thisIndex.type == IndexType::INDEX_TEXT) { + assignToNonMultikeyMandatoryIndex( + thisIndex, predsOverLeadingField, idxToNotFirst, &indexAssign); + } else if (thisIndex.multikey && !thisIndex.multikeyPaths.empty()) { // 2dsphere indexes are the only special index type that should ever have path-level // multikey information. invariant(INDEX_2DSPHERE == thisIndex.type); @@ -719,22 +750,9 @@ bool PlanEnumerator::enumerateMandatoryIndex(const IndexToPredMap& idxToFirst, } } } else { - // For non-multikey, we don't have to do anything too special. - // Just assign all "first" predicates and try to compound like usual. - indexAssign.preds = it->second; - - // Since everything in assign.preds prefixes the index, they all go - // at position '0' in the index, the first position. - indexAssign.positions.resize(indexAssign.preds.size(), 0); - - // And now we begin compound analysis. - - // Find everything that could use assign.index but isn't a pred over - // the first field of that index. - IndexToPredMap::const_iterator compIt = idxToNotFirst.find(indexAssign.index); - if (compIt != idxToNotFirst.end()) { - compound(compIt->second, thisIndex, &indexAssign); - } + // The index is not multikey. + assignToNonMultikeyMandatoryIndex( + thisIndex, predsOverLeadingField, idxToNotFirst, &indexAssign); } // The mandatory predicate must be assigned. diff --git a/src/mongo/db/query/plan_enumerator.h b/src/mongo/db/query/plan_enumerator.h index 5543bc68ca9..a29c1dd4e76 100644 --- a/src/mongo/db/query/plan_enumerator.h +++ b/src/mongo/db/query/plan_enumerator.h @@ -434,6 +434,17 @@ private: AndAssignment* andAssignment); /** + * Assigns predicates in 'predsOverLeadingField' and 'idxToNotFirst' to 'indexAssign'. Assumes + * that the index is not multikey. Also assumes that that the index is of a type used to answer + * "mandatory predicates" such as text or geoNear. + */ + void assignToNonMultikeyMandatoryIndex( + const IndexEntry& index, + const std::vector<MatchExpression*>& predsOverLeadingField, + const IndexToPredMap& idxToNotFirst, + OneIndexAssignment* indexAssign); + + /** * Try to assign predicates in 'tryCompound' to 'thisIndex' as compound assignments. * Output the assignments in 'assign'. */ diff --git a/src/mongo/db/query/planner_access.cpp b/src/mongo/db/query/planner_access.cpp index 9a6f52e18d0..90c4873ffbb 100644 --- a/src/mongo/db/query/planner_access.cpp +++ b/src/mongo/db/query/planner_access.cpp @@ -224,6 +224,17 @@ QuerySolutionNode* QueryPlannerAccess::makeLeafNode( TextMatchExpressionBase* textExpr = static_cast<TextMatchExpressionBase*>(expr); TextNode* ret = new TextNode(index); ret->ftsQuery = textExpr->getFTSQuery().clone(); + + // Count the number of prefix fields before the "text" field. + for (auto&& keyPatternElt : ret->index.keyPattern) { + // We know that the only key pattern with a type of String is the _fts field + // which is immediately after all prefix fields. + if (BSONType::String == keyPatternElt.type()) { + break; + } + ++(ret->numPrefixFields); + } + return ret; } else { // Note that indexKeyPattern.firstElement().fieldName() may not equal expr->path() @@ -335,10 +346,24 @@ void QueryPlannerAccess::mergeWithLeafNode(MatchExpression* expr, ScanBuildingSt const StageType type = node->getType(); - // Text data is covered, but not exactly. Text covering is unlike any other covering - // so we deal with it in addFilterToSolutionNode. if (STAGE_TEXT == type) { - scanState->tightness = IndexBoundsBuilder::INEXACT_COVERED; + auto textNode = static_cast<TextNode*>(node); + + if (pos < textNode->numPrefixFields) { + // This predicate is assigned to one of the prefix fields of the text index. Such + // predicates must always be equalities and must always be attached to the TEXT node. In + // order to ensure this happens, we assign INEXACT_COVERED tightness. + scanState->tightness = IndexBoundsBuilder::INEXACT_COVERED; + } else { + // The predicate is assigned to one of the trailing fields of the text index. We + // currently don't generate bounds for predicates assigned to trailing fields of a text + // index, but rather attempt to attach a covered filter. However, certain predicates can + // never be correctly covered (e.g. $exists), so we assign the tightness accordingly. + scanState->tightness = IndexBoundsBuilder::canUseCoveredMatching(expr, index) + ? IndexBoundsBuilder::INEXACT_COVERED + : IndexBoundsBuilder::INEXACT_FETCH; + } + return; } @@ -347,20 +372,23 @@ void QueryPlannerAccess::mergeWithLeafNode(MatchExpression* expr, ScanBuildingSt if (STAGE_GEO_NEAR_2D == type) { invariant(INDEX_2D == index.type); - // 2D indexes are weird - the "2d" field stores a normally-indexed BinData field, but - // additional array fields are *not* exploded into multi-keys - they are stored directly - // as arrays in the index. Also, no matter what the index expression, the "2d" field is - // always first. - // This means that we can only generically accumulate bounds for 2D indexes over the - // first "2d" field (pos == 0) - MatchExpressions over other fields in the 2D index may - // be covered (can be evaluated using only the 2D index key). The additional fields - // must not affect the index scan bounds, since they are not stored in an - // IndexScan-compatible format. + // 2D indexes have a special format - the "2d" field stores a normally-indexed BinData + // field, but additional array fields are *not* exploded into multi-keys - they are stored + // directly as arrays in the index. Also, no matter what the index expression, the "2d" + // field is always first. + // + // This means that we can only generically accumulate bounds for 2D indexes over the first + // "2d" field (pos == 0) - MatchExpressions over other fields in the 2D index may be covered + // (can be evaluated using only the 2D index key). The additional fields must not affect + // the index scan bounds, since they are not stored in an IndexScan-compatible format. if (pos > 0) { - // Marking this field as covered allows the planner to accumulate a MatchExpression - // over the returned 2D index keys instead of adding to the index bounds. - scanState->tightness = IndexBoundsBuilder::INEXACT_COVERED; + // The predicate is over a trailing field of the "2d" index. If possible, we assign it + // as a covered filter (the INEXACT_COVERED case). Otherwise, the filter must be + // evaluated after fetching the full documents. + scanState->tightness = IndexBoundsBuilder::canUseCoveredMatching(expr, index) + ? IndexBoundsBuilder::INEXACT_COVERED + : IndexBoundsBuilder::INEXACT_FETCH; return; } @@ -374,10 +402,15 @@ void QueryPlannerAccess::mergeWithLeafNode(MatchExpression* expr, ScanBuildingSt verify(type == STAGE_IXSCAN); IndexScanNode* scan = static_cast<IndexScanNode*>(node); - // See STAGE_GEO_NEAR_2D above - 2D indexes can only accumulate scan bounds over the - // first "2d" field (pos == 0) + // See STAGE_GEO_NEAR_2D above - 2D indexes can only accumulate scan bounds over the first + // "2d" field (pos == 0). if (INDEX_2D == index.type && pos > 0) { - scanState->tightness = IndexBoundsBuilder::INEXACT_COVERED; + // The predicate is over a trailing field of the "2d" index. If possible, we assign it + // as a covered filter (the INEXACT_COVERED case). Otherwise, the filter must be + // evaluated after fetching the full documents. + scanState->tightness = IndexBoundsBuilder::canUseCoveredMatching(expr, index) + ? IndexBoundsBuilder::INEXACT_COVERED + : IndexBoundsBuilder::INEXACT_FETCH; return; } @@ -416,25 +449,9 @@ void QueryPlannerAccess::mergeWithLeafNode(MatchExpression* expr, ScanBuildingSt void QueryPlannerAccess::finishTextNode(QuerySolutionNode* node, const IndexEntry& index) { TextNode* tn = static_cast<TextNode*>(node); - // Figure out what positions are prefix positions. We build an index key prefix from - // the predicates over the text index prefix keys. - // For example, say keyPattern = { a: 1, _fts: "text", _ftsx: 1, b: 1 } - // prefixEnd should be 1. - size_t prefixEnd = 0; - BSONObjIterator it(tn->index.keyPattern); - // Count how many prefix terms we have. - while (it.more()) { - // We know that the only key pattern with a type of String is the _fts field - // which is immediately after all prefix fields. - if (String == it.next().type()) { - break; - } - ++prefixEnd; - } - // If there's no prefix, the filter is already on the node and the index prefix is null. // We can just return. - if (!prefixEnd) { + if (!tn->numPrefixFields) { return; } @@ -448,7 +465,7 @@ void QueryPlannerAccess::finishTextNode(QuerySolutionNode* node, const IndexEntr if (MatchExpression::AND != textFilterMe->matchType()) { // Only one prefix term. - invariant(1 == prefixEnd); + invariant(1u == tn->numPrefixFields); // Sanity check: must be an EQ. invariant(MatchExpression::EQ == textFilterMe->matchType()); @@ -460,10 +477,10 @@ void QueryPlannerAccess::finishTextNode(QuerySolutionNode* node, const IndexEntr // Indexed by the keyPattern position index assignment. We want to add // prefixes in order but we must order them first. - vector<MatchExpression*> prefixExprs(prefixEnd, NULL); + vector<MatchExpression*> prefixExprs(tn->numPrefixFields, nullptr); AndMatchExpression* amExpr = static_cast<AndMatchExpression*>(textFilterMe); - invariant(amExpr->numChildren() >= prefixEnd); + invariant(amExpr->numChildren() >= tn->numPrefixFields); // Look through the AND children. The prefix children we want to // stash in prefixExprs. @@ -474,7 +491,7 @@ void QueryPlannerAccess::finishTextNode(QuerySolutionNode* node, const IndexEntr invariant(NULL != ixtag); // Skip this child if it's not part of a prefix, or if we've already assigned a // predicate to this prefix position. - if (ixtag->pos >= prefixEnd || prefixExprs[ixtag->pos] != NULL) { + if (ixtag->pos >= tn->numPrefixFields || prefixExprs[ixtag->pos] != NULL) { ++curChild; continue; } diff --git a/src/mongo/db/query/planner_ixselect.cpp b/src/mongo/db/query/planner_ixselect.cpp index 321e856d1b7..2bf2eedc25e 100644 --- a/src/mongo/db/query/planner_ixselect.cpp +++ b/src/mongo/db/query/planner_ixselect.cpp @@ -48,6 +48,28 @@ namespace mongo { +namespace { + +/** + * Checks whether the given index is compatible with each child of the given $elemMatch expression. + * Assumes that the match expression is of type ELEM_MATCH_VALUE. + */ +bool elemMatchValueCompatible(const BSONElement& elt, + const IndexEntry& index, + MatchExpression* elemMatch, + const CollatorInterface* collator) { + invariant(elemMatch->matchType() == MatchExpression::ELEM_MATCH_VALUE); + for (size_t child = 0; child < elemMatch->numChildren(); ++child) { + if (!QueryPlannerIXSelect::compatible( + elt, index, elemMatch->getChild(child), collator, true)) { + return false; + } + } + return true; +} + +} // namespace + static double fieldWithDefault(const BSONObj& infoObj, const string& name, double def) { BSONElement e = infoObj[name]; if (e.isNumber()) { @@ -168,7 +190,8 @@ void QueryPlannerIXSelect::findRelevantIndices(const unordered_set<string>& fiel bool QueryPlannerIXSelect::compatible(const BSONElement& elt, const IndexEntry& index, MatchExpression* node, - const CollatorInterface* collator) { + const CollatorInterface* collator, + bool elemMatchChild) { if ((boundsGeneratingNodeContainsComparisonToType(node, BSONType::String) || boundsGeneratingNodeContainsComparisonToType(node, BSONType::Array) || boundsGeneratingNodeContainsComparisonToType(node, BSONType::Object)) && @@ -195,16 +218,18 @@ bool QueryPlannerIXSelect::compatible(const BSONElement& elt, MatchExpression::MatchType exprtype = node->matchType(); if (indexedFieldType.empty()) { - // Can't check for null w/a sparse index. - if (exprtype == MatchExpression::EQ && index.sparse) { + // Can't use a sparse index for $eq with a null element, unless the equality is within a + // $elemMatch expression since the latter implies a match on the literal element 'null'. + if (exprtype == MatchExpression::EQ && index.sparse && !elemMatchChild) { const EqualityMatchExpression* expr = static_cast<const EqualityMatchExpression*>(node); if (expr->getData().isNull()) { return false; } } - // Can't check for $in w/ null element w/a sparse index. - if (exprtype == MatchExpression::MATCH_IN && index.sparse) { + // Can't use a sparse index for $in with a null element, unless the $eq is within a + // $elemMatch expression since the latter implies a match on the literal element 'null'. + if (exprtype == MatchExpression::MATCH_IN && index.sparse && !elemMatchChild) { const InMatchExpression* expr = static_cast<const InMatchExpression*>(node); if (expr->hasNull()) { return false; @@ -220,7 +245,10 @@ bool QueryPlannerIXSelect::compatible(const BSONElement& elt, // the expression is a NOT. if (exprtype == MatchExpression::NOT) { // Don't allow indexed NOT on special index types such as geo or text indices. - if (INDEX_BTREE != index.type) { + // TODO: SERVER-30994 should remove this check entirely and allow $not on the + // 'non-special' fields of non-btree indices. + // (e.g. {a: 1, geo: "2dsphere"}) + if (INDEX_BTREE != index.type && !elemMatchChild) { return false; } @@ -386,12 +414,19 @@ void QueryPlannerIXSelect::rateIndices(MatchExpression* node, BSONObjIterator it(indices[i].keyPattern); BSONElement elt = it.next(); if (elt.fieldName() == fullPath && compatible(elt, indices[i], node, collator)) { - rt->first.push_back(i); + if (node->matchType() != MatchExpression::ELEM_MATCH_VALUE || + elemMatchValueCompatible(elt, indices[i], node, collator)) { + rt->first.push_back(i); + } } while (it.more()) { elt = it.next(); - if (elt.fieldName() == fullPath && compatible(elt, indices[i], node, collator)) { - rt->notFirst.push_back(i); + if (elt.fieldName() == fullPath && + compatible(elt, indices[i], node, collator, false)) { + if (node->matchType() != MatchExpression::ELEM_MATCH_VALUE || + elemMatchValueCompatible(elt, indices[i], node, collator)) { + rt->notFirst.push_back(i); + } } } } diff --git a/src/mongo/db/query/planner_ixselect.h b/src/mongo/db/query/planner_ixselect.h index 914e9265d9b..7cb34973810 100644 --- a/src/mongo/db/query/planner_ixselect.h +++ b/src/mongo/db/query/planner_ixselect.h @@ -72,7 +72,8 @@ public: static bool compatible(const BSONElement& elt, const IndexEntry& index, MatchExpression* node, - const CollatorInterface* collator); + const CollatorInterface* collator, + bool elemMatchChild = false); /** * Determine how useful all of our relevant 'indices' are to all predicates in the subtree diff --git a/src/mongo/db/query/planner_ixselect_test.cpp b/src/mongo/db/query/planner_ixselect_test.cpp index 1c273db8716..0db164a68be 100644 --- a/src/mongo/db/query/planner_ixselect_test.cpp +++ b/src/mongo/db/query/planner_ixselect_test.cpp @@ -45,6 +45,8 @@ using namespace mongo; namespace { +constexpr CollatorInterface* kSimpleCollator = nullptr; + using std::unique_ptr; using std::string; using std::vector; @@ -304,6 +306,83 @@ TEST(QueryPlannerIXSelectTest, RateIndicesTaggedNodePathArrayNegation) { } /** + * $not within $elemMatch should not attempt to use a sparse index for $exists:false. + */ +TEST(QueryPlannerIXSelectTest, ElemMatchNotExistsShouldNotUseSparseIndex) { + std::vector<IndexEntry> indices; + auto idxEntry = IndexEntry(BSON("a" << 1)); + idxEntry.sparse = true; + indices.push_back(idxEntry); + std::set<size_t> expectedIndices; + testRateIndices("{a: {$elemMatch: {$not: {$exists: true}}}}", + "", + kSimpleCollator, + indices, + "", + expectedIndices); +} + +/** + * $in with a null value within $elemMatch can use a sparse index. + */ +TEST(QueryPlannerIXSelectTest, ElemMatchInNullValueShouldUseSparseIndex) { + std::vector<IndexEntry> indices; + auto idxEntry = IndexEntry(BSON("a" << 1)); + idxEntry.sparse = true; + indices.push_back(idxEntry); + std::set<size_t> expectedIndices = {0}; + testRateIndices( + "{a: {$elemMatch: {$in: [null]}}}", "", kSimpleCollator, indices, "a", expectedIndices); +} + +/** + * $geo queries within $elemMatch should not use a normal B-tree index. + */ +TEST(QueryPlannerIXSelectTest, ElemMatchGeoShouldNotUseBtreeIndex) { + std::vector<IndexEntry> indices; + auto idxEntry = IndexEntry(BSON("a" << 1)); + indices.push_back(idxEntry); + std::set<size_t> expectedIndices; + testRateIndices(R"({a: {$elemMatch: {$geoWithin: {$geometry: {type: 'Polygon', + coordinates: [[[0,0],[0,1],[1,0],[0,0]]]}}}}})", + "", + kSimpleCollator, + indices, + "a", + expectedIndices); +} + +/** + * $eq with a null value within $elemMatch can use a sparse index. + */ +TEST(QueryPlannerIXSelectTest, ElemMatchEqNullValueShouldUseSparseIndex) { + std::vector<IndexEntry> indices; + auto idxEntry = IndexEntry(BSON("a" << 1)); + idxEntry.sparse = true; + indices.push_back(idxEntry); + std::set<size_t> expectedIndices = {0}; + testRateIndices( + "{a: {$elemMatch: {$eq: null}}}", "", kSimpleCollator, indices, "a", expectedIndices); +} + +/** + * $elemMatch with multiple children will not use an index if any child is incompatible. + */ +TEST(QueryPlannerIXSelectTest, ElemMatchMultipleChildrenShouldRequireAllToBeCompatible) { + std::vector<IndexEntry> indices; + auto idxEntry = IndexEntry(BSON("a" << 1)); + idxEntry.sparse = true; + indices.push_back(idxEntry); + std::set<size_t> expectedIndices; + testRateIndices("{a: {$elemMatch: {$eq: null, $not: {$exists: true}}}}", + "", + kSimpleCollator, + indices, + "", + expectedIndices); +} + +/** * If the collator is null, we select the relevant index with a null collator. */ TEST(QueryPlannerIXSelectTest, NullCollatorsMatch) { diff --git a/src/mongo/db/query/query_planner_geo_test.cpp b/src/mongo/db/query/query_planner_geo_test.cpp index 9e087444079..0421546a60d 100644 --- a/src/mongo/db/query/query_planner_geo_test.cpp +++ b/src/mongo/db/query/query_planner_geo_test.cpp @@ -1531,4 +1531,66 @@ TEST_F(QueryPlanner2dsphereVersionTest, NegationWithoutGeoPredCannotUseGeoIndex) testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions); } +TEST_F(QueryPlannerTest, 2dInexactFetchPredicateOverTrailingFieldHandledCorrectly) { + params.options = QueryPlannerParams::NO_TABLE_SCAN; + + addIndex(BSON("a" + << "2d" + << "b" + << 1)); + + runQuery(fromjson("{a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$exists: true}}")); + assertNumSolutions(1U); + assertSolutionExists( + "{fetch: {filter: {a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$exists: true}}, node: " + "{ixscan: {filter: null, pattern: {a: '2d', b: 1}}}}}"); +} + +TEST_F(QueryPlannerTest, 2dInexactFetchPredicateOverTrailingFieldHandledCorrectlyMultikey) { + params.options = QueryPlannerParams::NO_TABLE_SCAN; + + const bool multikey = true; + addIndex(BSON("a" + << "2d" + << "b" + << 1), + multikey); + + runQuery(fromjson("{a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$exists: true}}")); + assertNumSolutions(1U); + assertSolutionExists( + "{fetch: {filter: {a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$exists: true}}, node: " + "{ixscan: {filter: null, pattern: {a: '2d', b: 1}}}}}"); +} + +TEST_F(QueryPlannerTest, 2dNearInexactFetchPredicateOverTrailingFieldHandledCorrectly) { + params.options = QueryPlannerParams::NO_TABLE_SCAN; + + addIndex(BSON("a" + << "2d" + << "b" + << 1)); + + runQuery(fromjson("{a: {$near: [0, 0]}, b: {$exists: true}}")); + assertNumSolutions(1U); + assertSolutionExists( + "{fetch: {filter: {b: {$exists: true}}, node: {geoNear2d: {a: '2d', b: 1}}}}"); +} + +TEST_F(QueryPlannerTest, 2dNearInexactFetchPredicateOverTrailingFieldMultikey) { + params.options = QueryPlannerParams::NO_TABLE_SCAN; + + const bool multikey = true; + addIndex(BSON("a" + << "2d" + << "b" + << 1), + multikey); + + runQuery(fromjson("{a: {$near: [0, 0]}, b: {$exists: true}}")); + assertNumSolutions(1U); + assertSolutionExists( + "{fetch: {filter: {b: {$exists: true}}, node: {geoNear2d: {a: '2d', b: 1}}}}"); +} + } // namespace diff --git a/src/mongo/db/query/query_planner_text_test.cpp b/src/mongo/db/query/query_planner_text_test.cpp index 5050653292a..cbb7fcdbd39 100644 --- a/src/mongo/db/query/query_planner_text_test.cpp +++ b/src/mongo/db/query/query_planner_text_test.cpp @@ -461,4 +461,77 @@ TEST_F(QueryPlannerTest, SortKeyMetaProjectionWithTextScoreMetaSort) { "{sortKeyGen: {node: {text: {search: 'foo'}}}}}}}}"); } +TEST_F(QueryPlannerTest, PredicatesOverLeadingFieldsWithSharedPathPrefixHandledCorrectly) { + const bool multikey = true; + addIndex(BSON("a.x" << 1 << "a.y" << 1 << "b.x" << 1 << "b.y" << 1 << "_fts" + << "text" + << "_ftsx" + << 1), + multikey); + + runQuery(fromjson("{'a.x': 1, 'a.y': 2, 'b.x': 3, 'b.y': 4, $text: {$search: 'foo'}}")); + + assertNumSolutions(1U); + assertSolutionExists( + "{text: {search: 'foo', prefix: {'a.x': 1, 'a.y': 2, 'b.x': 3, 'b.y': 4}}}"); +} + +TEST_F(QueryPlannerTest, EqualityToArrayOverLeadingFieldHandledCorrectly) { + addIndex(BSON("a" << 1 << "_fts" + << "text" + << "_ftsx" + << 1)); + + runQuery(fromjson("{a: [1, 2, 3], $text: {$search: 'foo'}}")); + + assertNumSolutions(1U); + assertSolutionExists("{text: {search: 'foo', prefix: {a: [1, 2, 3]}}}"); +} + +TEST_F(QueryPlannerTest, EqualityToArrayOverLeadingFieldHandledCorrectlyWithMultikeyTrue) { + const bool multikey = true; + addIndex(BSON("a" << 1 << "_fts" + << "text" + << "_ftsx" + << 1), + multikey); + + runQuery(fromjson("{a: [1, 2, 3], $text: {$search: 'foo'}}")); + + assertNumSolutions(1U); + assertSolutionExists("{text: {search: 'foo', prefix: {a: [1, 2, 3]}}}"); +} + +TEST_F(QueryPlannerTest, InexactFetchPredicateOverTrailingFieldHandledCorrectly) { + addIndex(BSON("a" << 1 << "_fts" + << "text" + << "_ftsx" + << 1 + << "b" + << 1)); + + runQuery(fromjson("{a: 3, $text: {$search: 'foo'}, b: {$exists: true}}")); + + assertNumSolutions(1U); + assertSolutionExists( + "{fetch: {filter: {b: {$exists: true}}, node: {text: {search: 'foo', prefix: {a: 3}}}}}"); +} + +TEST_F(QueryPlannerTest, InexactFetchPredicateOverTrailingFieldHandledCorrectlyMultikeyTrue) { + const bool multikey = true; + addIndex(BSON("a" << 1 << "_fts" + << "text" + << "_ftsx" + << 1 + << "b" + << 1), + multikey); + + runQuery(fromjson("{a: 3, $text: {$search: 'foo'}, b: {$exists: true}}")); + + assertNumSolutions(1U); + assertSolutionExists( + "{fetch: {filter: {b: {$exists: true}}, node: {text: {search: 'foo', prefix: {a: 3}}}}}"); +} + } // namespace diff --git a/src/mongo/db/query/query_solution.h b/src/mongo/db/query/query_solution.h index 7e68378042d..ff02e0df3a5 100644 --- a/src/mongo/db/query/query_solution.h +++ b/src/mongo/db/query/query_solution.h @@ -250,6 +250,13 @@ struct TextNode : public QuerySolutionNode { IndexEntry index; std::unique_ptr<fts::FTSQuery> ftsQuery; + // The number of fields in the prefix of the text index. For example, if the key pattern is + // + // { a: 1, b: 1, _fts: "text", _ftsx: 1, c: 1 } + // + // then the number of prefix fields is 2, because of "a" and "b". + size_t numPrefixFields = 0u; + // "Prefix" fields of a text index can handle equality predicates. We group them with the // text node while creating the text leaf node and convert them into a BSONObj index prefix // when we finish the text leaf node. diff --git a/src/mongo/db/range_deleter.cpp b/src/mongo/db/range_deleter.cpp index b97d9fae216..e0d1d5c5aa0 100644 --- a/src/mongo/db/range_deleter.cpp +++ b/src/mongo/db/range_deleter.cpp @@ -354,16 +354,13 @@ bool RangeDeleter::deleteNow(OperationContext* txn, taskDetails.stats.queueEndTS = jsTime(); taskDetails.stats.deleteStartTS = jsTime(); - bool result = _env->deleteRange(txn, taskDetails, &taskDetails.stats.deletedDocCount, errMsg); - + bool result = _env->deleteRange(txn, + taskDetails, + &taskDetails.stats.deletedDocCount, + taskDetails.stats.waitForReplDurationMs, + errMsg); taskDetails.stats.deleteEndTS = jsTime(); - if (result) { - taskDetails.stats.waitForReplStartTS = jsTime(); - result = _waitForMajority(txn, errMsg); - taskDetails.stats.waitForReplEndTS = jsTime(); - } - { stdx::lock_guard<stdx::mutex> sl(_queueMutex); _deleteSet.erase(&deleteRange); @@ -485,8 +482,11 @@ void RangeDeleter::doWork() { { auto txn = client->makeOperationContext(); nextTask->stats.deleteStartTS = jsTime(); - bool delResult = - _env->deleteRange(txn.get(), *nextTask, &nextTask->stats.deletedDocCount, &errMsg); + bool delResult = _env->deleteRange(txn.get(), + *nextTask, + &nextTask->stats.deletedDocCount, + nextTask->stats.waitForReplDurationMs, + &errMsg); nextTask->stats.deleteEndTS = jsTime(); if (delResult) { diff --git a/src/mongo/db/range_deleter.h b/src/mongo/db/range_deleter.h index 776f8a825fc..b6acafe3112 100644 --- a/src/mongo/db/range_deleter.h +++ b/src/mongo/db/range_deleter.h @@ -254,6 +254,7 @@ struct DeleteJobStats { Date_t deleteEndTS; Date_t waitForReplStartTS; Date_t waitForReplEndTS; + Milliseconds waitForReplDurationMs; long long int deletedDocCount; @@ -314,6 +315,7 @@ struct RangeDeleterEnv { virtual bool deleteRange(OperationContext* txn, const RangeDeleteEntry& taskDetails, long long int* deletedDocs, + Milliseconds& replWaitDuration, std::string* errMsg) = 0; /** diff --git a/src/mongo/db/range_deleter_db_env.cpp b/src/mongo/db/range_deleter_db_env.cpp index 2f692111c0a..421cafc9692 100644 --- a/src/mongo/db/range_deleter_db_env.cpp +++ b/src/mongo/db/range_deleter_db_env.cpp @@ -61,6 +61,7 @@ using std::string; bool RangeDeleterDBEnv::deleteRange(OperationContext* txn, const RangeDeleteEntry& taskDetails, long long int* deletedDocs, + Milliseconds& replWaitDuration, std::string* errMsg) { const string ns(taskDetails.options.range.ns); const BSONObj inclusiveLower(taskDetails.options.range.minKey); @@ -92,6 +93,7 @@ bool RangeDeleterDBEnv::deleteRange(OperationContext* txn, KeyRange(ns, inclusiveLower, exclusiveUpper, keyPattern), BoundInclusion::kIncludeStartKeyOnly, writeConcern, + replWaitDuration, removeSaverPtr, fromMigrate, onlyRemoveOrphans); diff --git a/src/mongo/db/range_deleter_db_env.h b/src/mongo/db/range_deleter_db_env.h index 0bca8c6618c..b0e5a11d0cc 100644 --- a/src/mongo/db/range_deleter_db_env.h +++ b/src/mongo/db/range_deleter_db_env.h @@ -46,13 +46,16 @@ struct RangeDeleterDBEnv : public RangeDeleterEnv { * Note that secondaryThrottle will be ignored if current process is not part * of a replica set. * - * docsDeleted would contain the number of docs deleted if the deletion was successful. + * deletedDocs would contain the number of docs deleted if the deletion was successful. + * + * Returns time spent waiting for majority replication in replWaitDuration. * * Does not throw Exceptions. */ virtual bool deleteRange(OperationContext* txn, const RangeDeleteEntry& taskDetails, long long int* deletedDocs, + Milliseconds& replWaitDuration, std::string* errMsg); /** diff --git a/src/mongo/db/range_deleter_mock_env.cpp b/src/mongo/db/range_deleter_mock_env.cpp index 1ee01d6a19a..9c50185ba07 100644 --- a/src/mongo/db/range_deleter_mock_env.cpp +++ b/src/mongo/db/range_deleter_mock_env.cpp @@ -104,6 +104,7 @@ DeletedRange RangeDeleterMockEnv::getLastDelete() const { bool RangeDeleterMockEnv::deleteRange(OperationContext* txn, const RangeDeleteEntry& taskDetails, long long int* deletedDocs, + Milliseconds& replWaitDuration, string* errMsg) { { stdx::unique_lock<stdx::mutex> sl(_pauseDeleteMutex); diff --git a/src/mongo/db/range_deleter_mock_env.h b/src/mongo/db/range_deleter_mock_env.h index 307dbe2bd53..5792b24499c 100644 --- a/src/mongo/db/range_deleter_mock_env.h +++ b/src/mongo/db/range_deleter_mock_env.h @@ -127,6 +127,7 @@ public: bool deleteRange(OperationContext* txn, const RangeDeleteEntry& taskDetails, long long int* deletedDocs, + Milliseconds& replWaitDuration, std::string* errMsg); /** diff --git a/src/mongo/db/repl/SConscript b/src/mongo/db/repl/SConscript index 05dbeff18a0..31f35d384f8 100644 --- a/src/mongo/db/repl/SConscript +++ b/src/mongo/db/repl/SConscript @@ -631,12 +631,12 @@ env.Library('replica_set_messages', 'is_master_response.cpp', 'member_config.cpp', 'old_update_position_args.cpp', + 'repl_set_config.cpp', 'repl_set_heartbeat_args.cpp', 'repl_set_heartbeat_args_v1.cpp', 'repl_set_heartbeat_response.cpp', 'repl_set_html_summary.cpp', 'repl_set_request_votes_args.cpp', - 'repl_set_config.cpp', 'repl_set_tag.cpp', 'update_position_args.cpp', 'last_vote.cpp', diff --git a/src/mongo/db/repl/bgsync.cpp b/src/mongo/db/repl/bgsync.cpp index 2c04c40bf55..fbbf1c56efd 100644 --- a/src/mongo/db/repl/bgsync.cpp +++ b/src/mongo/db/repl/bgsync.cpp @@ -225,15 +225,16 @@ void BackgroundSync::_runProducer() { } // we want to start when we're no longer primary // start() also loads _lastOpTimeFetched, which we know is set from the "if" - auto txn = cc().makeOperationContext(); - if (getState() == ProducerState::Starting) { - start(txn.get()); + { + auto opCtx = cc().makeOperationContext(); + if (getState() == ProducerState::Starting) { + start(opCtx.get()); + } } - - _produce(txn.get()); + _produce(); } -void BackgroundSync::_produce(OperationContext* opCtx) { +void BackgroundSync::_produce() { if (MONGO_FAIL_POINT(stopReplProducer)) { // This log output is used in js tests so please leave it. log() << "bgsync - stopReplProducer fail point " @@ -266,15 +267,18 @@ void BackgroundSync::_produce(OperationContext* opCtx) { } } - auto storageInterface = StorageInterface::get(opCtx); // find a target to sync from the last optime fetched OpTime lastOpTimeFetched; HostAndPort source; HostAndPort oldSource = _syncSourceHost; SyncSourceResolverResponse syncSourceResp; { - const OpTime minValidSaved = storageInterface->getMinValid(opCtx); - + OpTime minValidSaved; + { + auto opCtx = cc().makeOperationContext(); + auto storageInterface = StorageInterface::get(opCtx.get()); + minValidSaved = storageInterface->getMinValid(opCtx.get()); + } stdx::lock_guard<stdx::mutex> lock(_mutex); if (_state != ProducerState::Running) { return; @@ -397,8 +401,12 @@ void BackgroundSync::_produce(OperationContext* opCtx) { // Set the applied point if unset. This is most likely the first time we've established a sync // source since stepping down or otherwise clearing the applied point. We need to set this here, // before the OplogWriter gets a chance to append to the oplog. - if (storageInterface->getAppliedThrough(opCtx).isNull()) { - storageInterface->setAppliedThrough(opCtx, _replCoord->getMyLastAppliedOpTime()); + { + auto opCtx = cc().makeOperationContext(); + auto storageInterface = StorageInterface::get(opCtx.get()); + if (storageInterface->getAppliedThrough(opCtx.get()).isNull()) { + storageInterface->setAppliedThrough(opCtx.get(), _replCoord->getMyLastAppliedOpTime()); + } } // "lastFetched" not used. Already set in _enqueueDocuments. @@ -521,10 +529,18 @@ void BackgroundSync::_produce(OperationContext* opCtx) { } } - OplogInterfaceLocal localOplog(opCtx, rsOplogName); - RollbackSourceImpl rollbackSource(getConnection, source, rsOplogName); - rollback( - opCtx, localOplog, rollbackSource, syncSourceResp.rbid, _replCoord, storageInterface); + { + auto opCtx = cc().makeOperationContext(); + OplogInterfaceLocal localOplog(opCtx.get(), rsOplogName); + RollbackSourceImpl rollbackSource(getConnection, source, rsOplogName); + auto storageInterface = StorageInterface::get(opCtx.get()); + rollback(opCtx.get(), + localOplog, + rollbackSource, + syncSourceResp.rbid, + _replCoord, + storageInterface); + } // Reset the producer to clear the sync source and the last optime fetched. stop(true); diff --git a/src/mongo/db/repl/bgsync.h b/src/mongo/db/repl/bgsync.h index 6d068967723..00f23e59b1d 100644 --- a/src/mongo/db/repl/bgsync.h +++ b/src/mongo/db/repl/bgsync.h @@ -148,7 +148,7 @@ private: void _run(); // Production thread inner loop. void _runProducer(); - void _produce(OperationContext* txn); + void _produce(); /** * Checks current background sync state before pushing operations into blocking queue and diff --git a/src/mongo/db/repl/collection_cloner.cpp b/src/mongo/db/repl/collection_cloner.cpp index 9dc3674efa0..bbaf04ba3dc 100644 --- a/src/mongo/db/repl/collection_cloner.cpp +++ b/src/mongo/db/repl/collection_cloner.cpp @@ -114,7 +114,8 @@ CollectionCloner::CollectionCloner(executor::TaskExecutor* executor, stdx::placeholders::_2, stdx::placeholders::_3), rpc::ServerSelectionMetadata(true, boost::none).toBSON(), - RemoteCommandRequest::kNoTimeout, + RemoteCommandRequest::kNoTimeout /* find network timeout */, + RemoteCommandRequest::kNoTimeout /* getMore network timeout */, RemoteCommandRetryScheduler::makeRetryPolicy( numInitialSyncListIndexesAttempts, executor::RemoteCommandRequest::kNoTimeout, @@ -518,7 +519,8 @@ void CollectionCloner::_beginCollectionCallback(const executor::TaskExecutor::Ca stdx::placeholders::_3, onCompletionGuard), rpc::ServerSelectionMetadata(true, boost::none).toBSON(), - RemoteCommandRequest::kNoTimeout, + RemoteCommandRequest::kNoTimeout /* find network timeout */, + RemoteCommandRequest::kNoTimeout /* getMore network timeout */, RemoteCommandRetryScheduler::makeRetryPolicy( numInitialSyncCollectionFindAttempts.load(), executor::RemoteCommandRequest::kNoTimeout, diff --git a/src/mongo/db/repl/database_cloner.cpp b/src/mongo/db/repl/database_cloner.cpp index 461c4dfe1b7..d0211fdf058 100644 --- a/src/mongo/db/repl/database_cloner.cpp +++ b/src/mongo/db/repl/database_cloner.cpp @@ -114,7 +114,8 @@ DatabaseCloner::DatabaseCloner(executor::TaskExecutor* executor, stdx::placeholders::_2, stdx::placeholders::_3), rpc::ServerSelectionMetadata(true, boost::none).toBSON(), - RemoteCommandRequest::kNoTimeout, + RemoteCommandRequest::kNoTimeout /* find network timeout */, + RemoteCommandRequest::kNoTimeout /* getMore network timeout */, RemoteCommandRetryScheduler::makeRetryPolicy( numInitialSyncListCollectionsAttempts, executor::RemoteCommandRequest::kNoTimeout, diff --git a/src/mongo/db/repl/databases_cloner.cpp b/src/mongo/db/repl/databases_cloner.cpp index 4a18e1be1b8..c9016c322f4 100644 --- a/src/mongo/db/repl/databases_cloner.cpp +++ b/src/mongo/db/repl/databases_cloner.cpp @@ -231,6 +231,44 @@ void DatabasesCloner::setScheduleDbWorkFn_forTest(const CollectionCloner::Schedu _scheduleDbWorkFn = work; } +StatusWith<std::vector<BSONElement>> DatabasesCloner::parseListDatabasesResponse_forTest( + BSONObj dbResponse) { + return _parseListDatabasesResponse(dbResponse); +} + +void DatabasesCloner::setAdminAsFirst_forTest(std::vector<BSONElement>& dbsArray) { + _setAdminAsFirst(dbsArray); +} + +StatusWith<std::vector<BSONElement>> DatabasesCloner::_parseListDatabasesResponse( + BSONObj dbResponse) { + if (!dbResponse.hasField("databases")) { + return Status(ErrorCodes::BadValue, + "The 'listDatabases' response does not contain a 'databases' field."); + } + BSONElement response = dbResponse["databases"]; + try { + return response.Array(); + } catch (const MsgAssertionException& e) { + return Status(ErrorCodes::BadValue, + "The 'listDatabases' response is unable to be transformed into an array."); + } +} + +void DatabasesCloner::_setAdminAsFirst(std::vector<BSONElement>& dbsArray) { + auto adminIter = std::find_if(dbsArray.begin(), dbsArray.end(), [](BSONElement elem) { + if (!elem.isABSONObj()) { + return false; + } + auto bsonObj = elem.Obj(); + std::string databaseName = bsonObj.getStringField("name"); + return (databaseName == "admin"); + }); + if (adminIter != dbsArray.end()) { + std::iter_swap(adminIter, dbsArray.begin()); + } +} + void DatabasesCloner::_onListDatabaseFinish(const CommandCallbackArgs& cbd) { Status respStatus = cbd.response.status; if (respStatus.isOK()) { @@ -239,24 +277,44 @@ void DatabasesCloner::_onListDatabaseFinish(const CommandCallbackArgs& cbd) { UniqueLock lk(_mutex); if (!respStatus.isOK()) { - LOG(1) << "listDatabases failed: " << respStatus; + LOG(1) << "'listDatabases' failed: " << respStatus; _fail_inlock(&lk, respStatus); return; } - const auto respBSON = cbd.response.data; - // There should not be any cloners yet + // There should not be any cloners yet. invariant(_databaseCloners.size() == 0); - const auto dbsElem = respBSON["databases"].Obj(); - BSONForEach(arrayElement, dbsElem) { + const auto respBSON = cbd.response.data; + + auto databasesArray = _parseListDatabasesResponse(respBSON); + if (!databasesArray.isOK()) { + LOG(1) << "'listDatabases' returned a malformed response: " + << databasesArray.getStatus().toString(); + _fail_inlock(&lk, databasesArray.getStatus()); + return; + } + + auto dbsArray = databasesArray.getValue(); + // Ensure that the 'admin' database is the first element in the array of databases so that it + // will be the first to be cloned. This allows users to authenticate against a database while + // initial sync is occurring. + _setAdminAsFirst(dbsArray); + + for (BSONElement arrayElement : dbsArray) { const BSONObj dbBSON = arrayElement.Obj(); // Check to see if we want to exclude this db from the clone. if (!_includeDbFn(dbBSON)) { - LOG(1) << "excluding db: " << dbBSON; + LOG(1) << "Excluding database from the 'listDatabases' response: " << dbBSON; continue; } + if (!dbBSON.hasField("name")) { + LOG(1) << "Excluding database due to the 'listDatabases' response not containing a " + "'name' field for this entry: " + << dbBSON; + } + const std::string dbName = dbBSON["name"].str(); std::shared_ptr<DatabaseCloner> dbCloner{nullptr}; @@ -321,7 +379,6 @@ void DatabasesCloner::_onListDatabaseFinish(const CommandCallbackArgs& cbd) { // add cloner to list. _databaseCloners.push_back(dbCloner); } - if (_databaseCloners.size() == 0) { if (_status.isOK()) { _succeed_inlock(&lk); diff --git a/src/mongo/db/repl/databases_cloner.h b/src/mongo/db/repl/databases_cloner.h index 53cced9ee4f..13f11f1e2a1 100644 --- a/src/mongo/db/repl/databases_cloner.h +++ b/src/mongo/db/repl/databases_cloner.h @@ -34,6 +34,7 @@ #include "mongo/base/disallow_copying.h" #include "mongo/base/status.h" +#include "mongo/base/status_with.h" #include "mongo/bson/bsonobj.h" #include "mongo/client/fetcher.h" #include "mongo/db/namespace_string.h" @@ -104,6 +105,18 @@ public: */ void setScheduleDbWorkFn_forTest(const CollectionCloner::ScheduleDbWorkFn& scheduleDbWorkFn); + /** + * Calls DatabasesCloner::_setAdminAsFirst. + * For testing only. + */ + void setAdminAsFirst_forTest(std::vector<BSONElement>& dbsArray); + + /** + * Calls DatabasesCloner::_parseListDatabasesResponse. + * For testing only. + */ + StatusWith<std::vector<BSONElement>> parseListDatabasesResponse_forTest(BSONObj dbResponse); + private: bool _isActive_inlock() const; @@ -135,6 +148,23 @@ private: void _onListDatabaseFinish(const CommandCallbackArgs& cbd); + /** + * Takes a vector of BSONElements and scans for an element that contains a 'name' field with the + * value 'admin'. If found, the element is swapped with the first element in the vector. + * Otherwise, return. + * + * Used to parse the BSONResponse returned by listDatabases. + */ + void _setAdminAsFirst(std::vector<BSONElement>& dbsArray); + + /** + * Takes a 'listDatabases' command response and parses the response into a + * vector of BSON elements. + * + * If the input response is malformed, Status ErrorCodes::BadValue will be returned. + */ + StatusWith<std::vector<BSONElement>> _parseListDatabasesResponse(BSONObj dbResponse); + // // All member variables are labeled with one of the following codes indicating the // synchronization rules for accessing them. diff --git a/src/mongo/db/repl/databases_cloner_test.cpp b/src/mongo/db/repl/databases_cloner_test.cpp index 81552201485..e44114e5d58 100644 --- a/src/mongo/db/repl/databases_cloner_test.cpp +++ b/src/mongo/db/repl/databases_cloner_test.cpp @@ -314,6 +314,15 @@ protected: ASSERT_OK(result); }; + std::unique_ptr<DatabasesCloner> makeDummyDatabasesCloner() { + return stdx::make_unique<DatabasesCloner>(&getStorage(), + &getExecutor(), + &getDbWorkThreadPool(), + HostAndPort{"local:1234"}, + [](const BSONObj&) { return true; }, + [](const Status&) {}); + } + private: executor::ThreadPoolMock::Options makeThreadPoolMockOptions() const override; @@ -450,6 +459,87 @@ TEST_F(DBsClonerTest, StartupReturnsInternalErrorAfterSuccessfulStartup) { ASSERT_TRUE(cloner.isActive()); } +TEST_F(DBsClonerTest, ParseAndSetAdminFirstWhenAdminInListDatabasesResponse) { + const Responses responsesWithAdmin = { + {"listDatabases", fromjson("{ok:1, databases:[{name:'a'}, {name:'aab'}, {name:'admin'}]}")}, + {"listDatabases", fromjson("{ok:1, databases:[{name:'admin'}, {name:'a'}, {name:'b'}]}")}, + }; + std::unique_ptr<DatabasesCloner> cloner = makeDummyDatabasesCloner(); + + for (auto&& resp : responsesWithAdmin) { + auto parseResponseStatus = cloner->parseListDatabasesResponse_forTest(resp.second); + ASSERT_TRUE(parseResponseStatus.isOK()); + std::vector<BSONElement> dbNamesArray = parseResponseStatus.getValue(); + cloner->setAdminAsFirst_forTest(dbNamesArray); + ASSERT_EQUALS("admin", dbNamesArray[0].Obj().firstElement().str()); + } +} + +TEST_F(DBsClonerTest, ParseAndAttemptSetAdminFirstWhenAdminNotInListDatabasesResponse) { + const Responses responsesWithoutAdmin = { + {"listDatabases", fromjson("{ok:1, databases:[{name:'a'}, {name:'aab'}, {name:'abc'}]}")}, + {"listDatabases", fromjson("{ok:1, databases:[{name:'foo'}, {name:'a'}, {name:'b'}]}")}, + {"listDatabases", fromjson("{ok:1, databases:[{name:1}, {name:2}, {name:3}]}")}, + }; + std::unique_ptr<DatabasesCloner> cloner = makeDummyDatabasesCloner(); + + for (auto&& resp : responsesWithoutAdmin) { + auto parseResponseStatus = cloner->parseListDatabasesResponse_forTest(resp.second); + ASSERT_TRUE(parseResponseStatus.isOK()); + std::vector<BSONElement> dbNamesArray = parseResponseStatus.getValue(); + std::string expectedResult = dbNamesArray[0].Obj().firstElement().str(); + cloner->setAdminAsFirst_forTest(dbNamesArray); + ASSERT_EQUALS(expectedResult, dbNamesArray[0].Obj().firstElement().str()); + } +} + + +TEST_F(DBsClonerTest, ParseListDatabasesResponseWithMalformedResponses) { + Status expectedResultForNoDatabasesField{ + ErrorCodes::BadValue, + "The 'listDatabases' command response does not contain a databases field."}; + Status expectedResultForNoArrayOfDatabases{ + ErrorCodes::BadValue, + "The 'listDatabases' command response is unable to be transformed into an array."}; + + const Responses responsesWithoutDatabasesField = { + {"listDatabases", fromjson("{ok:1, fake:[{name:'a'}, {name:'aab'}, {name:'foo'}]}")}, + {"listDatabases", fromjson("{ok:1, fake:[{name:'admin'}, {name:'a'}, {name:'b'}]}")}, + }; + + const Responses responsesWithoutArrayOfDatabases = { + {"listDatabases", fromjson("{ok:1, databases:1}")}, + {"listDatabases", fromjson("{ok:1, databases:'abc'}")}, + }; + + const Responses responsesWithInvalidAdminNameField = { + {"listDatabases", fromjson("{ok:1, databases:[{name:'a'}, {name:'aab'}, {fake:'admin'}]}")}, + {"listDatabases", fromjson("{ok:1, databases:[{fake:'admin'}, {name:'a'}, {name:'b'}]}")}, + }; + + std::unique_ptr<DatabasesCloner> cloner = makeDummyDatabasesCloner(); + + for (auto&& resp : responsesWithoutDatabasesField) { + auto parseResponseStatus = cloner->parseListDatabasesResponse_forTest(resp.second); + ASSERT_EQ(parseResponseStatus.getStatus(), expectedResultForNoDatabasesField); + } + + for (auto&& resp : responsesWithoutArrayOfDatabases) { + auto parseResponseStatus = cloner->parseListDatabasesResponse_forTest(resp.second); + ASSERT_EQ(parseResponseStatus.getStatus(), expectedResultForNoArrayOfDatabases); + } + + for (auto&& resp : responsesWithInvalidAdminNameField) { + auto parseResponseStatus = cloner->parseListDatabasesResponse_forTest(resp.second); + ASSERT_TRUE(parseResponseStatus.isOK()); + // We expect no elements to be swapped. + std::vector<BSONElement> dbNamesArray = parseResponseStatus.getValue(); + std::string expectedResult = dbNamesArray[0].Obj().firstElement().str(); + cloner->setAdminAsFirst_forTest(dbNamesArray); + ASSERT_EQUALS(expectedResult, dbNamesArray[0].Obj().firstElement().str()); + } +} + TEST_F(DBsClonerTest, FailsOnListDatabases) { Status result{Status::OK()}; Status expectedResult{ErrorCodes::BadValue, "foo"}; diff --git a/src/mongo/db/repl/initial_syncer.cpp b/src/mongo/db/repl/initial_syncer.cpp index e9a920bb37b..f3bc1a8acd3 100644 --- a/src/mongo/db/repl/initial_syncer.cpp +++ b/src/mongo/db/repl/initial_syncer.cpp @@ -1189,7 +1189,8 @@ Status InitialSyncer::_scheduleLastOplogEntryFetcher_inlock(Fetcher::CallbackFn query, callback, rpc::ServerSelectionMetadata(true, boost::none).toBSON(), - RemoteCommandRequest::kNoTimeout, + RemoteCommandRequest::kNoTimeout /* find network timeout */, + RemoteCommandRequest::kNoTimeout /* getMore network timeout */, RemoteCommandRetryScheduler::makeRetryPolicy( numInitialSyncOplogFindAttempts, executor::RemoteCommandRequest::kNoTimeout, diff --git a/src/mongo/db/repl/master_slave.cpp b/src/mongo/db/repl/master_slave.cpp index 96cfe58efdc..6929d5be297 100644 --- a/src/mongo/db/repl/master_slave.cpp +++ b/src/mongo/db/repl/master_slave.cpp @@ -168,7 +168,7 @@ BSONObj ReplSource::jsobj() { BSONObjBuilder dbsNextPassBuilder; int n = 0; - for (set<string>::iterator i = addDbNextPass.begin(); i != addDbNextPass.end(); i++) { + for (set<std::string>::iterator i = addDbNextPass.begin(); i != addDbNextPass.end(); i++) { n++; dbsNextPassBuilder.appendBool(*i, 1); } @@ -177,7 +177,8 @@ BSONObj ReplSource::jsobj() { BSONObjBuilder incompleteCloneDbsBuilder; n = 0; - for (set<string>::iterator i = incompleteCloneDbs.begin(); i != incompleteCloneDbs.end(); i++) { + for (set<std::string>::iterator i = incompleteCloneDbs.begin(); i != incompleteCloneDbs.end(); + i++) { n++; incompleteCloneDbsBuilder.appendBool(*i, 1); } @@ -188,7 +189,7 @@ BSONObj ReplSource::jsobj() { } void ReplSource::ensureMe(OperationContext* txn) { - string myname = getHostName(); + std::string myname = getHostName(); // local.me is an identifier for a server for getLastError w:2+ bool exists = Helpers::getSingleton(txn, "local.me", _me); @@ -378,10 +379,10 @@ public: } virtual bool run(OperationContext* txn, - const string& ns, + const std::string& ns, BSONObj& cmdObj, int options, - string& errmsg, + std::string& errmsg, BSONObjBuilder& result) { HandshakeArgs handshake; Status status = handshake.initialize(cmdObj); @@ -398,7 +399,7 @@ public: } handshakeCmd; bool replHandshake(DBClientConnection* conn, const OID& myRID) { - string myname = getHostName(); + std::string myname = getHostName(); BSONObjBuilder cmd; cmd.append("handshake", myRID); @@ -450,7 +451,7 @@ void ReplSource::forceResync(OperationContext* txn, const char* requester) { BSONElement e = i.next(); if (e.eoo()) break; - string name = e.embeddedObject().getField("name").valuestr(); + std::string name = e.embeddedObject().getField("name").valuestr(); if (!e.embeddedObject().getBoolField("empty")) { if (name != "local") { if (only.empty() || only == name) { @@ -481,7 +482,7 @@ Status ReplSource::_updateIfDoneWithInitialSync() { return Status::OK(); } -void ReplSource::resyncDrop(OperationContext* txn, const string& dbName) { +void ReplSource::resyncDrop(OperationContext* txn, const std::string& dbName) { log() << "resync: dropping database " << dbName; invariant(txn->lockState()->isW()); @@ -531,13 +532,13 @@ void ReplSource::resync(OperationContext* txn, const std::string& dbName) { static DatabaseIgnorer ___databaseIgnorer; -void DatabaseIgnorer::doIgnoreUntilAfter(const string& db, const Timestamp& futureOplogTime) { +void DatabaseIgnorer::doIgnoreUntilAfter(const std::string& db, const Timestamp& futureOplogTime) { if (futureOplogTime > _ignores[db]) { _ignores[db] = futureOplogTime; } } -bool DatabaseIgnorer::ignoreAt(const string& db, const Timestamp& currentOplogTime) { +bool DatabaseIgnorer::ignoreAt(const std::string& db, const Timestamp& currentOplogTime) { if (_ignores[db].isNull()) { return false; } @@ -627,7 +628,7 @@ bool ReplSource::handleDuplicateDbName(OperationContext* txn, // The database is present on the master and no conflicting databases // are present on the master. Drop any local conflicts. - for (set<string>::const_iterator i = duplicates.begin(); i != duplicates.end(); ++i) { + for (set<std::string>::const_iterator i = duplicates.begin(); i != duplicates.end(); ++i) { ___databaseIgnorer.doIgnoreUntilAfter(*i, lastTime); incompleteCloneDbs.erase(*i); addDbNextPass.erase(*i); @@ -822,10 +823,10 @@ void ReplSource::_sync_pullOpLog_applyOperation(OperationContext* txn, } void ReplSource::syncToTailOfRemoteLog() { - string _ns = ns(); + std::string _ns = ns(); BSONObjBuilder b; if (!only.empty()) { - b.appendRegex("ns", string("^") + pcrecpp::RE::QuoteMeta(only)); + b.appendRegex("ns", std::string("^") + pcrecpp::RE::QuoteMeta(only)); } BSONObj last = oplogReader.findOne(_ns.c_str(), Query(b.done()).sort(BSON("$natural" << -1))); if (!last.isEmpty()) { @@ -873,7 +874,7 @@ public: */ int ReplSource::_sync_pullOpLog(OperationContext* txn, int& nApplied) { int okResultCode = restartSyncAfterSleep; - string ns = string("local.oplog.$") + sourceName(); + std::string ns = std::string("local.oplog.$") + sourceName(); LOG(2) << "sync_pullOpLog " << ns << " syncedTo:" << syncedTo.toStringLong() << '\n'; bool tailing = true; @@ -893,7 +894,7 @@ int ReplSource::_sync_pullOpLog(OperationContext* txn, int& nApplied) { BSONElement e = i.next(); if (e.eoo()) break; - string name = e.embeddedObject().getField("name").valuestr(); + std::string name = e.embeddedObject().getField("name").valuestr(); if (!e.embeddedObject().getBoolField("empty")) { if (name != "local") { if (only.empty() || only == name) { @@ -917,7 +918,7 @@ int ReplSource::_sync_pullOpLog(OperationContext* txn, int& nApplied) { if (!only.empty()) { // note we may here skip a LOT of data table scanning, a lot of work for the master. // maybe append "\\." here? - query.appendRegex("ns", string("^") + pcrecpp::RE::QuoteMeta(only)); + query.appendRegex("ns", std::string("^") + pcrecpp::RE::QuoteMeta(only)); } BSONObj queryObj = query.done(); // e.g. queryObj = { ts: { $gte: syncedTo } } @@ -936,7 +937,7 @@ int ReplSource::_sync_pullOpLog(OperationContext* txn, int& nApplied) { // show any deferred database creates from a previous pass { - set<string>::iterator i = addDbNextPass.begin(); + set<std::string>::iterator i = addDbNextPass.begin(); if (i != addDbNextPass.end()) { BSONObjBuilder b; b.append("ns", *i + '.'); @@ -980,7 +981,7 @@ int ReplSource::_sync_pullOpLog(OperationContext* txn, int& nApplied) { BSONObj op = oplogReader.nextSafe(); BSONElement ts = op.getField("ts"); if (ts.type() != Date && ts.type() != bsonTimestamp) { - string err = op.getStringField("$err"); + std::string err = op.getStringField("$err"); if (!err.empty()) { // 13051 is "tailable cursor requested on non capped collection" if (op.getIntField("code") == 13051) { @@ -1148,7 +1149,7 @@ int ReplSource::sync(OperationContext* txn, int& nApplied) { // FIXME Handle cases where this db isn't on default port, or default port is spec'd in // hostName. - if ((string("localhost") == hostName || string("127.0.0.1") == hostName) && + if ((std::string("localhost") == hostName || std::string("127.0.0.1") == hostName) && serverGlobalParams.port == ServerGlobalParams::DefaultDBPort) { log() << "can't sync from self (localhost). sources configuration may be wrong." << endl; sleepsecs(5); @@ -1293,7 +1294,7 @@ static void replMain(OperationContext* txn) { if (s) { stringstream ss; ss << "sleep " << s << " sec before next pass"; - string msg = ss.str(); + std::string msg = ss.str(); if (!serverGlobalParams.quiet) log() << msg << endl; ReplInfo r(msg.c_str()); diff --git a/src/mongo/db/repl/oplog.cpp b/src/mongo/db/repl/oplog.cpp index 43a1eba5042..76df271ed78 100644 --- a/src/mongo/db/repl/oplog.cpp +++ b/src/mongo/db/repl/oplog.cpp @@ -116,6 +116,11 @@ namespace { // cached copy...so don't rename, drop, etc.!!! Collection* _localOplogCollection = nullptr; +// Specifies whether we abort initial sync when attempting to apply a renameCollection operation. +// If set to true, users risk corrupting their data. This should only be enabled by expert users +// of the server who understand the risks this poses. +MONGO_EXPORT_SERVER_PARAMETER(allowUnsafeRenamesDuringInitialSync, bool, false); + PseudoRandom hashGenerator(std::unique_ptr<SecureRandom>(SecureRandom::create())->nextInt64()); // Synchronizes the section where a new Timestamp is generated and when it actually @@ -663,6 +668,45 @@ std::map<std::string, ApplyOpMetadata> opsMap = { } // namespace +std::pair<BSONObj, NamespaceString> prepForApplyOpsIndexInsert(const BSONElement& fieldO, + const BSONObj& op, + const NamespaceString& requestNss) { + uassert(ErrorCodes::NoSuchKey, + str::stream() << "Missing expected index spec in field 'o': " << op, + !fieldO.eoo()); + uassert(ErrorCodes::TypeMismatch, + str::stream() << "Expected object for index spec in field 'o': " << op, + fieldO.isABSONObj()); + BSONObj indexSpec = fieldO.embeddedObject(); + + std::string indexNs; + uassertStatusOK(bsonExtractStringField(indexSpec, "ns", &indexNs)); + const NamespaceString indexNss(indexNs); + uassert(ErrorCodes::InvalidNamespace, + str::stream() << "Invalid namespace in index spec: " << op, + indexNss.isValid()); + uassert(ErrorCodes::InvalidNamespace, + str::stream() << "Database name mismatch for database (" << requestNss.db() + << ") while creating index: " + << op, + requestNss.db() == indexNss.db()); + + if (!indexSpec["v"]) { + // If the "v" field isn't present in the index specification, then we assume it is a + // v=1 index from an older version of MongoDB. This is because + // (1) we haven't built v=0 indexes as the default for a long time, and + // (2) the index version has been included in the corresponding oplog entry since + // v=2 indexes were introduced. + BSONObjBuilder bob; + + bob.append("v", static_cast<int>(IndexVersion::kV1)); + bob.appendElements(indexSpec); + + indexSpec = bob.obj(); + } + + return std::make_pair(indexSpec, indexNss); +} // @return failure status if an update should have happened and the document DNE. // See replset initial sync code. Status applyOperation_inlock(OperationContext* txn, @@ -688,6 +732,7 @@ Status applyOperation_inlock(OperationContext* txn, o = fieldO.embeddedObject(); const StringData ns = fieldNs.valueStringData(); + NamespaceString requestNss{ns}; BSONObj o2; if (fieldO2.isABSONObj()) @@ -718,27 +763,11 @@ Status applyOperation_inlock(OperationContext* txn, invariant(*opType != 'c'); // commands are processed in applyCommand_inlock() if (*opType == 'i') { - if (nsToCollectionSubstring(ns) == "system.indexes") { - uassert(ErrorCodes::NoSuchKey, - str::stream() << "Missing expected index spec in field 'o': " << op, - !fieldO.eoo()); - uassert(ErrorCodes::TypeMismatch, - str::stream() << "Expected object for index spec in field 'o': " << op, - fieldO.isABSONObj()); - BSONObj indexSpec = fieldO.embeddedObject(); - - std::string indexNs; - uassertStatusOK(bsonExtractStringField(indexSpec, "ns", &indexNs)); - const NamespaceString indexNss(indexNs); - uassert(ErrorCodes::InvalidNamespace, - str::stream() << "Invalid namespace in index spec: " << op, - indexNss.isValid()); - uassert(ErrorCodes::InvalidNamespace, - str::stream() << "Database name mismatch for database (" - << nsToDatabaseSubstring(ns) - << ") while creating index: " - << op, - nsToDatabaseSubstring(ns) == indexNss.db()); + if (requestNss.isSystemDotIndexes()) { + BSONObj indexSpec; + NamespaceString indexNss; + std::tie(indexSpec, indexNss) = + repl::prepForApplyOpsIndexInsert(fieldO, op, requestNss); // Check if collection exists. auto indexCollection = db->getCollection(indexNss); @@ -749,20 +778,6 @@ Status applyOperation_inlock(OperationContext* txn, opCounters->gotInsert(); - if (!indexSpec["v"]) { - // If the "v" field isn't present in the index specification, then we assume it is a - // v=1 index from an older version of MongoDB. This is because - // (1) we haven't built v=0 indexes as the default for a long time, and - // (2) the index version has been included in the corresponding oplog entry since - // v=2 indexes were introduced. - BSONObjBuilder bob; - - bob.append("v", static_cast<int>(IndexVersion::kV1)); - bob.appendElements(indexSpec); - - indexSpec = bob.obj(); - } - bool relaxIndexConstraints = ReplicationCoordinator::get(txn)->shouldRelaxIndexConstraints(indexNss); if (indexSpec["background"].trueValue()) { @@ -863,13 +878,12 @@ Status applyOperation_inlock(OperationContext* txn, BSONObjBuilder b; b.append(o.getField("_id")); - const NamespaceString requestNs(ns); - UpdateRequest request(requestNs); + UpdateRequest request(requestNss); request.setQuery(b.done()); request.setUpdates(o); request.setUpsert(); - UpdateLifecycleImpl updateLifecycle(requestNs); + UpdateLifecycleImpl updateLifecycle(requestNss); request.setLifecycle(&updateLifecycle); UpdateResult res = update(txn, db, request); @@ -894,13 +908,12 @@ Status applyOperation_inlock(OperationContext* txn, str::stream() << "Failed to apply update due to missing _id: " << op.toString(), updateCriteria.hasField("_id")); - const NamespaceString requestNs(ns); - UpdateRequest request(requestNs); + UpdateRequest request(requestNss); request.setQuery(updateCriteria); request.setUpdates(o); request.setUpsert(upsert); - UpdateLifecycleImpl updateLifecycle(requestNs); + UpdateLifecycleImpl updateLifecycle(requestNss); request.setLifecycle(&updateLifecycle); UpdateResult ur = update(txn, db, request); @@ -954,7 +967,12 @@ Status applyOperation_inlock(OperationContext* txn, o.hasField("_id")); if (opType[1] == 0) { - deleteObjects(txn, collection, ns, o, PlanExecutor::YIELD_MANUAL, /*justOne*/ valueB); + deleteObjects(txn, + collection, + requestNss.ns().c_str(), + o, + PlanExecutor::YIELD_MANUAL, + /*justOne*/ valueB); } else verify(opType[1] == 'b'); // "db" advertisement if (incrementOpsAppliedStats) { @@ -970,16 +988,6 @@ Status applyOperation_inlock(OperationContext* txn, 14825, str::stream() << "error in applyOperation : unknown opType " << *opType); } - // AuthorizationManager's logOp method registers a RecoveryUnit::Change and to do so we need - // to a new WriteUnitOfWork, if we dont have a wrapping unit of work already. If we already - // have a wrapping WUOW, the extra nexting is harmless. The logOp really should have been - // done in the WUOW that did the write, but this won't happen because applyOps turns off - // observers. - WriteUnitOfWork wuow(txn); - getGlobalAuthorizationManager()->logOp( - txn, opType, ns.toString().c_str(), o, fieldO2.isABSONObj() ? &o2 : NULL); - wuow.commit(); - return Status::OK(); } @@ -1021,9 +1029,15 @@ Status applyCommand_inlock(OperationContext* txn, // Applying renameCollection during initial sync might lead to data corruption, so we restart // the initial sync. if (!inSteadyStateReplication && o.firstElementFieldName() == std::string("renameCollection")) { - return Status(ErrorCodes::OplogOperationUnsupported, - str::stream() << "Applying renameCollection not supported in initial sync: " - << redact(op)); + if (!allowUnsafeRenamesDuringInitialSync.load()) { + return Status(ErrorCodes::OplogOperationUnsupported, + str::stream() + << "Applying renameCollection not supported in initial sync: " + << redact(op)); + } + warning() << "allowUnsafeRenamesDuringInitialSync set to true. Applying renameCollection " + "operation during initial sync even though it may lead to data corruption: " + << redact(op); } // Applying commands in repl is done under Global W-lock, so it is safe to not @@ -1071,11 +1085,8 @@ Status applyCommand_inlock(OperationContext* txn, break; } default: - if (_oplogCollectionName == masterSlaveOplogName) { - error() << "Failed command " << redact(o) << " on " << nss.db() - << " with status " << status << " during oplog application"; - } else if (curOpToApply.acceptableErrors.find(status.code()) == - curOpToApply.acceptableErrors.end()) { + if (curOpToApply.acceptableErrors.find(status.code()) == + curOpToApply.acceptableErrors.end()) { error() << "Failed command " << redact(o) << " on " << nss.db() << " with status " << status << " during oplog application"; return status; diff --git a/src/mongo/db/repl/oplog.h b/src/mongo/db/repl/oplog.h index b2078e93d28..f51aadb8dcf 100644 --- a/src/mongo/db/repl/oplog.h +++ b/src/mongo/db/repl/oplog.h @@ -104,6 +104,15 @@ void oplogCheckCloseDatabase(OperationContext* txn, Database* db); using IncrementOpsAppliedStatsFn = stdx::function<void()>; /** + * Take the object field of a BSONObj, the BSONObj, and the namespace of + * the operation and perform necessary validation to ensure the BSONObj is a + * properly-formed command to insert into system.indexes. This is only to + * be used for insert operations into system.indexes. It is called via applyOps. + */ +std::pair<BSONObj, NamespaceString> prepForApplyOpsIndexInsert(const BSONElement& fieldO, + const BSONObj& op, + const NamespaceString& requestNss); +/** * Take a non-command op and apply it locally * Used for applying from an oplog * @param inSteadyStateReplication convert some updates to upserts for idempotency reasons diff --git a/src/mongo/db/repl/oplog_fetcher.cpp b/src/mongo/db/repl/oplog_fetcher.cpp index ece0ef266fd..c9b96ab77f7 100644 --- a/src/mongo/db/repl/oplog_fetcher.cpp +++ b/src/mongo/db/repl/oplog_fetcher.cpp @@ -36,6 +36,7 @@ #include "mongo/db/commands/server_status_metric.h" #include "mongo/db/jsobj.h" #include "mongo/db/repl/replication_coordinator.h" +#include "mongo/db/server_parameters.h" #include "mongo/db/stats/timer_stats.h" #include "mongo/rpc/metadata/oplog_query_metadata.h" #include "mongo/rpc/metadata/server_selection_metadata.h" @@ -56,8 +57,12 @@ MONGO_FP_DECLARE(stopReplProducer); namespace { -Seconds kOplogInitialFindMaxTime{60}; -Seconds kOplogQueryNetworkTimeout{65}; // 5 seconds past the find command's 1 minute maxTimeMs +// Number of seconds for the `maxTimeMS` on the initial `find` command. +MONGO_EXPORT_SERVER_PARAMETER(oplogInitialFindMaxSeconds, int, 60); + +// Number of milliseconds to add to the `find` and `getMore` timeouts to calculate the network +// timeout for the requests. +const Milliseconds kNetworkTimeoutBufferMS{5000}; Counter64 readersCreatedStats; ServerStatusMetricField<Counter64> displayReadersCreated("repl.network.readersCreated", @@ -91,17 +96,22 @@ Milliseconds calculateAwaitDataTimeout(const ReplSetConfig& config) { */ BSONObj makeFindCommandObject(const NamespaceString& nss, long long currentTerm, - OpTime lastOpTimeFetched) { + OpTime lastOpTimeFetched, + Milliseconds fetcherMaxTimeMS) { BSONObjBuilder cmdBob; cmdBob.append("find", nss.coll()); cmdBob.append("filter", BSON("ts" << BSON("$gte" << lastOpTimeFetched.getTimestamp()))); cmdBob.append("tailable", true); cmdBob.append("oplogReplay", true); cmdBob.append("awaitData", true); - cmdBob.append("maxTimeMS", durationCount<Milliseconds>(kOplogInitialFindMaxTime)); + cmdBob.append("maxTimeMS", durationCount<Milliseconds>(fetcherMaxTimeMS)); if (currentTerm != OpTime::kUninitializedTerm) { cmdBob.append("term", currentTerm); } + if (serverGlobalParams.featureCompatibility.version.load() == + ServerGlobalParams::FeatureCompatibility::Version::k34) { + cmdBob.append("readConcern", BSON("afterOpTime" << lastOpTimeFetched.toBSON())); + } return cmdBob.obj(); } @@ -450,6 +460,14 @@ BSONObj OplogFetcher::getMetadataObject_forTest() const { } Milliseconds OplogFetcher::getAwaitDataTimeout_forTest() const { + return _getGetMoreMaxTime(); +} + +Milliseconds OplogFetcher::_getFindMaxTime() const { + return Milliseconds(oplogInitialFindMaxSeconds.load() * 1000); +} + +Milliseconds OplogFetcher::_getGetMoreMaxTime() const { return _awaitDataTimeout; } @@ -660,7 +678,7 @@ void OplogFetcher::_callback(const Fetcher::QueryResponseStatus& result, getMoreBob->appendElements(makeGetMoreCommandObject(queryResponse.nss, queryResponse.cursorId, lastCommittedWithCurrentTerm, - _awaitDataTimeout)); + _getGetMoreMaxTime())); } void OplogFetcher::_finishCallback(Status status) { @@ -691,10 +709,11 @@ std::unique_ptr<Fetcher> OplogFetcher::_makeFetcher(long long currentTerm, _executor, _source, _nss.db().toString(), - makeFindCommandObject(_nss, currentTerm, lastFetchedOpTime), + makeFindCommandObject(_nss, currentTerm, lastFetchedOpTime, _getFindMaxTime()), stdx::bind(&OplogFetcher::_callback, this, stdx::placeholders::_1, stdx::placeholders::_3), _metadataObject, - kOplogQueryNetworkTimeout); + _getFindMaxTime() + kNetworkTimeoutBufferMS, + _getGetMoreMaxTime() + kNetworkTimeoutBufferMS); } bool OplogFetcher::_isShuttingDown() const { diff --git a/src/mongo/db/repl/oplog_fetcher.h b/src/mongo/db/repl/oplog_fetcher.h index ad4fde296b1..54bfbabbf8d 100644 --- a/src/mongo/db/repl/oplog_fetcher.h +++ b/src/mongo/db/repl/oplog_fetcher.h @@ -235,6 +235,16 @@ private: void _finishCallback(Status status, OpTimeWithHash opTimeWithHash); /** + * Returns how long the `find` command should wait before timing out. + */ + virtual Milliseconds _getFindMaxTime() const; + + /** + * Returns how long the `getMore` command should wait before timing out. + */ + virtual Milliseconds _getGetMoreMaxTime() const; + + /** * Creates a new instance of the fetcher to tail the remote oplog starting at the given optime. */ std::unique_ptr<Fetcher> _makeFetcher(long long currentTerm, OpTime lastFetchedOpTime); diff --git a/src/mongo/db/repl/repl_client_info.cpp b/src/mongo/db/repl/repl_client_info.cpp index 3e98a0cb9d3..5b77e3aa6f3 100644 --- a/src/mongo/db/repl/repl_client_info.cpp +++ b/src/mongo/db/repl/repl_client_info.cpp @@ -26,6 +26,8 @@ * it in the license file. */ +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kReplication + #include "mongo/platform/basic.h" #include "mongo/db/repl/repl_client_info.h" @@ -36,6 +38,7 @@ #include "mongo/db/operation_context.h" #include "mongo/db/repl/replication_coordinator_global.h" #include "mongo/util/decorable.h" +#include "mongo/util/log.h" namespace mongo { namespace repl { @@ -48,10 +51,23 @@ void ReplClientInfo::setLastOp(const OpTime& ot) { _lastOp = ot; } + void ReplClientInfo::setLastOpToSystemLastOpTime(OperationContext* txn) { ReplicationCoordinator* replCoord = repl::ReplicationCoordinator::get(txn->getServiceContext()); if (replCoord->isReplEnabled() && txn->writesAreReplicated()) { - setLastOp(replCoord->getMyLastAppliedOpTime()); + auto systemOpTime = replCoord->getMyLastAppliedOpTime(); + + // If the system optime has gone backwards, that must mean that there was a rollback. + // This is safe, but the last op for a Client should never go backwards, so just leave + // the last op for this Client as it was. + if (systemOpTime >= _lastOp) { + _lastOp = systemOpTime; + } else { + log() << "Not setting the last OpTime for this Client from " << _lastOp + << " to the current system time of " << systemOpTime + << " as that would be moving the OpTime backwards. This should only happen if " + "there was a rollback recently"; + } } } diff --git a/src/mongo/db/repl/repl_set_config.cpp b/src/mongo/db/repl/repl_set_config.cpp index 1ad2d245ac6..024c3ff04a5 100644 --- a/src/mongo/db/repl/repl_set_config.cpp +++ b/src/mongo/db/repl/repl_set_config.cpp @@ -35,11 +35,19 @@ #include "mongo/bson/util/bson_check.h" #include "mongo/bson/util/bson_extract.h" #include "mongo/db/jsobj.h" +#include "mongo/db/mongod_options.h" #include "mongo/db/server_options.h" +#include "mongo/db/server_parameters.h" #include "mongo/stdx/functional.h" #include "mongo/util/stringutils.h" namespace mongo { +/** + * Dont run any sharding validations. Can not be combined with --configsvr or shardvr. Intended to + * allow restarting config server or shard as an independent replica set. + */ +MONGO_EXPORT_STARTUP_SERVER_PARAMETER(skipShardingConfigurationChecks, bool, false); + namespace repl { const size_t ReplSetConfig::kMaxMembers; @@ -552,7 +560,8 @@ Status ReplSetConfig::validate() const { "servers cannot have a non-zero slaveDelay"); } } - if (serverGlobalParams.clusterRole != ClusterRole::ConfigServer) { + if (serverGlobalParams.clusterRole != ClusterRole::ConfigServer && + !skipShardingConfigurationChecks) { return Status(ErrorCodes::BadValue, "Nodes being used for config servers must be started with the " "--configsvr flag"); diff --git a/src/mongo/db/repl/replication_coordinator_external_state_impl.cpp b/src/mongo/db/repl/replication_coordinator_external_state_impl.cpp index cfc6dbb55e1..05d070c08fe 100644 --- a/src/mongo/db/repl/replication_coordinator_external_state_impl.cpp +++ b/src/mongo/db/repl/replication_coordinator_external_state_impl.cpp @@ -240,8 +240,11 @@ void ReplicationCoordinatorExternalStateImpl::startSteadyStateReplication( _applierThread->startup(); log() << "Starting replication reporter thread"; invariant(!_syncSourceFeedbackThread); - _syncSourceFeedbackThread.reset(new stdx::thread(stdx::bind( - &SyncSourceFeedback::run, &_syncSourceFeedback, _taskExecutor.get(), _bgSync.get()))); + _syncSourceFeedbackThread.reset(new stdx::thread(stdx::bind(&SyncSourceFeedback::run, + &_syncSourceFeedback, + _taskExecutor.get(), + _bgSync.get(), + replCoord))); } void ReplicationCoordinatorExternalStateImpl::stopDataReplication(OperationContext* txn) { diff --git a/src/mongo/db/repl/replication_coordinator_impl.cpp b/src/mongo/db/repl/replication_coordinator_impl.cpp index 3de3d35bb61..ad1a9fd93ba 100644 --- a/src/mongo/db/repl/replication_coordinator_impl.cpp +++ b/src/mongo/db/repl/replication_coordinator_impl.cpp @@ -2922,6 +2922,26 @@ ReplicationCoordinatorImpl::_setCurrentRSConfig_inlock(const ReplSetConfig& newC const ReplSetConfig oldConfig = _rsConfig; _rsConfig = newConfig; _protVersion.store(_rsConfig.getProtocolVersion()); + + // Warn if running --nojournal and writeConcernMajorityJournalDefault = false + StorageEngine* storageEngine = getGlobalServiceContext()->getGlobalStorageEngine(); + if (storageEngine && !storageEngine->isDurable() && + (newConfig.getWriteConcernMajorityShouldJournal() && + (!oldConfig.isInitialized() || !oldConfig.getWriteConcernMajorityShouldJournal()))) { + log() << startupWarningsLog; + log() << "** WARNING: This replica set is running without journaling enabled but the " + << startupWarningsLog; + log() << "** writeConcernMajorityJournalDefault option to the replica set config " + << startupWarningsLog; + log() << "** is set to true. The writeConcernMajorityJournalDefault " + << startupWarningsLog; + log() << "** option to the replica set config must be set to false " + << startupWarningsLog; + log() << "** or w:majority write concerns will never complete." + << startupWarningsLog; + log() << startupWarningsLog; + } + log() << "New replica set config in use: " << _rsConfig.toBSON() << rsLog; _selfIndex = myIndex; if (_selfIndex >= 0) { diff --git a/src/mongo/db/repl/replication_coordinator_impl_elect_v1.cpp b/src/mongo/db/repl/replication_coordinator_impl_elect_v1.cpp index fbd976ec6b7..1dd9bf5b7c3 100644 --- a/src/mongo/db/repl/replication_coordinator_impl_elect_v1.cpp +++ b/src/mongo/db/repl/replication_coordinator_impl_elect_v1.cpp @@ -133,7 +133,9 @@ void ReplicationCoordinatorImpl::_startElectSelfV1() { return; } - log() << "conducting a dry run election to see if we could be elected"; + long long term = _topCoord->getTerm(); + + log() << "conducting a dry run election to see if we could be elected. current term: " << term; _voteRequester.reset(new VoteRequester); // This is necessary because the voteRequester may call directly into winning an @@ -141,12 +143,11 @@ void ReplicationCoordinatorImpl::_startElectSelfV1() { // _mutex again. lk.unlock(); - long long term = _topCoord->getTerm(); StatusWith<ReplicationExecutor::EventHandle> nextPhaseEvh = _voteRequester->start(&_replExecutor, _rsConfig, _selfIndex, - _topCoord->getTerm(), + term, true, // dry run lastOpTime); if (nextPhaseEvh.getStatus() == ErrorCodes::ShutdownInProgress) { @@ -165,7 +166,8 @@ void ReplicationCoordinatorImpl::_onDryRunComplete(long long originalTerm) { LockGuard lk(_topoMutex); if (_topCoord->getTerm() != originalTerm) { - log() << "not running for primary, we have been superceded already"; + log() << "not running for primary, we have been superseded already during dry run. " + << "original term: " << originalTerm << ", current term: " << _topCoord->getTerm(); return; } @@ -175,23 +177,24 @@ void ReplicationCoordinatorImpl::_onDryRunComplete(long long originalTerm) { log() << "not running for primary, we received insufficient votes"; return; } else if (endResult == VoteRequester::Result::kStaleTerm) { - log() << "not running for primary, we have been superceded already"; + log() << "not running for primary, we have been superseded already"; return; } else if (endResult != VoteRequester::Result::kSuccessfullyElected) { log() << "not running for primary, we received an unexpected problem"; return; } - log() << "dry election run succeeded, running for election"; + long long newTerm = originalTerm + 1; + log() << "dry election run succeeded, running for election in term " << newTerm; // Stepdown is impossible from this term update. TopologyCoordinator::UpdateTermResult updateTermResult; - _updateTerm_incallback(originalTerm + 1, &updateTermResult); + _updateTerm_incallback(newTerm, &updateTermResult); invariant(updateTermResult == TopologyCoordinator::UpdateTermResult::kUpdatedTerm); // Secure our vote for ourself first _topCoord->voteForMyselfV1(); // Store the vote in persistent storage. - LastVote lastVote{originalTerm + 1, _selfIndex}; + LastVote lastVote{newTerm, _selfIndex}; auto cbStatus = _replExecutor.scheduleDBWork( [this, lastVote](const ReplicationExecutor::CallbackArgs& cbData) { @@ -232,12 +235,19 @@ void ReplicationCoordinatorImpl::_startVoteRequester(long long newTerm) { LockGuard lk(_topoMutex); + if (_topCoord->getTerm() != newTerm) { + log() << "not running for primary, we have been superseded already while writing our last " + "vote. election term: " + << newTerm << ", current term: " << _topCoord->getTerm(); + return; + } + const auto lastOpTime = _isDurableStorageEngine() ? getMyLastDurableOpTime() : getMyLastAppliedOpTime(); _voteRequester.reset(new VoteRequester); - StatusWith<ReplicationExecutor::EventHandle> nextPhaseEvh = _voteRequester->start( - &_replExecutor, _rsConfig, _selfIndex, _topCoord->getTerm(), false, lastOpTime); + StatusWith<ReplicationExecutor::EventHandle> nextPhaseEvh = + _voteRequester->start(&_replExecutor, _rsConfig, _selfIndex, newTerm, false, lastOpTime); if (nextPhaseEvh.getStatus() == ErrorCodes::ShutdownInProgress) { return; } @@ -249,14 +259,15 @@ void ReplicationCoordinatorImpl::_startVoteRequester(long long newTerm) { lossGuard.dismiss(); } -void ReplicationCoordinatorImpl::_onVoteRequestComplete(long long originalTerm) { +void ReplicationCoordinatorImpl::_onVoteRequestComplete(long long newTerm) { invariant(_voteRequester); LoseElectionGuardV1 lossGuard(this); LockGuard lk(_topoMutex); - if (_topCoord->getTerm() != originalTerm) { - log() << "not becoming primary, we have been superceded already"; + if (_topCoord->getTerm() != newTerm) { + log() << "not becoming primary, we have been superseded already during election. " + << "election term: " << newTerm << ", current term: " << _topCoord->getTerm(); return; } @@ -267,7 +278,7 @@ void ReplicationCoordinatorImpl::_onVoteRequestComplete(long long originalTerm) log() << "not becoming primary, we received insufficient votes"; return; case VoteRequester::Result::kStaleTerm: - log() << "not becoming primary, we have been superceded already"; + log() << "not becoming primary, we have been superseded already"; return; case VoteRequester::Result::kSuccessfullyElected: log() << "election succeeded, assuming primary role in term " << _topCoord->getTerm(); diff --git a/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp b/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp index 6e87f882b48..0f550869546 100644 --- a/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp +++ b/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp @@ -431,7 +431,7 @@ TEST_F(ReplCoordTest, ElectionFailsWhenDryRunResponseContainsANewerTerm) { getReplCoord()->waitForElectionFinish_forTest(); stopCapturingLogMessages(); ASSERT_EQUALS( - 1, countLogLinesContaining("not running for primary, we have been superceded already")); + 1, countLogLinesContaining("not running for primary, we have been superseded already")); } TEST_F(ReplCoordTest, NodeWillNotStandForElectionDuringHeartbeatReconfig) { @@ -689,7 +689,7 @@ TEST_F(ReplCoordTest, ElectionFailsWhenVoteRequestResponseContainsANewerTerm) { getReplCoord()->waitForElectionFinish_forTest(); stopCapturingLogMessages(); ASSERT_EQUALS(1, - countLogLinesContaining("not becoming primary, we have been superceded already")); + countLogLinesContaining("not becoming primary, we have been superseded already")); } TEST_F(ReplCoordTest, ElectionFailsWhenTermChangesDuringDryRun) { @@ -728,8 +728,9 @@ TEST_F(ReplCoordTest, ElectionFailsWhenTermChangesDuringDryRun) { simulateSuccessfulDryRun(onDryRunRequest); stopCapturingLogMessages(); - ASSERT_EQUALS( - 1, countLogLinesContaining("not running for primary, we have been superceded already")); + ASSERT_EQUALS(1, + countLogLinesContaining( + "not running for primary, we have been superseded already during dry run")); } TEST_F(ReplCoordTest, ElectionFailsWhenTermChangesDuringActualElection) { @@ -784,7 +785,7 @@ TEST_F(ReplCoordTest, ElectionFailsWhenTermChangesDuringActualElection) { getReplCoord()->waitForElectionFinish_forTest(); stopCapturingLogMessages(); ASSERT_EQUALS(1, - countLogLinesContaining("not becoming primary, we have been superceded already")); + countLogLinesContaining("not becoming primary, we have been superseded already")); } class PriorityTakeoverTest : public ReplCoordTest { diff --git a/src/mongo/db/repl/replication_coordinator_impl_heartbeat.cpp b/src/mongo/db/repl/replication_coordinator_impl_heartbeat.cpp index b72d7e7dbcc..66c9799029d 100644 --- a/src/mongo/db/repl/replication_coordinator_impl_heartbeat.cpp +++ b/src/mongo/db/repl/replication_coordinator_impl_heartbeat.cpp @@ -778,18 +778,24 @@ void ReplicationCoordinatorImpl::_scheduleNextLivenessUpdate_inlock() { } auto nextTimeout = earliestDate + _rsConfig.getElectionTimeoutPeriod(); - if (nextTimeout > _replExecutor.now()) { - LOG(3) << "scheduling next check at " << nextTimeout; - auto cbh = _scheduleWorkAt(nextTimeout, - stdx::bind(&ReplicationCoordinatorImpl::_handleLivenessTimeout, - this, - stdx::placeholders::_1)); - if (!cbh) { - return; - } - _handleLivenessTimeoutCbh = cbh; - _earliestMemberId = earliestMemberId; + LOG(3) << "scheduling next check at " << nextTimeout; + + // It is possible we will schedule the next timeout in the past. + // ReplicationExecutor::_scheduleWorkAt() schedules its work immediately if it's given a + // time <= now(). + // If we missed the timeout, it means that on our last check the earliest live member was + // just barely fresh and it has become stale since then. We must schedule another liveness + // check to continue conducting liveness checks and be able to step down from primary if we + // lose contact with a majority of nodes. + auto cbh = _scheduleWorkAt(nextTimeout, + stdx::bind(&ReplicationCoordinatorImpl::_handleLivenessTimeout, + this, + stdx::placeholders::_1)); + if (!cbh) { + return; } + _handleLivenessTimeoutCbh = cbh; + _earliestMemberId = earliestMemberId; } void ReplicationCoordinatorImpl::_cancelAndRescheduleLivenessUpdate_inlock(int updatedMemberId) { diff --git a/src/mongo/db/repl/replication_executor_test.cpp b/src/mongo/db/repl/replication_executor_test.cpp index ba41df46769..2e8a66465de 100644 --- a/src/mongo/db/repl/replication_executor_test.cpp +++ b/src/mongo/db/repl/replication_executor_test.cpp @@ -249,6 +249,21 @@ TEST_F(ReplicationExecutorTest, ScheduleCallbackAtNow) { executor.waitForEvent(finishEvent); } +TEST_F(ReplicationExecutorTest, ScheduleCallbackInPast) { + launchExecutorThread(); + getNet()->exitNetwork(); + + ReplicationExecutor& executor = getReplExecutor(); + auto finishEvent = assertGet(executor.makeEvent()); + auto fn = [&executor, finishEvent](const ReplicationExecutor::CallbackArgs& cbData) { + ASSERT_OK(cbData.status); + executor.signalEvent(finishEvent); + }; + + auto cb = executor.scheduleWorkAt(getNet()->now() - Milliseconds(1000), fn); + executor.waitForEvent(finishEvent); +} + TEST_F(ReplicationExecutorTest, ScheduleCallbackAtAFutureTime) { launchExecutorThread(); getNet()->exitNetwork(); diff --git a/src/mongo/db/repl/sync_source_feedback.cpp b/src/mongo/db/repl/sync_source_feedback.cpp index 4d395e376e6..a82b1553212 100644 --- a/src/mongo/db/repl/sync_source_feedback.cpp +++ b/src/mongo/db/repl/sync_source_feedback.cpp @@ -50,27 +50,23 @@ namespace repl { namespace { /** - * Calculates the keep alive interval based on the current configuration in the replication - * coordinator. + * Calculates the keep alive interval based on the given ReplSetConfig. */ -Milliseconds calculateKeepAliveInterval(OperationContext* txn, stdx::mutex& mtx) { - stdx::lock_guard<stdx::mutex> lock(mtx); - auto replCoord = repl::ReplicationCoordinator::get(txn); - auto rsConfig = replCoord->getConfig(); - auto keepAliveInterval = rsConfig.getElectionTimeoutPeriod() / 2; - return keepAliveInterval; +Milliseconds calculateKeepAliveInterval(const ReplSetConfig& rsConfig) { + return rsConfig.getElectionTimeoutPeriod() / 2; } /** * Returns function to prepare update command */ Reporter::PrepareReplSetUpdatePositionCommandFn makePrepareReplSetUpdatePositionCommandFn( - OperationContext* txn, + ReplicationCoordinator* replCoord, stdx::mutex& mtx, const HostAndPort& syncTarget, BackgroundSync* bgsync) { - return [&mtx, syncTarget, txn, bgsync](ReplicationCoordinator::ReplSetUpdatePositionCommandStyle - commandStyle) -> StatusWith<BSONObj> { + return [&mtx, syncTarget, replCoord, bgsync]( + ReplicationCoordinator::ReplSetUpdatePositionCommandStyle + commandStyle) -> StatusWith<BSONObj> { auto currentSyncTarget = bgsync->getSyncTarget(); if (currentSyncTarget != syncTarget) { if (currentSyncTarget.empty()) { @@ -86,7 +82,6 @@ Reporter::PrepareReplSetUpdatePositionCommandFn makePrepareReplSetUpdatePosition } } - auto replCoord = repl::ReplicationCoordinator::get(txn); if (replCoord->getMemberState().primary()) { // Primary has no one to send updates to. return Status(ErrorCodes::InvalidSyncSource, @@ -114,7 +109,7 @@ void SyncSourceFeedback::forwardSlaveProgress() { } } -Status SyncSourceFeedback::_updateUpstream(OperationContext* txn, +Status SyncSourceFeedback::_updateUpstream(ReplicationCoordinator* replCoord, BackgroundSync* bgsync, Reporter* reporter) { auto syncTarget = reporter->getTarget(); @@ -139,7 +134,6 @@ Status SyncSourceFeedback::_updateUpstream(OperationContext* txn, } else { // Blacklist sync target for .5 seconds and find a new one. stdx::lock_guard<stdx::mutex> lock(_mtx); - auto replCoord = repl::ReplicationCoordinator::get(txn); const auto blacklistDuration = Milliseconds{500}; const auto until = Date_t::now() + blacklistDuration; log() << "Blacklisting " << syncTarget << " due to error: '" << status << "' for " @@ -161,7 +155,9 @@ void SyncSourceFeedback::shutdown() { _cond.notify_all(); } -void SyncSourceFeedback::run(executor::TaskExecutor* executor, BackgroundSync* bgsync) { +void SyncSourceFeedback::run(executor::TaskExecutor* executor, + BackgroundSync* bgsync, + ReplicationCoordinator* replCoord) { Client::initThread("SyncSourceFeedback"); HostAndPort syncTarget; @@ -170,10 +166,9 @@ void SyncSourceFeedback::run(executor::TaskExecutor* executor, BackgroundSync* b Milliseconds keepAliveInterval(0); while (true) { // breaks once _shutdownSignaled is true - auto txn = cc().makeOperationContext(); if (keepAliveInterval == Milliseconds(0)) { - keepAliveInterval = calculateKeepAliveInterval(txn.get(), _mtx); + keepAliveInterval = calculateKeepAliveInterval(replCoord->getConfig()); } { @@ -189,7 +184,7 @@ void SyncSourceFeedback::run(executor::TaskExecutor* executor, BackgroundSync* b continue; } } - MemberState state = ReplicationCoordinator::get(txn.get())->getMemberState(); + MemberState state = replCoord->getMemberState(); if (!(state.primary() || state.startup())) { break; } @@ -204,7 +199,7 @@ void SyncSourceFeedback::run(executor::TaskExecutor* executor, BackgroundSync* b { stdx::lock_guard<stdx::mutex> lock(_mtx); - MemberState state = ReplicationCoordinator::get(txn.get())->getMemberState(); + MemberState state = replCoord->getMemberState(); if (state.primary() || state.startup()) { continue; } @@ -226,7 +221,7 @@ void SyncSourceFeedback::run(executor::TaskExecutor* executor, BackgroundSync* b // Update keepalive value from config. auto oldKeepAliveInterval = keepAliveInterval; - keepAliveInterval = calculateKeepAliveInterval(txn.get(), _mtx); + keepAliveInterval = calculateKeepAliveInterval(replCoord->getConfig()); if (oldKeepAliveInterval != keepAliveInterval) { LOG(1) << "new syncSourceFeedback keep alive duration = " << keepAliveInterval << " (previously " << oldKeepAliveInterval << ")"; @@ -235,7 +230,7 @@ void SyncSourceFeedback::run(executor::TaskExecutor* executor, BackgroundSync* b Reporter reporter( executor, - makePrepareReplSetUpdatePositionCommandFn(txn.get(), _mtx, syncTarget, bgsync), + makePrepareReplSetUpdatePositionCommandFn(replCoord, _mtx, syncTarget, bgsync), syncTarget, keepAliveInterval); { @@ -250,7 +245,7 @@ void SyncSourceFeedback::run(executor::TaskExecutor* executor, BackgroundSync* b _reporter = nullptr; }); - auto status = _updateUpstream(txn.get(), bgsync, &reporter); + auto status = _updateUpstream(replCoord, bgsync, &reporter); if (!status.isOK()) { LOG(1) << "The replication progress command (replSetUpdatePosition) failed and will be " "retried: " diff --git a/src/mongo/db/repl/sync_source_feedback.h b/src/mongo/db/repl/sync_source_feedback.h index 40c29dc172a..35ed470fc26 100644 --- a/src/mongo/db/repl/sync_source_feedback.h +++ b/src/mongo/db/repl/sync_source_feedback.h @@ -31,6 +31,7 @@ #include "mongo/base/disallow_copying.h" #include "mongo/base/status.h" +#include "mongo/db/repl/replication_coordinator.h" #include "mongo/stdx/condition_variable.h" #include "mongo/stdx/mutex.h" @@ -63,7 +64,9 @@ public: * * Task executor is used to run replSetUpdatePosition command on sync source. */ - void run(executor::TaskExecutor* executor, BackgroundSync* bgsync); + void run(executor::TaskExecutor* executor, + BackgroundSync* bgsync, + ReplicationCoordinator* replCoord); /// Signals the run() method to terminate. void shutdown(); @@ -72,7 +75,9 @@ private: /* Inform the sync target of our current position in the oplog, as well as the positions * of all secondaries chained through us. */ - Status _updateUpstream(OperationContext* txn, BackgroundSync* bgsync, Reporter* reporter); + Status _updateUpstream(ReplicationCoordinator* replCoord, + BackgroundSync* bgsync, + Reporter* reporter); // protects cond, _shutdownSignaled, _keepAliveInterval, and _positionChanged. stdx::mutex _mtx; diff --git a/src/mongo/db/repl/sync_source_resolver.cpp b/src/mongo/db/repl/sync_source_resolver.cpp index 948a471b245..17a706f1b61 100644 --- a/src/mongo/db/repl/sync_source_resolver.cpp +++ b/src/mongo/db/repl/sync_source_resolver.cpp @@ -54,6 +54,7 @@ const Seconds SyncSourceResolver::kFirstOplogEntryEmptyBlacklistDuration(10); const Seconds SyncSourceResolver::kFirstOplogEntryNullTimestampBlacklistDuration(10); const Minutes SyncSourceResolver::kTooStaleBlacklistDuration(1); const Seconds SyncSourceResolver::kNoRequiredOpTimeBlacklistDuration(60); +const int SyncSourceResolver::kUninitializedRollbackId(-1); SyncSourceResolver::SyncSourceResolver(executor::TaskExecutor* taskExecutor, SyncSourceSelector* syncSourceSelector, @@ -175,11 +176,13 @@ std::unique_ptr<Fetcher> SyncSourceResolver::_makeFirstOplogEntryFetcher( candidate, earliestOpTimeSeen), rpc::ServerSelectionMetadata(true, boost::none).toBSON(), - kFetcherTimeout); + kFetcherTimeout /* find network timeout */, + kFetcherTimeout /* getMore network timeout */); } std::unique_ptr<Fetcher> SyncSourceResolver::_makeRequiredOpTimeFetcher(HostAndPort candidate, - OpTime earliestOpTimeSeen) { + OpTime earliestOpTimeSeen, + int rbid) { // This query is structured so that it is executed on the sync source using the oplog // start hack (oplogReplay=true and $gt/$gte predicate over "ts"). return stdx::make_unique<Fetcher>( @@ -193,9 +196,11 @@ std::unique_ptr<Fetcher> SyncSourceResolver::_makeRequiredOpTimeFetcher(HostAndP this, stdx::placeholders::_1, candidate, - earliestOpTimeSeen), + earliestOpTimeSeen, + rbid), rpc::ServerSelectionMetadata(true, boost::none).toBSON(), - kFetcherTimeout); + kFetcherTimeout /* find network timeout */, + kFetcherTimeout /* getMore network timeout */); } Status SyncSourceResolver::_scheduleFetcher(std::unique_ptr<Fetcher> fetcher) { @@ -205,6 +210,9 @@ Status SyncSourceResolver::_scheduleFetcher(std::unique_ptr<Fetcher> fetcher) { // executor. auto status = fetcher->schedule(); if (status.isOK()) { + // Fetcher destruction blocks on all outstanding callbacks. If we are currently in a + // Fetcher-related callback, we can't destroy the Fetcher just yet, so we assign it to a + // temporary unique pointer to allow the destruction to run to completion. _shuttingDownFetcher = std::move(_fetcher); _fetcher = std::move(fetcher); } else { @@ -311,10 +319,26 @@ void SyncSourceResolver::_firstOplogEntryFetcherCallback( return; } - _scheduleRBIDRequest(candidate, earliestOpTimeSeen); + auto status = _scheduleRBIDRequest(candidate, earliestOpTimeSeen); + if (!status.isOK()) { + _finishCallback(status); + } } -void SyncSourceResolver::_scheduleRBIDRequest(HostAndPort candidate, OpTime earliestOpTimeSeen) { +Status SyncSourceResolver::_scheduleRBIDRequest(HostAndPort candidate, OpTime earliestOpTimeSeen) { + // Once a work is scheduled, nothing prevents it finishing. We need the mutex to protect the + // access of member variables after scheduling, because otherwise the scheduled callback could + // finish and allow the destructor to fire before we access the member variables. + stdx::lock_guard<stdx::mutex> lk(_mutex); + if (_state == State::kShuttingDown) { + return Status( + ErrorCodes::CallbackCanceled, + str::stream() + << "sync source resolver shut down while checking rollbackId on candidate: " + << candidate); + } + + invariant(_state == State::kRunning); auto handle = _taskExecutor->scheduleRemoteCommand( {candidate, "admin", BSON("replSetGetRBID" << 1), nullptr, kFetcherTimeout}, stdx::bind(&SyncSourceResolver::_rbidRequestCallback, @@ -324,15 +348,11 @@ void SyncSourceResolver::_scheduleRBIDRequest(HostAndPort candidate, OpTime earl stdx::placeholders::_1)); if (!handle.isOK()) { - _finishCallback(handle.getStatus()); - return; + return handle.getStatus(); } - stdx::lock_guard<stdx::mutex> lk(_mutex); _rbidCommandHandle = std::move(handle.getValue()); - if (_state == State::kShuttingDown) { - _taskExecutor->cancel(_rbidCommandHandle); - } + return Status::OK(); } void SyncSourceResolver::_rbidRequestCallback( @@ -344,10 +364,11 @@ void SyncSourceResolver::_rbidRequestCallback( return; } + int rbid = kUninitializedRollbackId; try { uassertStatusOK(rbidReply.response.status); uassertStatusOK(getStatusFromCommandResult(rbidReply.response.data)); - _rbid = rbidReply.response.data["rbid"].Int(); + rbid = rbidReply.response.data["rbid"].Int(); } catch (const DBException& ex) { const auto until = _taskExecutor->now() + kFetcherErrorBlacklistDuration; log() << "Blacklisting " << candidate << " due to error: '" << ex << "' for " @@ -360,13 +381,15 @@ void SyncSourceResolver::_rbidRequestCallback( if (!_requiredOpTime.isNull()) { // Schedule fetcher to look for '_requiredOpTime' in the remote oplog. // Unittest requires that this kind of failure be handled specially. - auto status = _scheduleFetcher(_makeRequiredOpTimeFetcher(candidate, earliestOpTimeSeen)); + auto status = + _scheduleFetcher(_makeRequiredOpTimeFetcher(candidate, earliestOpTimeSeen, rbid)); if (!status.isOK()) { _finishCallback(status); } return; } - _finishCallback(candidate); + + _finishCallback(candidate, rbid); } Status SyncSourceResolver::_compareRequiredOpTimeWithQueryResponse( @@ -399,7 +422,8 @@ Status SyncSourceResolver::_compareRequiredOpTimeWithQueryResponse( void SyncSourceResolver::_requiredOpTimeFetcherCallback( const StatusWith<Fetcher::QueryResponse>& queryResult, HostAndPort candidate, - OpTime earliestOpTimeSeen) { + OpTime earliestOpTimeSeen, + int rbid) { if (_isShuttingDown()) { _finishCallback(Status(ErrorCodes::CallbackCanceled, str::stream() << "sync source resolver shut down while looking for " @@ -444,18 +468,18 @@ void SyncSourceResolver::_requiredOpTimeFetcherCallback( return; } - _finishCallback(candidate); + _finishCallback(candidate, rbid); } Status SyncSourceResolver::_chooseAndProbeNextSyncSource(OpTime earliestOpTimeSeen) { auto candidateResult = _chooseNewSyncSource(); if (!candidateResult.isOK()) { - return _finishCallback(candidateResult); + return _finishCallback(candidateResult.getStatus()); } if (candidateResult.getValue().empty()) { if (earliestOpTimeSeen.isNull()) { - return _finishCallback(candidateResult); + return _finishCallback(candidateResult.getValue(), kUninitializedRollbackId); } SyncSourceResolverResponse response; @@ -473,16 +497,22 @@ Status SyncSourceResolver::_chooseAndProbeNextSyncSource(OpTime earliestOpTimeSe return Status::OK(); } -Status SyncSourceResolver::_finishCallback(StatusWith<HostAndPort> result) { +Status SyncSourceResolver::_finishCallback(HostAndPort hostAndPort, int rbid) { SyncSourceResolverResponse response; - response.syncSourceStatus = std::move(result); - if (response.isOK() && !response.getSyncSource().empty()) { - invariant(_requiredOpTime.isNull() || _rbid); - response.rbid = _rbid; + response.syncSourceStatus = std::move(hostAndPort); + if (rbid != kUninitializedRollbackId) { + response.rbid = rbid; } return _finishCallback(response); } +Status SyncSourceResolver::_finishCallback(Status status) { + invariant(!status.isOK()); + SyncSourceResolverResponse response; + response.syncSourceStatus = std::move(status); + return _finishCallback(response); +} + Status SyncSourceResolver::_finishCallback(const SyncSourceResolverResponse& response) { try { _onCompletion(response); diff --git a/src/mongo/db/repl/sync_source_resolver.h b/src/mongo/db/repl/sync_source_resolver.h index 201658caacf..0f9343cfcdb 100644 --- a/src/mongo/db/repl/sync_source_resolver.h +++ b/src/mongo/db/repl/sync_source_resolver.h @@ -102,6 +102,7 @@ public: static const Seconds kFirstOplogEntryNullTimestampBlacklistDuration; static const Minutes kTooStaleBlacklistDuration; static const Seconds kNoRequiredOpTimeBlacklistDuration; + static const int kUninitializedRollbackId; /** * Callback function to report final status of resolving sync source. @@ -154,7 +155,8 @@ private: * Creates fetcher to check the remote oplog for '_requiredOpTime'. */ std::unique_ptr<Fetcher> _makeRequiredOpTimeFetcher(HostAndPort candidate, - OpTime earliestOpTimeSeen); + OpTime earliestOpTimeSeen, + int rbid); /** * Schedules fetcher to read oplog on sync source. @@ -179,7 +181,7 @@ private: /** * Schedules a replSetGetRBID command against the candidate to fetch its current rollback id. */ - void _scheduleRBIDRequest(HostAndPort candidate, OpTime earliestOpTimeSeen); + Status _scheduleRBIDRequest(HostAndPort candidate, OpTime earliestOpTimeSeen); void _rbidRequestCallback(HostAndPort candidate, OpTime earliestOpTimeSeen, const executor::TaskExecutor::RemoteCommandCallbackArgs& rbidReply); @@ -194,7 +196,8 @@ private: */ void _requiredOpTimeFetcherCallback(const StatusWith<Fetcher::QueryResponse>& queryResult, HostAndPort candidate, - OpTime earliestOpTimeSeen); + OpTime earliestOpTimeSeen, + int rbid); /** * Obtains new sync source candidate and schedules remote command to fetcher first oplog entry. @@ -207,7 +210,8 @@ private: * Invokes completion callback and transitions state to State::kComplete. * Returns result.getStatus(). */ - Status _finishCallback(StatusWith<HostAndPort> result); + Status _finishCallback(HostAndPort hostAndPort, int rbid); + Status _finishCallback(Status status); Status _finishCallback(const SyncSourceResolverResponse& response); // Executor used to send remote commands to sync source candidates. @@ -229,9 +233,6 @@ private: // resolver via this callback in a SyncSourceResolverResponse struct when the resolver finishes. const OnCompletionFn _onCompletion; - // The rbid we will return to our caller. - int _rbid; - // Protects members of this sync source resolver defined below. mutable stdx::mutex _mutex; mutable stdx::condition_variable _condition; diff --git a/src/mongo/db/repl/sync_tail.cpp b/src/mongo/db/repl/sync_tail.cpp index 30f1e28b619..3f65e3fc6b7 100644 --- a/src/mongo/db/repl/sync_tail.cpp +++ b/src/mongo/db/repl/sync_tail.cpp @@ -448,7 +448,6 @@ void applyOps(std::vector<MultiApplier::OperationPtrs>& writerVectors, const MultiApplier::ApplyOperationFn& func, std::vector<Status>* statusVector) { invariant(writerVectors.size() == statusVector->size()); - TimerHolder timer(&applyBatchStats); for (size_t i = 0; i < writerVectors.size(); i++) { if (!writerVectors[i].empty()) { writerPool->schedule([&func, &writerVectors, statusVector, i] { @@ -695,32 +694,53 @@ public: } private: + /** + * Calculates batch limit size (in bytes) using the maximum capped collection size of the oplog + * size. + * Batches are limited to 10% of the oplog. + */ + std::size_t _calculateBatchLimitBytes() { + auto opCtx = cc().makeOperationContext(); + auto storageInterface = StorageInterface::get(opCtx.get()); + auto oplogMaxSizeResult = + storageInterface->getOplogMaxSize(opCtx.get(), NamespaceString(rsOplogName)); + auto oplogMaxSize = fassertStatusOK(40301, oplogMaxSizeResult); + return std::min(oplogMaxSize / 10, std::size_t(replBatchLimitBytes)); + } + + /** + * If slaveDelay is enabled, this function calculates the most recent timestamp of any oplog + * entries that can be be returned in a batch. + */ + boost::optional<Date_t> _calculateSlaveDelayLatestTimestamp() { + auto service = cc().getServiceContext(); + auto replCoord = ReplicationCoordinator::get(service); + auto slaveDelay = replCoord->getSlaveDelaySecs(); + if (slaveDelay <= Seconds(0)) { + return {}; + } + auto fastClockSource = service->getFastClockSource(); + return fastClockSource->now() - slaveDelay; + } + void run() { Client::initThread("ReplBatcher"); - const ServiceContext::UniqueOperationContext txnPtr = cc().makeOperationContext(); - OperationContext& txn = *txnPtr; - const auto replCoord = ReplicationCoordinator::get(&txn); - const auto fastClockSource = txn.getServiceContext()->getFastClockSource(); - const auto oplogMaxSize = fassertStatusOK( - 40301, - StorageInterface::get(&txn)->getOplogMaxSize(&txn, NamespaceString(rsOplogName))); - // Batches are limited to 10% of the oplog. BatchLimits batchLimits; - batchLimits.bytes = std::min(oplogMaxSize / 10, size_t(replBatchLimitBytes)); + batchLimits.bytes = _calculateBatchLimitBytes(); while (true) { - const auto slaveDelay = replCoord->getSlaveDelaySecs(); - batchLimits.slaveDelayLatestTimestamp = (slaveDelay > Seconds(0)) - ? (fastClockSource->now() - slaveDelay) - : boost::optional<Date_t>(); + batchLimits.slaveDelayLatestTimestamp = _calculateSlaveDelayLatestTimestamp(); // Check this once per batch since users can change it at runtime. batchLimits.ops = replBatchLimitOperations.load(); OpQueue ops; // tryPopAndWaitForMore adds to ops and returns true when we need to end a batch early. - while (!_syncTail->tryPopAndWaitForMore(&txn, &ops, batchLimits)) { + { + auto opCtx = cc().makeOperationContext(); + while (!_syncTail->tryPopAndWaitForMore(opCtx.get(), &ops, batchLimits)) { + } } if (ops.empty() && !ops.mustShutdown()) { @@ -755,14 +775,15 @@ private: void SyncTail::oplogApplication(ReplicationCoordinator* replCoord) { OpQueueBatcher batcher(this); - const ServiceContext::UniqueOperationContext txnPtr = cc().makeOperationContext(); - OperationContext& txn = *txnPtr; std::unique_ptr<ApplyBatchFinalizer> finalizer{ getGlobalServiceContext()->getGlobalStorageEngine()->isDurable() ? new ApplyBatchFinalizerForJournal(replCoord) : new ApplyBatchFinalizer(replCoord)}; while (true) { // Exits on message from OpQueueBatcher. + const ServiceContext::UniqueOperationContext txnPtr = cc().makeOperationContext(); + OperationContext& txn = *txnPtr; + // For pausing replication in tests. while (MONGO_FAIL_POINT(rsSyncApplyStop)) { // Tests should not trigger clean shutdown while that failpoint is active. If we @@ -1301,6 +1322,9 @@ StatusWith<OpTime> multiApply(OperationContext* txn, std::vector<Status> statusVector(workerPool->getNumThreads(), Status::OK()); { + // Each node records cumulative batch application stats for itself using this timer. + TimerHolder timer(&applyBatchStats); + // We must wait for the all work we've dispatched to complete before leaving this block // because the spawned threads refer to objects on our stack, including writerVectors. std::vector<MultiApplier::OperationPtrs> writerVectors(workerPool->getNumThreads()); diff --git a/src/mongo/db/repl/topology_coordinator_impl.cpp b/src/mongo/db/repl/topology_coordinator_impl.cpp index 88337b6a500..f5302008206 100644 --- a/src/mongo/db/repl/topology_coordinator_impl.cpp +++ b/src/mongo/db/repl/topology_coordinator_impl.cpp @@ -36,6 +36,7 @@ #include "mongo/db/audit.h" #include "mongo/db/client.h" +#include "mongo/db/mongod_options.h" #include "mongo/db/operation_context.h" #include "mongo/db/repl/heartbeat_response_action.h" #include "mongo/db/repl/is_master_response.h" @@ -1411,7 +1412,7 @@ bool TopologyCoordinatorImpl::_aMajoritySeemsToBeUp() const { return vUp * 2 > _rsConfig.getTotalVotingMembers(); } -bool TopologyCoordinatorImpl::_canSeeHealthyPrimaryOfEqualOrGreaterPriority( +int TopologyCoordinatorImpl::_findHealthyPrimaryOfEqualOrGreaterPriority( const int candidateIndex) const { const double candidatePriority = _rsConfig.getMemberAt(candidateIndex).getPriority(); for (auto it = _hbdata.begin(); it != _hbdata.end(); ++it) { @@ -1421,11 +1422,11 @@ bool TopologyCoordinatorImpl::_canSeeHealthyPrimaryOfEqualOrGreaterPriority( const int itIndex = indexOfIterator(_hbdata, it); const double priority = _rsConfig.getMemberAt(itIndex).getPriority(); if (itIndex != candidateIndex && priority >= candidatePriority) { - return true; + return itIndex; } } - return false; + return -1; } bool TopologyCoordinatorImpl::_isOpTimeCloseEnoughToLatestToElect( @@ -2232,7 +2233,7 @@ MemberState TopologyCoordinatorImpl::getMemberState() const { } if (_rsConfig.isConfigServer()) { - if (_options.clusterRole != ClusterRole::ConfigServer) { + if (_options.clusterRole != ClusterRole::ConfigServer && !skipShardingConfigurationChecks) { return MemberState::RS_REMOVED; } else { invariant(_storageEngineSupportsReadCommitted != ReadCommittedSupport::kUnknown); @@ -2241,7 +2242,7 @@ MemberState TopologyCoordinatorImpl::getMemberState() const { } } } else { - if (_options.clusterRole == ClusterRole::ConfigServer) { + if (_options.clusterRole == ClusterRole::ConfigServer && !skipShardingConfigurationChecks) { return MemberState::RS_REMOVED; } } @@ -2571,29 +2572,54 @@ void TopologyCoordinatorImpl::processReplSetRequestVotes(const ReplSetRequestVot if (args.getTerm() < _term) { response->setVoteGranted(false); - response->setReason("candidate's term is lower than mine"); + response->setReason(str::stream() << "candidate's term (" << args.getTerm() + << ") is lower than mine (" + << _term + << ")"); } else if (args.getConfigVersion() != _rsConfig.getConfigVersion()) { response->setVoteGranted(false); - response->setReason("candidate's config version differs from mine"); + response->setReason(str::stream() << "candidate's config version (" + << args.getConfigVersion() + << ") differs from mine (" + << _rsConfig.getConfigVersion() + << ")"); } else if (args.getSetName() != _rsConfig.getReplSetName()) { response->setVoteGranted(false); - response->setReason("candidate's set name differs from mine"); + response->setReason(str::stream() << "candidate's set name (" << args.getSetName() + << ") differs from mine (" + << _rsConfig.getReplSetName() + << ")"); } else if (args.getLastDurableOpTime() < lastAppliedOpTime) { response->setVoteGranted(false); - response->setReason("candidate's data is staler than mine"); + response + ->setReason(str::stream() + << "candidate's data is staler than mine. candidate's last applied OpTime: " + << args.getLastDurableOpTime().toString() + << ", my last applied OpTime: " + << lastAppliedOpTime.toString()); } else if (!args.isADryRun() && _lastVote.getTerm() == args.getTerm()) { response->setVoteGranted(false); - response->setReason("already voted for another candidate this term"); - } else if (_selfConfig().isArbiter() && - _canSeeHealthyPrimaryOfEqualOrGreaterPriority(args.getCandidateIndex())) { - response->setVoteGranted(false); - response->setReason("can see a healthy primary of equal or greater priority"); + response->setReason(str::stream() + << "already voted for another candidate (" + << _rsConfig.getMemberAt(_lastVote.getCandidateIndex()).getHostAndPort() + << ") this term (" + << _lastVote.getTerm() + << ")"); } else { - if (!args.isADryRun()) { - _lastVote.setTerm(args.getTerm()); - _lastVote.setCandidateIndex(args.getCandidateIndex()); + int betterPrimary = _findHealthyPrimaryOfEqualOrGreaterPriority(args.getCandidateIndex()); + if (_selfConfig().isArbiter() && betterPrimary >= 0) { + response->setVoteGranted(false); + response->setReason(str::stream() + << "can see a healthy primary (" + << _rsConfig.getMemberAt(betterPrimary).getHostAndPort() + << ") of equal or greater priority"); + } else { + if (!args.isADryRun()) { + _lastVote.setTerm(args.getTerm()); + _lastVote.setCandidateIndex(args.getCandidateIndex()); + } + response->setVoteGranted(true); } - response->setVoteGranted(true); } } diff --git a/src/mongo/db/repl/topology_coordinator_impl.h b/src/mongo/db/repl/topology_coordinator_impl.h index d4b0476cde1..1a03338e3c4 100644 --- a/src/mongo/db/repl/topology_coordinator_impl.h +++ b/src/mongo/db/repl/topology_coordinator_impl.h @@ -310,9 +310,9 @@ private: // Sees if a majority number of votes are held by members who are currently "up" bool _aMajoritySeemsToBeUp() const; - // Returns true if the node can see a healthy primary of equal or greater priority to the - // candidate. - bool _canSeeHealthyPrimaryOfEqualOrGreaterPriority(const int candidateIndex) const; + // Checks if the node can see a healthy primary of equal or greater priority to the + // candidate. If so, returns the index of that node. Otherwise returns -1. + int _findHealthyPrimaryOfEqualOrGreaterPriority(const int candidateIndex) const; // Is otherOpTime close enough (within 10 seconds) to the latest known optime to qualify // for an election diff --git a/src/mongo/db/repl/topology_coordinator_impl_test.cpp b/src/mongo/db/repl/topology_coordinator_impl_test.cpp index e3708a9bf54..1f8d4b56c40 100644 --- a/src/mongo/db/repl/topology_coordinator_impl_test.cpp +++ b/src/mongo/db/repl/topology_coordinator_impl_test.cpp @@ -5912,7 +5912,8 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantVotesToTwoDifferentNodesInTheSameTerm) { // different candidate same term, should be a problem getTopoCoord().processReplSetRequestVotes(args2, &response2, lastAppliedOpTime); - ASSERT_EQUALS("already voted for another candidate this term", response2.getReason()); + ASSERT_EQUALS("already voted for another candidate (hself:27017) this term (1)", + response2.getReason()); ASSERT_FALSE(response2.getVoteGranted()); } @@ -6028,7 +6029,8 @@ TEST_F(TopoCoordTest, VoteRequestShouldNotPreventDryRunsForThatTerm) { ReplSetRequestVotesResponse response2; getTopoCoord().processReplSetRequestVotes(args2, &response2, lastAppliedOpTime); - ASSERT_EQUALS("already voted for another candidate this term", response2.getReason()); + ASSERT_EQUALS("already voted for another candidate (hself:27017) this term (1)", + response2.getReason()); ASSERT_FALSE(response2.getVoteGranted()); } @@ -6063,7 +6065,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantVoteWhenReplSetNameDoesNotMatch) { OpTime lastAppliedOpTime; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's set name differs from mine", response.getReason()); + ASSERT_EQUALS("candidate's set name (wrongName) differs from mine (rs0)", response.getReason()); ASSERT_FALSE(response.getVoteGranted()); } @@ -6098,7 +6100,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantVoteWhenConfigVersionDoesNotMatch) { OpTime lastAppliedOpTime; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's config version differs from mine", response.getReason()); + ASSERT_EQUALS("candidate's config version (0) differs from mine (1)", response.getReason()); ASSERT_FALSE(response.getVoteGranted()); } @@ -6145,7 +6147,8 @@ TEST_F(TopoCoordTest, ArbiterDoesNotGrantVoteWhenItCanSeeAHealthyPrimaryOfEqualO OpTime lastAppliedOpTime; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("can see a healthy primary of equal or greater priority", response.getReason()); + ASSERT_EQUALS("can see a healthy primary (h2:27017) of equal or greater priority", + response.getReason()); ASSERT_FALSE(response.getVoteGranted()); } @@ -6184,7 +6187,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantVoteWhenTermIsStale) { OpTime lastAppliedOpTime; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's term is lower than mine", response.getReason()); + ASSERT_EQUALS("candidate's term (1) is lower than mine (2)", response.getReason()); ASSERT_EQUALS(2, response.getTerm()); ASSERT_FALSE(response.getVoteGranted()); } @@ -6221,7 +6224,12 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantVoteWhenOpTimeIsStale) { OpTime lastAppliedOpTime2 = {Timestamp(20, 0), 0}; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime2); - ASSERT_EQUALS("candidate's data is staler than mine", response.getReason()); + ASSERT_EQUALS( + str::stream() << "candidate's data is staler than mine. candidate's last applied OpTime: " + << OpTime().toString() + << ", my last applied OpTime: " + << OpTime(Timestamp(20, 0), 0).toString(), + response.getReason()); ASSERT_FALSE(response.getVoteGranted()); } @@ -6281,7 +6289,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantDryRunVoteWhenReplSetNameDoesNotMatch) { ReplSetRequestVotesResponse response; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's set name differs from mine", response.getReason()); + ASSERT_EQUALS("candidate's set name (wrongName) differs from mine (rs0)", response.getReason()); ASSERT_EQUALS(1, response.getTerm()); ASSERT_FALSE(response.getVoteGranted()); } @@ -6342,7 +6350,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantDryRunVoteWhenConfigVersionDoesNotMatch) { ReplSetRequestVotesResponse response; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's config version differs from mine", response.getReason()); + ASSERT_EQUALS("candidate's config version (0) differs from mine (1)", response.getReason()); ASSERT_EQUALS(1, response.getTerm()); ASSERT_FALSE(response.getVoteGranted()); } @@ -6402,7 +6410,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantDryRunVoteWhenTermIsStale) { ReplSetRequestVotesResponse response; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's term is lower than mine", response.getReason()); + ASSERT_EQUALS("candidate's term (0) is lower than mine (1)", response.getReason()); ASSERT_EQUALS(1, response.getTerm()); ASSERT_FALSE(response.getVoteGranted()); } @@ -6525,7 +6533,12 @@ TEST_F(TopoCoordTest, DoNotGrantDryRunVoteWhenOpTimeIsStale) { OpTime lastAppliedOpTime2 = {Timestamp(20, 0), 0}; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime2); - ASSERT_EQUALS("candidate's data is staler than mine", response.getReason()); + ASSERT_EQUALS( + str::stream() << "candidate's data is staler than mine. candidate's last applied OpTime: " + << OpTime().toString() + << ", my last applied OpTime: " + << OpTime(Timestamp(20, 0), 0).toString(), + response.getReason()); ASSERT_EQUALS(1, response.getTerm()); ASSERT_FALSE(response.getVoteGranted()); } diff --git a/src/mongo/db/repl/topology_coordinator_impl_v1_test.cpp b/src/mongo/db/repl/topology_coordinator_impl_v1_test.cpp index 8062208949b..8f60269cd71 100644 --- a/src/mongo/db/repl/topology_coordinator_impl_v1_test.cpp +++ b/src/mongo/db/repl/topology_coordinator_impl_v1_test.cpp @@ -2574,7 +2574,8 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantVotesToTwoDifferentNodesInTheSameTerm) { // different candidate same term, should be a problem getTopoCoord().processReplSetRequestVotes(args2, &response2, lastAppliedOpTime); - ASSERT_EQUALS("already voted for another candidate this term", response2.getReason()); + ASSERT_EQUALS("already voted for another candidate (hself:27017) this term (1)", + response2.getReason()); ASSERT_FALSE(response2.getVoteGranted()); } @@ -2674,7 +2675,8 @@ TEST_F(TopoCoordTest, DryRunVoteRequestShouldNotPreventSubsequentDryRunsForThatT ReplSetRequestVotesResponse response4; getTopoCoord().processReplSetRequestVotes(args4, &response4, lastAppliedOpTime); - ASSERT_EQUALS("already voted for another candidate this term", response4.getReason()); + ASSERT_EQUALS("already voted for another candidate (hself:27017) this term (1)", + response4.getReason()); ASSERT_FALSE(response4.getVoteGranted()); } @@ -2732,7 +2734,8 @@ TEST_F(TopoCoordTest, VoteRequestShouldNotPreventDryRunsForThatTerm) { ReplSetRequestVotesResponse response2; getTopoCoord().processReplSetRequestVotes(args2, &response2, lastAppliedOpTime); - ASSERT_EQUALS("already voted for another candidate this term", response2.getReason()); + ASSERT_EQUALS("already voted for another candidate (hself:27017) this term (1)", + response2.getReason()); ASSERT_FALSE(response2.getVoteGranted()); } @@ -2767,7 +2770,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantVoteWhenReplSetNameDoesNotMatch) { OpTime lastAppliedOpTime; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's set name differs from mine", response.getReason()); + ASSERT_EQUALS("candidate's set name (wrongName) differs from mine (rs0)", response.getReason()); ASSERT_FALSE(response.getVoteGranted()); } @@ -2802,7 +2805,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantVoteWhenConfigVersionDoesNotMatch) { OpTime lastAppliedOpTime; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's config version differs from mine", response.getReason()); + ASSERT_EQUALS("candidate's config version (0) differs from mine (1)", response.getReason()); ASSERT_FALSE(response.getVoteGranted()); } @@ -2841,7 +2844,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantVoteWhenTermIsStale) { OpTime lastAppliedOpTime; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's term is lower than mine", response.getReason()); + ASSERT_EQUALS("candidate's term (1) is lower than mine (2)", response.getReason()); ASSERT_EQUALS(2, response.getTerm()); ASSERT_FALSE(response.getVoteGranted()); } @@ -2878,7 +2881,12 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantVoteWhenOpTimeIsStale) { OpTime lastAppliedOpTime2 = {Timestamp(20, 0), 0}; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime2); - ASSERT_EQUALS("candidate's data is staler than mine", response.getReason()); + ASSERT_EQUALS( + str::stream() << "candidate's data is staler than mine. candidate's last applied OpTime: " + << OpTime().toString() + << ", my last applied OpTime: " + << OpTime(Timestamp(20, 0), 0).toString(), + response.getReason()); ASSERT_FALSE(response.getVoteGranted()); } @@ -2938,7 +2946,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantDryRunVoteWhenReplSetNameDoesNotMatch) { ReplSetRequestVotesResponse response; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's set name differs from mine", response.getReason()); + ASSERT_EQUALS("candidate's set name (wrongName) differs from mine (rs0)", response.getReason()); ASSERT_EQUALS(1, response.getTerm()); ASSERT_FALSE(response.getVoteGranted()); } @@ -2999,7 +3007,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantDryRunVoteWhenConfigVersionDoesNotMatch) { ReplSetRequestVotesResponse response; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's config version differs from mine", response.getReason()); + ASSERT_EQUALS("candidate's config version (0) differs from mine (1)", response.getReason()); ASSERT_EQUALS(1, response.getTerm()); ASSERT_FALSE(response.getVoteGranted()); } @@ -3059,7 +3067,7 @@ TEST_F(TopoCoordTest, NodeDoesNotGrantDryRunVoteWhenTermIsStale) { ReplSetRequestVotesResponse response; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime); - ASSERT_EQUALS("candidate's term is lower than mine", response.getReason()); + ASSERT_EQUALS("candidate's term (0) is lower than mine (1)", response.getReason()); ASSERT_EQUALS(1, response.getTerm()); ASSERT_FALSE(response.getVoteGranted()); } @@ -3182,7 +3190,12 @@ TEST_F(TopoCoordTest, DoNotGrantDryRunVoteWhenOpTimeIsStale) { OpTime lastAppliedOpTime2 = {Timestamp(20, 0), 0}; getTopoCoord().processReplSetRequestVotes(args, &response, lastAppliedOpTime2); - ASSERT_EQUALS("candidate's data is staler than mine", response.getReason()); + ASSERT_EQUALS( + str::stream() << "candidate's data is staler than mine. candidate's last applied OpTime: " + << OpTime().toString() + << ", my last applied OpTime: " + << OpTime(Timestamp(20, 0), 0).toString(), + response.getReason()); ASSERT_EQUALS(1, response.getTerm()); ASSERT_FALSE(response.getVoteGranted()); } diff --git a/src/mongo/db/s/SConscript b/src/mongo/db/s/SConscript index b2abb86c056..b4c8b12a3d2 100644 --- a/src/mongo/db/s/SConscript +++ b/src/mongo/db/s/SConscript @@ -47,7 +47,6 @@ env.Library( source=[ 'active_migrations_registry.cpp', 'chunk_move_write_concern_options.cpp', - 'collection_range_deleter.cpp', 'collection_sharding_state.cpp', 'metadata_manager.cpp', 'migration_chunk_cloner_source.cpp', diff --git a/src/mongo/db/s/balancer/balancer.cpp b/src/mongo/db/s/balancer/balancer.cpp index 36987fb81ec..7e334b8b86e 100644 --- a/src/mongo/db/s/balancer/balancer.cpp +++ b/src/mongo/db/s/balancer/balancer.cpp @@ -243,8 +243,9 @@ void Balancer::waitForBalancerToStop() { void Balancer::joinCurrentRound(OperationContext* opCtx) { stdx::unique_lock<stdx::mutex> scopedLock(_mutex); const auto numRoundsAtStart = _numBalancerRounds; - _condVar.wait(scopedLock, - [&] { return !_inBalancerRound || _numBalancerRounds != numRoundsAtStart; }); + opCtx->waitForConditionOrInterrupt(_condVar, scopedLock, [&] { + return !_inBalancerRound || _numBalancerRounds != numRoundsAtStart; + }); } Status Balancer::rebalanceSingleChunk(OperationContext* opCtx, const ChunkType& chunk) { diff --git a/src/mongo/db/s/balancer/balancer.h b/src/mongo/db/s/balancer/balancer.h index d8847a4ee90..7f601a849e2 100644 --- a/src/mongo/db/s/balancer/balancer.h +++ b/src/mongo/db/s/balancer/balancer.h @@ -110,7 +110,8 @@ public: /** * Potentially blocking method, which will return immediately if the balancer is not running a - * balancer round and will block until the current round completes otherwise. + * balancer round and will block until the current round completes otherwise. If the operation + * context's deadline is exceeded, it will throw an ExceededTimeLimit exception. */ void joinCurrentRound(OperationContext* txn); diff --git a/src/mongo/db/s/balancer/balancer_chunk_selection_policy_impl.cpp b/src/mongo/db/s/balancer/balancer_chunk_selection_policy_impl.cpp index 5d96be7004b..2f7c6feb7ec 100644 --- a/src/mongo/db/s/balancer/balancer_chunk_selection_policy_impl.cpp +++ b/src/mongo/db/s/balancer/balancer_chunk_selection_policy_impl.cpp @@ -32,7 +32,6 @@ #include "mongo/db/s/balancer/balancer_chunk_selection_policy_impl.h" -#include <set> #include <vector> #include "mongo/base/status_with.h" @@ -59,7 +58,7 @@ namespace { /** * Does a linear pass over the information cached in the specified chunk manager and extracts chunk - * distrubution and chunk placement information which is needed by the balancer policy. + * distribution and chunk placement information which is needed by the balancer policy. */ StatusWith<DistributionStatus> createCollectionDistributionStatus( OperationContext* opCtx, const ShardStatisticsVector& allShards, ChunkManager* chunkMgr) { @@ -258,6 +257,7 @@ StatusWith<MigrateInfoVector> BalancerChunkSelectionPolicyImpl::selectChunksToMo } MigrateInfoVector candidateChunks; + std::set<ShardId> usedShards; for (const auto& coll : collections) { if (coll.getDropped()) { @@ -271,8 +271,8 @@ StatusWith<MigrateInfoVector> BalancerChunkSelectionPolicyImpl::selectChunksToMo continue; } - auto candidatesStatus = - _getMigrateCandidatesForCollection(opCtx, nss, shardStats, aggressiveBalanceHint); + auto candidatesStatus = _getMigrateCandidatesForCollection( + opCtx, nss, shardStats, aggressiveBalanceHint, &usedShards); if (candidatesStatus == ErrorCodes::NamespaceNotFound) { // Namespace got dropped before we managed to get to it, so just skip it continue; @@ -416,7 +416,8 @@ StatusWith<MigrateInfoVector> BalancerChunkSelectionPolicyImpl::_getMigrateCandi OperationContext* opCtx, const NamespaceString& nss, const ShardStatisticsVector& shardStats, - bool aggressiveBalanceHint) { + bool aggressiveBalanceHint, + std::set<ShardId>* usedShards) { auto routingInfoStatus = Grid::get(opCtx)->catalogCache()->getShardedCollectionRoutingInfoWithRefresh(opCtx, nss); if (!routingInfoStatus.isOK()) { @@ -475,7 +476,7 @@ StatusWith<MigrateInfoVector> BalancerChunkSelectionPolicyImpl::_getMigrateCandi } } - return BalancerPolicy::balance(shardStats, distribution, aggressiveBalanceHint); + return BalancerPolicy::balance(shardStats, distribution, aggressiveBalanceHint, usedShards); } } // namespace mongo diff --git a/src/mongo/db/s/balancer/balancer_chunk_selection_policy_impl.h b/src/mongo/db/s/balancer/balancer_chunk_selection_policy_impl.h index f010d8c723b..5a1277cd5ea 100644 --- a/src/mongo/db/s/balancer/balancer_chunk_selection_policy_impl.h +++ b/src/mongo/db/s/balancer/balancer_chunk_selection_policy_impl.h @@ -67,7 +67,8 @@ private: OperationContext* txn, const NamespaceString& nss, const ShardStatisticsVector& shardStats, - bool aggressiveBalanceHint); + bool aggressiveBalanceHint, + std::set<ShardId>* usedShards); // Source for obtaining cluster statistics. Not owned and must not be destroyed before the // policy object is destroyed. diff --git a/src/mongo/db/s/balancer/balancer_policy.cpp b/src/mongo/db/s/balancer/balancer_policy.cpp index 348d8cec603..f5a0473d93e 100644 --- a/src/mongo/db/s/balancer/balancer_policy.cpp +++ b/src/mongo/db/s/balancer/balancer_policy.cpp @@ -1,30 +1,30 @@ /** -* Copyright (C) 2010 10gen Inc. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License, version 3, -* as published by the Free Software Foundation. -* -* 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 -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see <http://www.gnu.org/licenses/>. -* -* 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 GNU Affero General 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. -*/ + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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::kSharding @@ -32,7 +32,6 @@ #include "mongo/db/s/balancer/balancer_policy.h" -#include "mongo/bson/simple_bsonobj_comparator.h" #include "mongo/s/catalog/type_shard.h" #include "mongo/s/catalog/type_tags.h" #include "mongo/util/log.h" @@ -291,20 +290,17 @@ ShardId BalancerPolicy::_getMostOverloadedShard(const ShardStatisticsVector& sha vector<MigrateInfo> BalancerPolicy::balance(const ShardStatisticsVector& shardStats, const DistributionStatus& distribution, - bool shouldAggressivelyBalance) { + bool shouldAggressivelyBalance, + std::set<ShardId>* usedShards) { vector<MigrateInfo> migrations; - // Set of shards, which have already been used for migrations. Used so we don't return multiple - // migrations for the same shard. - set<ShardId> usedShards; - // 1) Check for shards, which are in draining mode { for (const auto& stat : shardStats) { if (!stat.isDraining) continue; - if (usedShards.count(stat.shardId)) + if (usedShards->count(stat.shardId)) continue; const vector<ChunkType>& chunks = distribution.getChunks(stat.shardId); @@ -326,7 +322,7 @@ vector<MigrateInfo> BalancerPolicy::balance(const ShardStatisticsVector& shardSt const string tag = distribution.getTagForChunk(chunk); const ShardId to = - _getLeastLoadedReceiverShard(shardStats, distribution, tag, usedShards); + _getLeastLoadedReceiverShard(shardStats, distribution, tag, *usedShards); if (!to.isValid()) { if (migrations.empty()) { warning() << "Chunk " << redact(chunk.toString()) @@ -337,8 +333,8 @@ vector<MigrateInfo> BalancerPolicy::balance(const ShardStatisticsVector& shardSt invariant(to != stat.shardId); migrations.emplace_back(to, chunk); - invariant(usedShards.insert(stat.shardId).second); - invariant(usedShards.insert(to).second); + invariant(usedShards->insert(stat.shardId).second); + invariant(usedShards->insert(to).second); break; } @@ -352,7 +348,7 @@ vector<MigrateInfo> BalancerPolicy::balance(const ShardStatisticsVector& shardSt // 2) Check for chunks, which are on the wrong shard and must be moved off of it if (!distribution.tags().empty()) { for (const auto& stat : shardStats) { - if (usedShards.count(stat.shardId)) + if (usedShards->count(stat.shardId)) continue; const vector<ChunkType>& chunks = distribution.getChunks(stat.shardId); @@ -373,7 +369,7 @@ vector<MigrateInfo> BalancerPolicy::balance(const ShardStatisticsVector& shardSt } const ShardId to = - _getLeastLoadedReceiverShard(shardStats, distribution, tag, usedShards); + _getLeastLoadedReceiverShard(shardStats, distribution, tag, *usedShards); if (!to.isValid()) { if (migrations.empty()) { warning() << "Chunk " << redact(chunk.toString()) << " violates zone " @@ -384,8 +380,8 @@ vector<MigrateInfo> BalancerPolicy::balance(const ShardStatisticsVector& shardSt invariant(to != stat.shardId); migrations.emplace_back(to, chunk); - invariant(usedShards.insert(stat.shardId).second); - invariant(usedShards.insert(to).second); + invariant(usedShards->insert(stat.shardId).second); + invariant(usedShards->insert(to).second); break; } } @@ -434,7 +430,7 @@ vector<MigrateInfo> BalancerPolicy::balance(const ShardStatisticsVector& shardSt idealNumberOfChunksPerShardForTag, imbalanceThreshold, &migrations, - &usedShards)) + usedShards)) ; } diff --git a/src/mongo/db/s/balancer/balancer_policy.h b/src/mongo/db/s/balancer/balancer_policy.h index 63f44c3ca68..afba1db4915 100644 --- a/src/mongo/db/s/balancer/balancer_policy.h +++ b/src/mongo/db/s/balancer/balancer_policy.h @@ -1,39 +1,42 @@ /** -* Copyright (C) 2010 10gen Inc. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License, version 3, -* as published by the Free Software Foundation. -* -* 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 -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see <http://www.gnu.org/licenses/>. -* -* 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 GNU Affero General 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. -*/ + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ #pragma once +#include <set> +#include <vector> + #include "mongo/base/disallow_copying.h" #include "mongo/bson/bsonobj.h" #include "mongo/bson/simple_bsonobj_comparator.h" #include "mongo/db/s/balancer/cluster_statistics.h" #include "mongo/s/catalog/type_chunk.h" -#include "mongo/s/client/shard.h" +#include "mongo/s/shard_id.h" namespace mongo { @@ -179,10 +182,15 @@ public: * * The shouldAggressivelyBalance parameter causes the threshold for chunk could disparity * between shards to be lowered. + * + * The usedShards parameter is in/out and it contains the set of shards, which have already been + * used for migrations. Used so we don't return multiple conflicting migrations for the same + * shard. */ static std::vector<MigrateInfo> balance(const ShardStatisticsVector& shardStats, const DistributionStatus& distribution, - bool shouldAggressivelyBalance); + bool shouldAggressivelyBalance, + std::set<ShardId>* usedShards); /** * Using the specified distribution information, returns a suggested better location for the diff --git a/src/mongo/db/s/balancer/balancer_policy_test.cpp b/src/mongo/db/s/balancer/balancer_policy_test.cpp index de5ceafaa83..f6abd7c8acf 100644 --- a/src/mongo/db/s/balancer/balancer_policy_test.cpp +++ b/src/mongo/db/s/balancer/balancer_policy_test.cpp @@ -110,14 +110,22 @@ std::pair<ShardStatisticsVector, ShardToChunksMap> generateCluster( return std::make_pair(std::move(shardStats), std::move(chunkMap)); } +std::vector<MigrateInfo> balanceChunks(const ShardStatisticsVector& shardStats, + const DistributionStatus& distribution, + bool shouldAggressivelyBalance) { + std::set<ShardId> usedShards; + return BalancerPolicy::balance( + shardStats, distribution, shouldAggressivelyBalance, &usedShards); +} + TEST(BalancerPolicy, Basic) { auto cluster = generateCluster( {{ShardStatistics(kShardId0, kNoMaxSize, 4, false, emptyTagSet, emptyShardVersion), 4}, {ShardStatistics(kShardId1, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}, {ShardStatistics(kShardId2, kNoMaxSize, 3, false, emptyTagSet, emptyShardVersion), 3}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); ASSERT_EQ(kShardId1, migrations[0].to); @@ -131,8 +139,8 @@ TEST(BalancerPolicy, SmallClusterShouldBePerfectlyBalanced) { {ShardStatistics(kShardId1, kNoMaxSize, 2, false, emptyTagSet, emptyShardVersion), 2}, {ShardStatistics(kShardId2, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId1, migrations[0].from); ASSERT_EQ(kShardId2, migrations[0].to); @@ -146,10 +154,8 @@ TEST(BalancerPolicy, SingleChunkShouldNotMove) { {ShardStatistics(kShardId1, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}}); ASSERT( - BalancerPolicy::balance(cluster.first, DistributionStatus(kNamespace, cluster.second), true) - .empty()); - ASSERT(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false) + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), true).empty()); + ASSERT(balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false) .empty()); } @@ -161,10 +167,8 @@ TEST(BalancerPolicy, BalanceThresholdObeyed) { {ShardStatistics(kShardId3, kNoMaxSize, 1, false, emptyTagSet, emptyShardVersion), 1}}); ASSERT( - BalancerPolicy::balance(cluster.first, DistributionStatus(kNamespace, cluster.second), true) - .empty()); - ASSERT(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false) + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), true).empty()); + ASSERT(balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false) .empty()); } @@ -175,8 +179,8 @@ TEST(BalancerPolicy, ParallelBalancing) { {ShardStatistics(kShardId2, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}, {ShardStatistics(kShardId3, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(2U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); @@ -199,8 +203,8 @@ TEST(BalancerPolicy, ParallelBalancingDoesNotPutChunksOnShardsAboveTheOptimal) { {ShardStatistics(kShardId4, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}, {ShardStatistics(kShardId5, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(2U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); @@ -221,8 +225,60 @@ TEST(BalancerPolicy, ParallelBalancingDoesNotMoveChunksFromShardsBelowOptimal) { {ShardStatistics(kShardId2, kNoMaxSize, 5, false, emptyTagSet, emptyShardVersion), 5}, {ShardStatistics(kShardId3, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}}); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + ASSERT_EQ(1U, migrations.size()); + + ASSERT_EQ(kShardId0, migrations[0].from); + ASSERT_EQ(kShardId3, migrations[0].to); + ASSERT_BSONOBJ_EQ(cluster.second[kShardId0][0].getMin(), migrations[0].minKey); + ASSERT_BSONOBJ_EQ(cluster.second[kShardId0][0].getMax(), migrations[0].maxKey); +} + +TEST(BalancerPolicy, ParallelBalancingNotSchedulingOnInUseSourceShardsWithMoveNecessary) { + auto cluster = generateCluster( + {{ShardStatistics(kShardId0, kNoMaxSize, 8, false, emptyTagSet, emptyShardVersion), 8}, + {ShardStatistics(kShardId1, kNoMaxSize, 4, false, emptyTagSet, emptyShardVersion), 4}, + {ShardStatistics(kShardId2, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}, + {ShardStatistics(kShardId3, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}}); + + // Here kShardId0 would have been selected as a donor + std::set<ShardId> usedShards{kShardId0}; + const auto migrations(BalancerPolicy::balance( + cluster.first, DistributionStatus(kNamespace, cluster.second), false, &usedShards)); + ASSERT_EQ(1U, migrations.size()); + + ASSERT_EQ(kShardId1, migrations[0].from); + ASSERT_EQ(kShardId2, migrations[0].to); + ASSERT_BSONOBJ_EQ(cluster.second[kShardId1][0].getMin(), migrations[0].minKey); + ASSERT_BSONOBJ_EQ(cluster.second[kShardId1][0].getMax(), migrations[0].maxKey); +} + +TEST(BalancerPolicy, ParallelBalancingNotSchedulingOnInUseSourceShardsWithMoveNotNecessary) { + auto cluster = generateCluster( + {{ShardStatistics(kShardId0, kNoMaxSize, 12, false, emptyTagSet, emptyShardVersion), 12}, + {ShardStatistics(kShardId1, kNoMaxSize, 4, false, emptyTagSet, emptyShardVersion), 4}, + {ShardStatistics(kShardId2, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}, + {ShardStatistics(kShardId3, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}}); + + // Here kShardId0 would have been selected as a donor + std::set<ShardId> usedShards{kShardId0}; + const auto migrations(BalancerPolicy::balance( + cluster.first, DistributionStatus(kNamespace, cluster.second), false, &usedShards)); + ASSERT_EQ(0U, migrations.size()); +} + +TEST(BalancerPolicy, ParallelBalancingNotSchedulingOnInUseDestinationShards) { + auto cluster = generateCluster( + {{ShardStatistics(kShardId0, kNoMaxSize, 4, false, emptyTagSet, emptyShardVersion), 4}, + {ShardStatistics(kShardId1, kNoMaxSize, 4, false, emptyTagSet, emptyShardVersion), 4}, + {ShardStatistics(kShardId2, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 0}, + {ShardStatistics(kShardId3, kNoMaxSize, 1, false, emptyTagSet, emptyShardVersion), 1}}); + + // Here kShardId2 would have been selected as a recipient + std::set<ShardId> usedShards{kShardId2}; const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + cluster.first, DistributionStatus(kNamespace, cluster.second), false, &usedShards)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); @@ -241,8 +297,8 @@ TEST(BalancerPolicy, JumboChunksNotMoved) { cluster.second[kShardId0][2].setJumbo(true); cluster.second[kShardId0][3].setJumbo(true); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); ASSERT_EQ(kShardId1, migrations[0].to); @@ -267,8 +323,8 @@ TEST(BalancerPolicy, JumboChunksNotMovedParallel) { cluster.second[kShardId2][2].setJumbo(false); // Only chunk 1 is not jumbo cluster.second[kShardId2][3].setJumbo(true); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(2U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); @@ -288,8 +344,8 @@ TEST(BalancerPolicy, DrainingSingleChunk) { {{ShardStatistics(kShardId0, kNoMaxSize, 2, true, emptyTagSet, emptyShardVersion), 1}, {ShardStatistics(kShardId1, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 5}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); ASSERT_EQ(kShardId1, migrations[0].to); @@ -305,8 +361,8 @@ TEST(BalancerPolicy, DrainingSingleChunkPerShard) { {ShardStatistics(kShardId2, kNoMaxSize, 2, true, emptyTagSet, emptyShardVersion), 1}, {ShardStatistics(kShardId3, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 5}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(2U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); @@ -326,8 +382,8 @@ TEST(BalancerPolicy, DrainingWithTwoChunksFirstOneSelected) { {{ShardStatistics(kShardId0, kNoMaxSize, 2, true, emptyTagSet, emptyShardVersion), 2}, {ShardStatistics(kShardId1, kNoMaxSize, 0, false, emptyTagSet, emptyShardVersion), 5}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); ASSERT_EQ(kShardId1, migrations[0].to); @@ -343,8 +399,8 @@ TEST(BalancerPolicy, DrainingMultipleShardsFirstOneSelected) { {ShardStatistics(kShardId1, kNoMaxSize, 5, true, emptyTagSet, emptyShardVersion), 2}, {ShardStatistics(kShardId2, kNoMaxSize, 5, false, emptyTagSet, emptyShardVersion), 16}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); ASSERT_EQ(kShardId2, migrations[0].to); @@ -359,8 +415,8 @@ TEST(BalancerPolicy, DrainingMultipleShardsWontAcceptChunks) { {ShardStatistics(kShardId1, kNoMaxSize, 0, true, emptyTagSet, emptyShardVersion), 0}, {ShardStatistics(kShardId2, kNoMaxSize, 0, true, emptyTagSet, emptyShardVersion), 0}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT(migrations.empty()); } @@ -374,7 +430,7 @@ TEST(BalancerPolicy, DrainingSingleAppropriateShardFoundDueToTag) { ASSERT_OK(distribution.addRangeToZone(ZoneRange( cluster.second[kShardId2][0].getMin(), cluster.second[kShardId2][0].getMax(), "LAX"))); - const auto migrations(BalancerPolicy::balance(cluster.first, distribution, false)); + const auto migrations(balanceChunks(cluster.first, distribution, false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId2, migrations[0].from); ASSERT_EQ(kShardId1, migrations[0].to); @@ -392,7 +448,7 @@ TEST(BalancerPolicy, DrainingNoAppropriateShardsFoundDueToTag) { ASSERT_OK(distribution.addRangeToZone(ZoneRange( cluster.second[kShardId2][0].getMin(), cluster.second[kShardId2][0].getMax(), "SEA"))); - const auto migrations(BalancerPolicy::balance(cluster.first, distribution, false)); + const auto migrations(balanceChunks(cluster.first, distribution, false)); ASSERT(migrations.empty()); } @@ -403,8 +459,8 @@ TEST(BalancerPolicy, NoBalancingDueToAllNodesEitherDrainingOrMaxedOut) { {ShardStatistics(kShardId1, 1, 1, false, emptyTagSet, emptyShardVersion), 6}, {ShardStatistics(kShardId2, kNoMaxSize, 1, true, emptyTagSet, emptyShardVersion), 1}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT(migrations.empty()); } @@ -417,8 +473,8 @@ TEST(BalancerPolicy, BalancerRespectsMaxShardSizeOnlyBalanceToNonMaxed) { {ShardStatistics(kShardId1, kNoMaxSize, 5, false, emptyTagSet, emptyShardVersion), 5}, {ShardStatistics(kShardId2, kNoMaxSize, 10, false, emptyTagSet, emptyShardVersion), 10}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId2, migrations[0].from); ASSERT_EQ(kShardId1, migrations[0].to); @@ -435,8 +491,8 @@ TEST(BalancerPolicy, BalancerRespectsMaxShardSizeWhenAllBalanced) { {ShardStatistics(kShardId1, kNoMaxSize, 4, false, emptyTagSet, emptyShardVersion), 4}, {ShardStatistics(kShardId2, kNoMaxSize, 4, false, emptyTagSet, emptyShardVersion), 4}}); - const auto migrations(BalancerPolicy::balance( - cluster.first, DistributionStatus(kNamespace, cluster.second), false)); + const auto migrations( + balanceChunks(cluster.first, DistributionStatus(kNamespace, cluster.second), false)); ASSERT(migrations.empty()); } @@ -451,7 +507,7 @@ TEST(BalancerPolicy, BalancerRespectsTagsWhenDraining) { ASSERT_OK(distribution.addRangeToZone(ZoneRange(kMinBSONKey, BSON("x" << 7), "a"))); ASSERT_OK(distribution.addRangeToZone(ZoneRange(BSON("x" << 8), kMaxBSONKey, "b"))); - const auto migrations(BalancerPolicy::balance(cluster.first, distribution, false)); + const auto migrations(balanceChunks(cluster.first, distribution, false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId1, migrations[0].from); ASSERT_EQ(kShardId0, migrations[0].to); @@ -470,7 +526,7 @@ TEST(BalancerPolicy, BalancerRespectsTagPolicyBeforeImbalance) { DistributionStatus distribution(kNamespace, cluster.second); ASSERT_OK(distribution.addRangeToZone(ZoneRange(kMinBSONKey, BSON("x" << 100), "a"))); - const auto migrations(BalancerPolicy::balance(cluster.first, distribution, false)); + const auto migrations(balanceChunks(cluster.first, distribution, false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId2, migrations[0].from); ASSERT_EQ(kShardId0, migrations[0].to); @@ -490,7 +546,7 @@ TEST(BalancerPolicy, BalancerFixesIncorrectTagsWithCrossShardViolationOfTags) { ASSERT_OK(distribution.addRangeToZone(ZoneRange(kMinBSONKey, BSON("x" << 1), "b"))); ASSERT_OK(distribution.addRangeToZone(ZoneRange(BSON("x" << 8), kMaxBSONKey, "a"))); - const auto migrations(BalancerPolicy::balance(cluster.first, distribution, false)); + const auto migrations(balanceChunks(cluster.first, distribution, false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId0, migrations[0].from); ASSERT_EQ(kShardId2, migrations[0].to); @@ -508,7 +564,7 @@ TEST(BalancerPolicy, BalancerFixesIncorrectTagsInOtherwiseBalancedCluster) { DistributionStatus distribution(kNamespace, cluster.second); ASSERT_OK(distribution.addRangeToZone(ZoneRange(kMinBSONKey, BSON("x" << 10), "a"))); - const auto migrations(BalancerPolicy::balance(cluster.first, distribution, false)); + const auto migrations(balanceChunks(cluster.first, distribution, false)); ASSERT_EQ(1U, migrations.size()); ASSERT_EQ(kShardId2, migrations[0].from); ASSERT_EQ(kShardId0, migrations[0].to); @@ -527,7 +583,7 @@ TEST(BalancerPolicy, BalancerFixesIncorrectTagsInOtherwiseBalancedClusterParalle DistributionStatus distribution(kNamespace, cluster.second); ASSERT_OK(distribution.addRangeToZone(ZoneRange(kMinBSONKey, BSON("x" << 20), "a"))); - const auto migrations(BalancerPolicy::balance(cluster.first, distribution, false)); + const auto migrations(balanceChunks(cluster.first, distribution, false)); ASSERT_EQ(2U, migrations.size()); ASSERT_EQ(kShardId2, migrations[0].from); @@ -550,7 +606,7 @@ TEST(BalancerPolicy, BalancerHandlesNoShardsWithTag) { ASSERT_OK( distribution.addRangeToZone(ZoneRange(kMinBSONKey, BSON("x" << 7), "NonExistentZone"))); - ASSERT(BalancerPolicy::balance(cluster.first, distribution, false).empty()); + ASSERT(balanceChunks(cluster.first, distribution, false).empty()); } TEST(DistributionStatus, AddTagRangeOverlap) { diff --git a/src/mongo/db/s/collection_range_deleter.cpp b/src/mongo/db/s/collection_range_deleter.cpp deleted file mode 100644 index 803541d16f9..00000000000 --- a/src/mongo/db/s/collection_range_deleter.cpp +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Copyright (C) 2016 MongoDB Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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 - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * 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 GNU Affero General 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::kSharding - -#include "mongo/platform/basic.h" - -#include "mongo/db/s/collection_range_deleter.h" - -#include <algorithm> - -#include "mongo/db/catalog/collection.h" -#include "mongo/db/client.h" -#include "mongo/db/db_raii.h" -#include "mongo/db/dbhelpers.h" -#include "mongo/db/exec/working_set_common.h" -#include "mongo/db/index/index_descriptor.h" -#include "mongo/db/keypattern.h" -#include "mongo/db/query/internal_plans.h" -#include "mongo/db/query/query_knobs.h" -#include "mongo/db/query/query_planner.h" -#include "mongo/db/repl/repl_client_info.h" -#include "mongo/db/repl/replication_coordinator_global.h" -#include "mongo/db/s/collection_sharding_state.h" -#include "mongo/db/s/sharding_state.h" -#include "mongo/db/write_concern.h" -#include "mongo/executor/task_executor.h" -#include "mongo/util/log.h" -#include "mongo/util/mongoutils/str.h" -#include "mongo/util/scopeguard.h" - -namespace mongo { - -class ChunkRange; -class OldClientWriteContext; - -using CallbackArgs = executor::TaskExecutor::CallbackArgs; -using logger::LogComponent; - -namespace { - -const WriteConcernOptions kMajorityWriteConcern(WriteConcernOptions::kMajority, - WriteConcernOptions::SyncMode::UNSET, - Seconds(60)); - -} // unnamed namespace - -CollectionRangeDeleter::CollectionRangeDeleter(NamespaceString nss) : _nss(std::move(nss)) {} - -void CollectionRangeDeleter::run() { - Client::initThread(getThreadName()); - ON_BLOCK_EXIT([&] { Client::destroy(); }); - auto txn = cc().makeOperationContext().get(); - bool hasNextRangeToClean = cleanupNextRange(txn); - - // If there are more ranges to run, we add <this> back onto the task executor to run again. - if (hasNextRangeToClean) { - auto executor = ShardingState::get(txn)->getRangeDeleterTaskExecutor(); - executor->scheduleWork([this](const CallbackArgs& cbArgs) { run(); }); - } else { - delete this; - } -} - -bool CollectionRangeDeleter::cleanupNextRange(OperationContext* txn) { - int numDocumentsDeleted; - - { - AutoGetCollection autoColl(txn, _nss, MODE_IX); - Collection* collection = autoColl.getCollection(); - if (!collection) { - return false; - } - - CollectionShardingState* shardingState = CollectionShardingState::get(txn, _nss); - MetadataManager& metadataManager = *shardingState->_metadataManager; - - if (!_rangeInProgress && !metadataManager.hasRangesToClean()) { - // Nothing left to do - return false; - } - - if (!_rangeInProgress || !metadataManager.isInRangesToClean(_rangeInProgress.get())) { - // No valid chunk in progress, get a new one - _rangeInProgress = metadataManager.getNextRangeToClean(); - } - - auto metadata = shardingState->getMetadata(); - if (!metadata) { - return false; - } - - numDocumentsDeleted = _doDeletion(txn, collection, metadata->getKeyPattern()); - if (numDocumentsDeleted <= 0) { - metadataManager.removeRangeToClean(_rangeInProgress.get()); - _rangeInProgress = boost::none; - return true; - } - } - - // wait for replication - WriteConcernResult wcResult; - auto currentClientOpTime = repl::ReplClientInfo::forClient(txn->getClient()).getLastOp(); - Status status = waitForWriteConcern(txn, currentClientOpTime, kMajorityWriteConcern, &wcResult); - if (!status.isOK()) { - warning() << "Error when waiting for write concern after removing chunks in " << _nss - << " : " << status.reason(); - } - - return true; -} - -int CollectionRangeDeleter::_doDeletion(OperationContext* txn, - Collection* collection, - const BSONObj& keyPattern) { - invariant(_rangeInProgress); - invariant(collection); - - // The IndexChunk has a keyPattern that may apply to more than one index - we need to - // select the index and get the full index keyPattern here. - const IndexDescriptor* idx = - collection->getIndexCatalog()->findShardKeyPrefixedIndex(txn, keyPattern, false); - if (idx == NULL) { - warning() << "Unable to find shard key index for " << keyPattern.toString() << " in " - << _nss; - return -1; - } - - KeyPattern indexKeyPattern(idx->keyPattern().getOwned()); - - // Extend bounds to match the index we found - const BSONObj& min = - Helpers::toKeyFormat(indexKeyPattern.extendRangeBound(_rangeInProgress->getMin(), false)); - const BSONObj& max = - Helpers::toKeyFormat(indexKeyPattern.extendRangeBound(_rangeInProgress->getMax(), false)); - - LOG(1) << "begin removal of " << min << " to " << max << " in " << _nss; - - auto indexName = idx->indexName(); - IndexDescriptor* desc = collection->getIndexCatalog()->findIndexByName(txn, indexName); - if (!desc) { - warning() << "shard key index with name " << indexName << " on '" << _nss - << "' was dropped"; - return -1; - } - - std::unique_ptr<PlanExecutor> exec( - InternalPlanner::indexScan(txn, - collection, - desc, - min, - max, - BoundInclusion::kIncludeStartKeyOnly, - PlanExecutor::YIELD_MANUAL, - InternalPlanner::FORWARD, - InternalPlanner::IXSCAN_FETCH)); - int numDeleted = 0; - const int maxItersBeforeYield = std::max(static_cast<int>(internalQueryExecYieldIterations), 1); - - while (numDeleted < maxItersBeforeYield) { - RecordId rloc; - BSONObj obj; - PlanExecutor::ExecState state; - state = exec->getNext(&obj, &rloc); - if (PlanExecutor::IS_EOF == state) { - break; - } - - if (PlanExecutor::FAILURE == state || PlanExecutor::DEAD == state) { - warning(LogComponent::kSharding) - << PlanExecutor::statestr(state) << " - cursor error while trying to delete " << min - << " to " << max << " in " << _nss << ": " << WorkingSetCommon::toStatusString(obj) - << ", stats: " << Explain::getWinningPlanStats(exec.get()); - break; - } - - invariant(PlanExecutor::ADVANCED == state); - - WriteUnitOfWork wuow(txn); - - NamespaceString nss(_nss); - if (!repl::getGlobalReplicationCoordinator()->canAcceptWritesFor(nss)) { - warning() << "stepped down from primary while deleting chunk; " - << "orphaning data in " << _nss << " in range [" << min << ", " << max << ")"; - return numDeleted; - } - - OpDebug* const nullOpDebug = nullptr; - collection->deleteDocument(txn, rloc, nullOpDebug, true); - wuow.commit(); - numDeleted++; - } - - return numDeleted; -} - -} // namespace mongo diff --git a/src/mongo/db/s/collection_sharding_state.h b/src/mongo/db/s/collection_sharding_state.h index 2a01bec5567..1abababcc1b 100644 --- a/src/mongo/db/s/collection_sharding_state.h +++ b/src/mongo/db/s/collection_sharding_state.h @@ -168,8 +168,6 @@ public: void onDropCollection(OperationContext* txn, const NamespaceString& collectionName); private: - friend class CollectionRangeDeleter; - /** * Checks whether the shard version of the operation matches that of the collection. * diff --git a/src/mongo/db/s/metadata_manager.cpp b/src/mongo/db/s/metadata_manager.cpp index 28ca20e2f6f..0bba1ff478e 100644 --- a/src/mongo/db/s/metadata_manager.cpp +++ b/src/mongo/db/s/metadata_manager.cpp @@ -34,7 +34,6 @@ #include "mongo/bson/simple_bsonobj_comparator.h" #include "mongo/db/range_arithmetic.h" -#include "mongo/db/s/collection_range_deleter.h" #include "mongo/db/s/sharding_state.h" #include "mongo/stdx/memory.h" #include "mongo/util/log.h" @@ -47,9 +46,8 @@ MetadataManager::MetadataManager(ServiceContext* sc, NamespaceString nss) : _nss(std::move(nss)), _serviceContext(sc), _activeMetadataTracker(stdx::make_unique<CollectionMetadataTracker>(nullptr)), - _receivingChunks(SimpleBSONObjComparator::kInstance.makeBSONObjIndexedMap<CachedChunkInfo>()), - _rangesToClean( - SimpleBSONObjComparator::kInstance.makeBSONObjIndexedMap<RangeToCleanDescriptor>()) {} + _receivingChunks( + SimpleBSONObjComparator::kInstance.makeBSONObjIndexedMap<CachedChunkInfo>()) {} MetadataManager::~MetadataManager() { stdx::lock_guard<stdx::mutex> scopedLock(_managerLock); @@ -74,7 +72,6 @@ void MetadataManager::refreshActiveMetadata(std::unique_ptr<CollectionMetadata> // collection sharding information regardless of whether the node is sharded or not. if (!remoteMetadata && !_activeMetadataTracker->metadata) { invariant(_receivingChunks.empty()); - invariant(_rangesToClean.empty()); return; } @@ -84,8 +81,6 @@ void MetadataManager::refreshActiveMetadata(std::unique_ptr<CollectionMetadata> << _activeMetadataTracker->metadata->toStringBasic() << " as no longer sharded"; _receivingChunks.clear(); - _rangesToClean.clear(); - _setActiveMetadata_inlock(nullptr); return; } @@ -100,8 +95,6 @@ void MetadataManager::refreshActiveMetadata(std::unique_ptr<CollectionMetadata> << remoteMetadata->toStringBasic(); invariant(_receivingChunks.empty()); - invariant(_rangesToClean.empty()); - _setActiveMetadata_inlock(std::move(remoteMetadata)); return; } @@ -115,8 +108,6 @@ void MetadataManager::refreshActiveMetadata(std::unique_ptr<CollectionMetadata> << remoteMetadata->toStringBasic() << " due to epoch change"; _receivingChunks.clear(); - _rangesToClean.clear(); - _setActiveMetadata_inlock(std::move(remoteMetadata)); return; } @@ -168,9 +159,6 @@ void MetadataManager::refreshActiveMetadata(std::unique_ptr<CollectionMetadata> const ChunkRange receivingRange(itRecv->first, itRecv->second.getMaxKey()); _receivingChunks.erase(itRecv); - - // Make sure any potentially partially copied chunks are scheduled to be cleaned up - _addRangeToClean_inlock(receivingRange); } // Need to reset the iterator @@ -207,14 +195,10 @@ void MetadataManager::beginReceive(const ChunkRange& range) { const ChunkRange receivingRange(itRecv->first, itRecv->second.getMaxKey()); _receivingChunks.erase(itRecv); - - // Make sure any potentially partially copied chunks are scheduled to be cleaned up - _addRangeToClean_inlock(receivingRange); } // Need to ensure that the background range deleter task won't delete the range we are about to // receive - _removeRangeToClean_inlock(range, Status::OK()); _receivingChunks.insert( std::make_pair(range.getMin().getOwned(), CachedChunkInfo(range.getMax().getOwned(), ChunkVersion::IGNORED()))); @@ -241,9 +225,6 @@ void MetadataManager::forgetReceive(const ChunkRange& range) { _receivingChunks.erase(it); } - // This is potentially a partially received data, which needs to be cleaned up - _addRangeToClean_inlock(range); - // For compatibility with the current range deleter, update the pending chunks on the collection // metadata to exclude the chunk being received, which was added in beginReceive ChunkType chunk; @@ -343,91 +324,9 @@ ScopedCollectionMetadata::operator bool() const { return _tracker && _tracker->metadata.get(); } -RangeMap MetadataManager::getCopyOfRangesToClean() { - stdx::lock_guard<stdx::mutex> scopedLock(_managerLock); - return _getCopyOfRangesToClean_inlock(); -} - -RangeMap MetadataManager::_getCopyOfRangesToClean_inlock() { - RangeMap ranges = SimpleBSONObjComparator::kInstance.makeBSONObjIndexedMap<CachedChunkInfo>(); - for (auto it = _rangesToClean.begin(); it != _rangesToClean.end(); ++it) { - ranges.insert(std::make_pair( - it->first, CachedChunkInfo(it->second.getMax(), ChunkVersion::IGNORED()))); - } - return ranges; -} - -std::shared_ptr<Notification<Status>> MetadataManager::addRangeToClean(const ChunkRange& range) { - stdx::lock_guard<stdx::mutex> scopedLock(_managerLock); - return _addRangeToClean_inlock(range); -} - -std::shared_ptr<Notification<Status>> MetadataManager::_addRangeToClean_inlock( - const ChunkRange& range) { - // This first invariant currently makes an unnecessary copy, to reuse the - // rangeMapOverlaps helper function. - invariant(!rangeMapOverlaps(_getCopyOfRangesToClean_inlock(), range.getMin(), range.getMax())); - invariant(!rangeMapOverlaps(_receivingChunks, range.getMin(), range.getMax())); - - RangeToCleanDescriptor descriptor(range.getMax().getOwned()); - _rangesToClean.insert(std::make_pair(range.getMin().getOwned(), descriptor)); - - // If _rangesToClean was previously empty, we need to start the collection range deleter - if (_rangesToClean.size() == 1UL) { - ShardingState::get(_serviceContext)->scheduleCleanup(_nss); - } - - return descriptor.getNotification(); -} - -void MetadataManager::removeRangeToClean(const ChunkRange& range, Status deletionStatus) { - stdx::lock_guard<stdx::mutex> scopedLock(_managerLock); - _removeRangeToClean_inlock(range, deletionStatus); -} - -void MetadataManager::_removeRangeToClean_inlock(const ChunkRange& range, Status deletionStatus) { - auto it = _rangesToClean.upper_bound(range.getMin()); - // We want our iterator to point at the greatest value - // that is still less than or equal to range. - if (it != _rangesToClean.begin()) { - --it; - } - - for (; it != _rangesToClean.end() && - SimpleBSONObjComparator::kInstance.evaluate(it->first < range.getMax());) { - if (SimpleBSONObjComparator::kInstance.evaluate(it->second.getMax() <= range.getMin())) { - ++it; - continue; - } - - // There's overlap between *it and range so we remove *it - // and then replace with new ranges. - BSONObj oldMin = it->first; - BSONObj oldMax = it->second.getMax(); - it->second.complete(deletionStatus); - _rangesToClean.erase(it++); - if (SimpleBSONObjComparator::kInstance.evaluate(oldMin < range.getMin())) { - _addRangeToClean_inlock(ChunkRange(oldMin, range.getMin())); - } - - if (SimpleBSONObjComparator::kInstance.evaluate(oldMax > range.getMax())) { - _addRangeToClean_inlock(ChunkRange(range.getMax(), oldMax)); - } - } -} - void MetadataManager::append(BSONObjBuilder* builder) { stdx::lock_guard<stdx::mutex> scopedLock(_managerLock); - BSONArrayBuilder rtcArr(builder->subarrayStart("rangesToClean")); - for (const auto& entry : _rangesToClean) { - BSONObjBuilder obj; - ChunkRange r = ChunkRange(entry.first, entry.second.getMax()); - r.append(&obj); - rtcArr.append(obj.done()); - } - rtcArr.done(); - BSONArrayBuilder pcArr(builder->subarrayStart("pendingChunks")); for (const auto& entry : _receivingChunks) { BSONObjBuilder obj; @@ -447,23 +346,4 @@ void MetadataManager::append(BSONObjBuilder* builder) { amrArr.done(); } -bool MetadataManager::hasRangesToClean() { - stdx::lock_guard<stdx::mutex> scopedLock(_managerLock); - return !_rangesToClean.empty(); -} - -bool MetadataManager::isInRangesToClean(const ChunkRange& range) { - stdx::lock_guard<stdx::mutex> scopedLock(_managerLock); - // For convenience, this line makes an unnecessary copy, to reuse the - // rangeMapContains helper function. - return rangeMapContains(_getCopyOfRangesToClean_inlock(), range.getMin(), range.getMax()); -} - -ChunkRange MetadataManager::getNextRangeToClean() { - stdx::lock_guard<stdx::mutex> scopedLock(_managerLock); - invariant(!_rangesToClean.empty()); - auto it = _rangesToClean.begin(); - return ChunkRange(it->first, it->second.getMax()); -} - } // namespace mongo diff --git a/src/mongo/db/s/metadata_manager.h b/src/mongo/db/s/metadata_manager.h index fa61e9ef2ea..b2e712d7f9f 100644 --- a/src/mongo/db/s/metadata_manager.h +++ b/src/mongo/db/s/metadata_manager.h @@ -85,52 +85,10 @@ public: RangeMap getCopyOfReceivingChunks(); /** - * Adds a new range to be cleaned up. - * The newly introduced range must not overlap with the existing ranges. - */ - std::shared_ptr<Notification<Status>> addRangeToClean(const ChunkRange& range); - - /** - * Calls removeRangeToClean with Status::OK. - */ - void removeRangeToClean(const ChunkRange& range) { - removeRangeToClean(range, Status::OK()); - } - - /** - * Removes the specified range from the ranges to be cleaned up. - * The specified deletionStatus will be returned to callers waiting - * on whether the deletion succeeded or failed. - */ - void removeRangeToClean(const ChunkRange& range, Status deletionStatus); - - /** - * Gets copy of the set of chunk ranges which are scheduled for cleanup. - * Converts RangeToCleanMap to RangeMap. - */ - RangeMap getCopyOfRangesToClean(); - - /** * Appends information on all the chunk ranges in rangesToClean to builder. */ void append(BSONObjBuilder* builder); - /** - * Returns true if _rangesToClean is not empty. - */ - bool hasRangesToClean(); - - /** - * Returns true if the exact range is in _rangesToClean. - */ - bool isInRangesToClean(const ChunkRange& range); - - /** - * Gets and returns, but does not remove, a single ChunkRange from _rangesToClean. - * Should not be called if _rangesToClean is empty: it will hit an invariant. - */ - ChunkRange getNextRangeToClean(); - private: friend class ScopedCollectionMetadata; @@ -193,12 +151,6 @@ private: */ void _removeMetadata_inlock(CollectionMetadataTracker* metadataTracker); - std::shared_ptr<Notification<Status>> _addRangeToClean_inlock(const ChunkRange& range); - - void _removeRangeToClean_inlock(const ChunkRange& range, Status deletionStatus); - - RangeMap _getCopyOfRangesToClean_inlock(); - void _setActiveMetadata_inlock(std::unique_ptr<CollectionMetadata> newMetadata); const NamespaceString _nss; @@ -219,10 +171,6 @@ private: // Chunk ranges which are currently assumed to be transferred to the shard. Indexed by the min // key of the range. RangeMap _receivingChunks; - - // Set of ranges to be deleted. Indexed by the min key of the range. - typedef BSONObjIndexedMap<RangeToCleanDescriptor> RangeToCleanMap; - RangeToCleanMap _rangesToClean; }; class ScopedCollectionMetadata { diff --git a/src/mongo/db/s/metadata_manager_test.cpp b/src/mongo/db/s/metadata_manager_test.cpp index 19f4fd405ab..207eb4fd37b 100644 --- a/src/mongo/db/s/metadata_manager_test.cpp +++ b/src/mongo/db/s/metadata_manager_test.cpp @@ -50,12 +50,6 @@ using unittest::assertGet; class MetadataManagerTest : public ServiceContextMongoDTest { protected: - void setUp() override { - ServiceContextMongoDTest::setUp(); - ShardingState::get(getServiceContext()) - ->setScheduleCleanupFunctionForTest([](const NamespaceString& nss) {}); - } - static std::unique_ptr<CollectionMetadata> makeEmptyMetadata() { const OID epoch = OID::gen(); @@ -106,8 +100,7 @@ TEST_F(MetadataManagerTest, SetAndGetActiveMetadata) { ScopedCollectionMetadata scopedMetadata = manager.getActiveMetadata(manager_ptr); ASSERT_EQ(cmPtr, scopedMetadata.getMetadata()); -}; - +} TEST_F(MetadataManagerTest, ResetActiveMetadata) { manager.refreshActiveMetadata(makeEmptyMetadata()); @@ -124,167 +117,8 @@ TEST_F(MetadataManagerTest, ResetActiveMetadata) { ScopedCollectionMetadata scopedMetadata2 = manager.getActiveMetadata(manager_ptr); ASSERT_EQ(cm2Ptr, scopedMetadata2.getMetadata()); -}; - -TEST_F(MetadataManagerTest, AddAndRemoveRangesToClean) { - ChunkRange cr1 = ChunkRange(BSON("key" << 0), BSON("key" << 10)); - ChunkRange cr2 = ChunkRange(BSON("key" << 10), BSON("key" << 20)); - - manager.addRangeToClean(cr1); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 1UL); - manager.removeRangeToClean(cr1); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 0UL); - - manager.addRangeToClean(cr1); - manager.addRangeToClean(cr2); - manager.removeRangeToClean(cr1); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 1UL); - auto ranges = manager.getCopyOfRangesToClean(); - auto it = ranges.find(cr2.getMin()); - ChunkRange remainingChunk = ChunkRange(it->first, it->second.getMaxKey()); - ASSERT_EQ(remainingChunk.toString(), cr2.toString()); - manager.removeRangeToClean(cr2); -} - -// Tests that a removal in the middle of an existing ChunkRange results in -// two correct chunk ranges. -TEST_F(MetadataManagerTest, RemoveRangeInMiddleOfRange) { - ChunkRange cr1 = ChunkRange(BSON("key" << 0), BSON("key" << 10)); - - manager.addRangeToClean(cr1); - manager.removeRangeToClean(ChunkRange(BSON("key" << 4), BSON("key" << 6))); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 2UL); - - auto ranges = manager.getCopyOfRangesToClean(); - auto it = ranges.find(BSON("key" << 0)); - ChunkRange expectedChunk = ChunkRange(BSON("key" << 0), BSON("key" << 4)); - ChunkRange remainingChunk = ChunkRange(it->first, it->second.getMaxKey()); - ASSERT_EQ(remainingChunk.toString(), expectedChunk.toString()); - - it++; - expectedChunk = ChunkRange(BSON("key" << 6), BSON("key" << 10)); - remainingChunk = ChunkRange(it->first, it->second.getMaxKey()); - ASSERT_EQ(remainingChunk.toString(), expectedChunk.toString()); - - manager.removeRangeToClean(cr1); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 0UL); -} - -// Tests removals that overlap with just one ChunkRange. -TEST_F(MetadataManagerTest, RemoveRangeWithSingleRangeOverlap) { - ChunkRange cr1 = ChunkRange(BSON("key" << 0), BSON("key" << 10)); - - manager.addRangeToClean(cr1); - manager.removeRangeToClean(ChunkRange(BSON("key" << 0), BSON("key" << 5))); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 1UL); - auto ranges = manager.getCopyOfRangesToClean(); - auto it = ranges.find(BSON("key" << 5)); - ChunkRange remainingChunk = ChunkRange(it->first, it->second.getMaxKey()); - ChunkRange expectedChunk = ChunkRange(BSON("key" << 5), BSON("key" << 10)); - ASSERT_EQ(remainingChunk.toString(), expectedChunk.toString()); - - manager.removeRangeToClean(ChunkRange(BSON("key" << 4), BSON("key" << 6))); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 1UL); - ranges = manager.getCopyOfRangesToClean(); - it = ranges.find(BSON("key" << 6)); - remainingChunk = ChunkRange(it->first, it->second.getMaxKey()); - expectedChunk = ChunkRange(BSON("key" << 6), BSON("key" << 10)); - ASSERT_EQ(remainingChunk.toString(), expectedChunk.toString()); - - manager.removeRangeToClean(ChunkRange(BSON("key" << 9), BSON("key" << 13))); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 1UL); - ranges = manager.getCopyOfRangesToClean(); - it = ranges.find(BSON("key" << 6)); - remainingChunk = ChunkRange(it->first, it->second.getMaxKey()); - expectedChunk = ChunkRange(BSON("key" << 6), BSON("key" << 9)); - ASSERT_EQ(remainingChunk.toString(), expectedChunk.toString()); - - manager.removeRangeToClean(ChunkRange(BSON("key" << 0), BSON("key" << 10))); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 0UL); -} - -// Tests removals that overlap with more than one ChunkRange. -TEST_F(MetadataManagerTest, RemoveRangeWithMultipleRangeOverlaps) { - ChunkRange cr1 = ChunkRange(BSON("key" << 0), BSON("key" << 10)); - ChunkRange cr2 = ChunkRange(BSON("key" << 10), BSON("key" << 20)); - ChunkRange cr3 = ChunkRange(BSON("key" << 20), BSON("key" << 30)); - - manager.addRangeToClean(cr1); - manager.addRangeToClean(cr2); - manager.addRangeToClean(cr3); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 3UL); - - manager.removeRangeToClean(ChunkRange(BSON("key" << 8), BSON("key" << 22))); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 2UL); - auto ranges = manager.getCopyOfRangesToClean(); - auto it = ranges.find(BSON("key" << 0)); - ChunkRange remainingChunk = ChunkRange(it->first, it->second.getMaxKey()); - ChunkRange expectedChunk = ChunkRange(BSON("key" << 0), BSON("key" << 8)); - ASSERT_EQ(remainingChunk.toString(), expectedChunk.toString()); - it++; - remainingChunk = ChunkRange(it->first, it->second.getMaxKey()); - expectedChunk = ChunkRange(BSON("key" << 22), BSON("key" << 30)); - ASSERT_EQ(remainingChunk.toString(), expectedChunk.toString()); - - manager.removeRangeToClean(ChunkRange(BSON("key" << 0), BSON("key" << 30))); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 0UL); -} - -TEST_F(MetadataManagerTest, AddAndRemoveRangeNotificationsBlockAndYield) { - manager.refreshActiveMetadata(makeEmptyMetadata()); - - ChunkRange cr1(BSON("key" << 0), BSON("key" << 10)); - auto notification = manager.addRangeToClean(cr1); - manager.removeRangeToClean(cr1, Status::OK()); - ASSERT_OK(notification->get()); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 0UL); -} - -TEST_F(MetadataManagerTest, RemoveRangeToCleanCorrectlySetsBadStatus) { - manager.refreshActiveMetadata(makeEmptyMetadata()); - - ChunkRange cr1(BSON("key" << 0), BSON("key" << 10)); - auto notification = manager.addRangeToClean(cr1); - manager.removeRangeToClean(cr1, Status(ErrorCodes::InternalError, "test error")); - ASSERT_NOT_OK(notification->get()); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 0UL); -} - -TEST_F(MetadataManagerTest, RemovingSubrangeStillSetsNotificationStatus) { - manager.refreshActiveMetadata(makeEmptyMetadata()); - - ChunkRange cr1(BSON("key" << 0), BSON("key" << 10)); - auto notification = manager.addRangeToClean(cr1); - manager.removeRangeToClean(ChunkRange(BSON("key" << 3), BSON("key" << 7))); - ASSERT_OK(notification->get()); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 2UL); - manager.removeRangeToClean(cr1); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 0UL); - - notification = manager.addRangeToClean(cr1); - manager.removeRangeToClean(ChunkRange(BSON("key" << 7), BSON("key" << 15))); - ASSERT_OK(notification->get()); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 1UL); - manager.removeRangeToClean(cr1); - ASSERT_EQ(manager.getCopyOfRangesToClean().size(), 0UL); } -TEST_F(MetadataManagerTest, NotificationBlocksUntilDeletion) { - manager.refreshActiveMetadata(makeEmptyMetadata()); - - ChunkRange cr1(BSON("key" << 0), BSON("key" << 10)); - auto notification = manager.addRangeToClean(cr1); - auto txn = cc().makeOperationContext().get(); - // Once the new range deleter is set up, this might fail if the range deleter - // deleted cr1 before we got here... - ASSERT_FALSE(notification->waitFor(txn, Milliseconds(0))); - - manager.removeRangeToClean(cr1); - ASSERT_TRUE(notification->waitFor(txn, Milliseconds(0))); - ASSERT_OK(notification->get()); -} - - TEST_F(MetadataManagerTest, RefreshAfterSuccessfulMigrationSinglePending) { manager.refreshActiveMetadata(makeEmptyMetadata()); @@ -417,29 +251,5 @@ TEST_F(MetadataManagerTest, RefreshMetadataAfterDropAndRecreate) { ASSERT_EQ(newVersion, chunkEntry->second.getVersion()); } -// Tests membership functions for _rangesToClean -TEST_F(MetadataManagerTest, RangesToCleanMembership) { - manager.refreshActiveMetadata(makeEmptyMetadata()); - - ASSERT(!manager.hasRangesToClean()); - - ChunkRange cr1 = ChunkRange(BSON("key" << 0), BSON("key" << 10)); - manager.addRangeToClean(cr1); - - ASSERT(manager.hasRangesToClean()); - ASSERT(manager.isInRangesToClean(cr1)); -} - -// Tests that getNextRangeToClean successfully pulls a stored ChunkRange -TEST_F(MetadataManagerTest, GetNextRangeToClean) { - manager.refreshActiveMetadata(makeEmptyMetadata()); - - ChunkRange cr1 = ChunkRange(BSON("key" << 0), BSON("key" << 10)); - manager.addRangeToClean(cr1); - - ChunkRange cr2 = manager.getNextRangeToClean(); - ASSERT_EQ(cr1.toString(), cr2.toString()); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/s/migration_chunk_cloner_source.h b/src/mongo/db/s/migration_chunk_cloner_source.h index 04cf9e36df2..cb3b10a8ef8 100644 --- a/src/mongo/db/s/migration_chunk_cloner_source.h +++ b/src/mongo/db/s/migration_chunk_cloner_source.h @@ -88,9 +88,13 @@ public: * This must only be called once and no more methods on the cloner must be used afterwards * regardless of whether it succeeds or not. * + * Returns statistics about the move. These are informational only and should not be + * interpreted by the caller for any means other than reporting. + * * NOTE: Must be called without any locks. */ - virtual Status commitClone(OperationContext* txn) = 0; + + virtual StatusWith<BSONObj> commitClone(OperationContext* opCtx) = 0; /** * Tells the recipient to abort the clone and cleanup any unused data. This method's diff --git a/src/mongo/db/s/migration_chunk_cloner_source_legacy.cpp b/src/mongo/db/s/migration_chunk_cloner_source_legacy.cpp index a11d74ef587..ea83fa83d0d 100644 --- a/src/mongo/db/s/migration_chunk_cloner_source_legacy.cpp +++ b/src/mongo/db/s/migration_chunk_cloner_source_legacy.cpp @@ -75,9 +75,11 @@ bool isInRange(const BSONObj& obj, BSONObj createRequestWithSessionId(StringData commandName, const NamespaceString& nss, - const MigrationSessionId& sessionId) { + const MigrationSessionId& sessionId, + bool waitForSteadyOrDone = false) { BSONObjBuilder builder; builder.append(commandName, nss.ns()); + builder.append("waitForSteadyOrDone", waitForSteadyOrDone); sessionId.append(&builder); return builder.obj(); } @@ -231,13 +233,8 @@ Status MigrationChunkClonerSourceLegacy::awaitUntilCriticalSectionIsAppropriate( int iteration = 0; while ((Date_t::now() - startTime) < maxTimeToWait) { - // Exponential sleep backoff, up to 1024ms. Don't sleep much on the first few iterations, - // since we want empty chunk migrations to be fast. - sleepmillis(1LL << std::min(iteration, 10)); - iteration++; - auto responseStatus = _callRecipient( - createRequestWithSessionId(kRecvChunkStatus, _args.getNss(), _sessionId)); + createRequestWithSessionId(kRecvChunkStatus, _args.getNss(), _sessionId, true)); if (!responseStatus.isOK()) { return {responseStatus.getStatus().code(), str::stream() @@ -247,6 +244,11 @@ Status MigrationChunkClonerSourceLegacy::awaitUntilCriticalSectionIsAppropriate( const BSONObj& res = responseStatus.getValue(); + if (!res["waited"].boolean()) { + sleepmillis(1LL << std::min(iteration, 10)); + } + iteration++; + stdx::lock_guard<stdx::mutex> sl(_mutex); const std::size_t cloneLocsRemaining = _cloneLocs.size(); @@ -306,7 +308,7 @@ Status MigrationChunkClonerSourceLegacy::awaitUntilCriticalSectionIsAppropriate( return {ErrorCodes::ExceededTimeLimit, "Timed out waiting for the cloner to catch up"}; } -Status MigrationChunkClonerSourceLegacy::commitClone(OperationContext* txn) { +StatusWith<BSONObj> MigrationChunkClonerSourceLegacy::commitClone(OperationContext* txn) { invariant(_state == kCloning); invariant(!txn->lockState()->isLocked()); @@ -314,7 +316,7 @@ Status MigrationChunkClonerSourceLegacy::commitClone(OperationContext* txn) { _callRecipient(createRequestWithSessionId(kRecvChunkCommit, _args.getNss(), _sessionId)); if (responseStatus.isOK()) { _cleanup(txn); - return Status::OK(); + return responseStatus; } cancelClone(txn); @@ -576,8 +578,7 @@ Status MigrationChunkClonerSourceLegacy::_storeCurrentLocs(OperationContext* txn if (totalRecs > 0) { avgRecSize = collection->dataSize(txn) / totalRecs; maxRecsWhenFull = _args.getMaxChunkSizeBytes() / avgRecSize; - maxRecsWhenFull = std::min((unsigned long long)(kMaxObjectPerChunk + 1), - 130 * maxRecsWhenFull / 100 /* slack */); + maxRecsWhenFull = 130 * maxRecsWhenFull / 100; // pad some slack } else { avgRecSize = 0; maxRecsWhenFull = kMaxObjectPerChunk + 1; diff --git a/src/mongo/db/s/migration_chunk_cloner_source_legacy.h b/src/mongo/db/s/migration_chunk_cloner_source_legacy.h index c683df2be29..860282a16d0 100644 --- a/src/mongo/db/s/migration_chunk_cloner_source_legacy.h +++ b/src/mongo/db/s/migration_chunk_cloner_source_legacy.h @@ -66,7 +66,7 @@ public: Status awaitUntilCriticalSectionIsAppropriate(OperationContext* txn, Milliseconds maxTimeToWait) override; - Status commitClone(OperationContext* txn) override; + StatusWith<BSONObj> commitClone(OperationContext* txn) override; void cancelClone(OperationContext* txn) override; diff --git a/src/mongo/db/s/migration_destination_manager.cpp b/src/mongo/db/s/migration_destination_manager.cpp index 2e748cf3ca9..1c02208ee27 100644 --- a/src/mongo/db/s/migration_destination_manager.cpp +++ b/src/mongo/db/s/migration_destination_manager.cpp @@ -148,10 +148,14 @@ bool opReplicatedEnough(OperationContext* txn, const repl::OpTime& lastOpApplied, const WriteConcernOptions& writeConcern) { WriteConcernResult writeConcernResult; + writeConcernResult.wTimedOut = false; - Status waitForMajorityWriteConcernStatus = + Status majorityStatus = waitForWriteConcern(txn, lastOpApplied, kMajorityWriteConcern, &writeConcernResult); - if (!waitForMajorityWriteConcernStatus.isOK()) { + if (!majorityStatus.isOK()) { + if (!writeConcernResult.wTimedOut) { + uassertStatusOK(majorityStatus); + } return false; } @@ -159,13 +163,16 @@ bool opReplicatedEnough(OperationContext* txn, // write concerns in case the user's write concern is stronger than majority WriteConcernOptions userWriteConcern(writeConcern); userWriteConcern.wTimeout = -1; + writeConcernResult.wTimedOut = false; - Status waitForUserWriteConcernStatus = + Status userStatus = waitForWriteConcern(txn, lastOpApplied, userWriteConcern, &writeConcernResult); - if (!waitForUserWriteConcernStatus.isOK()) { + if (!userStatus.isOK()) { + if (!writeConcernResult.wTimedOut) { + uassertStatusOK(userStatus); + } return false; } - return true; } @@ -220,6 +227,7 @@ MigrationDestinationManager::State MigrationDestinationManager::getState() const void MigrationDestinationManager::setState(State newState) { stdx::lock_guard<stdx::mutex> sl(_mutex); _state = newState; + _stateChangedCV.notify_all(); } bool MigrationDestinationManager::isActive() const { @@ -231,7 +239,21 @@ bool MigrationDestinationManager::_isActive_inlock() const { return _sessionId.is_initialized(); } -void MigrationDestinationManager::report(BSONObjBuilder& b) { +void MigrationDestinationManager::report(BSONObjBuilder& b, + OperationContext* opCtx, + bool waitForSteadyOrDone) { + if (waitForSteadyOrDone) { + stdx::unique_lock<stdx::mutex> lock(_mutex); + try { + opCtx->waitForConditionOrInterruptFor(_stateChangedCV, lock, Seconds(1), [&]() -> bool { + return _state != READY && _state != CLONE && _state != CATCHUP; + }); + } catch (...) { + // Ignoring this error because this is an optional parameter and we catch timeout + // exceptions later. + } + b.append("waited", true); + } stdx::lock_guard<stdx::mutex> sl(_mutex); b.appendBool("active", _sessionId.is_initialized()); @@ -286,6 +308,7 @@ Status MigrationDestinationManager::start(const NamespaceString& nss, invariant(!_scopedRegisterReceiveChunk); _state = READY; + _stateChangedCV.notify_all(); _errmsg = ""; _nss = nss; @@ -336,6 +359,7 @@ bool MigrationDestinationManager::abort(const MigrationSessionId& sessionId) { } _state = ABORT; + _stateChangedCV.notify_all(); _errmsg = "aborted"; return true; @@ -344,6 +368,7 @@ bool MigrationDestinationManager::abort(const MigrationSessionId& sessionId) { void MigrationDestinationManager::abortWithoutSessionIdCheck() { stdx::lock_guard<stdx::mutex> sl(_mutex); _state = ABORT; + _stateChangedCV.notify_all(); _errmsg = "aborted without session id check"; } @@ -368,6 +393,7 @@ bool MigrationDestinationManager::startCommit(const MigrationSessionId& sessionI } _state = COMMIT_START; + _stateChangedCV.notify_all(); const auto deadline = Date_t::now() + Seconds(30); @@ -375,6 +401,8 @@ bool MigrationDestinationManager::startCommit(const MigrationSessionId& sessionI if (stdx::cv_status::timeout == _isActiveCV.wait_until(lock, deadline.toSystemTimePoint())) { _state = FAIL; + _stateChangedCV.notify_all(); + log() << "startCommit never finished!" << migrateLog; return false; } diff --git a/src/mongo/db/s/migration_destination_manager.h b/src/mongo/db/s/migration_destination_manager.h index 0b16202d55e..4bd269f37fd 100644 --- a/src/mongo/db/s/migration_destination_manager.h +++ b/src/mongo/db/s/migration_destination_manager.h @@ -77,7 +77,7 @@ public: /** * Reports the state of the migration manager as a BSON document. */ - void report(BSONObjBuilder& b); + void report(BSONObjBuilder& b, OperationContext* opCtx, bool waitForSteadyOrDone); /** * Returns a report on the active migration, if the migration is active. Otherwise return an @@ -222,6 +222,9 @@ private: State _state{READY}; std::string _errmsg; + + // Condition variable, which is signalled every time the state of the migration changes. + stdx::condition_variable _stateChangedCV; }; } // namespace mongo diff --git a/src/mongo/db/s/migration_destination_manager_legacy_commands.cpp b/src/mongo/db/s/migration_destination_manager_legacy_commands.cpp index a20db670259..ac9752fd96d 100644 --- a/src/mongo/db/s/migration_destination_manager_legacy_commands.cpp +++ b/src/mongo/db/s/migration_destination_manager_legacy_commands.cpp @@ -220,7 +220,9 @@ public: int, string& errmsg, BSONObjBuilder& result) { - ShardingState::get(txn)->migrationDestinationManager()->report(result); + bool waitForSteadyOrDone = cmdObj["waitForSteadyOrDone"].boolean(); + ShardingState::get(txn)->migrationDestinationManager()->report( + result, txn, waitForSteadyOrDone); return true; } @@ -261,12 +263,13 @@ public: int, string& errmsg, BSONObjBuilder& result) { + const MigrationSessionId migrationSessionid( uassertStatusOK(MigrationSessionId::extractFromBSON(cmdObj))); const bool ok = ShardingState::get(txn)->migrationDestinationManager()->startCommit(migrationSessionid); - ShardingState::get(txn)->migrationDestinationManager()->report(result); + ShardingState::get(txn)->migrationDestinationManager()->report(result, txn, false); return ok; } @@ -313,11 +316,11 @@ public: if (migrationSessionIdStatus.isOK()) { const bool ok = mdm->abort(migrationSessionIdStatus.getValue()); - mdm->report(result); + mdm->report(result, txn, false); return ok; } else if (migrationSessionIdStatus == ErrorCodes::NoSuchKey) { mdm->abortWithoutSessionIdCheck(); - mdm->report(result); + mdm->report(result, txn, false); return true; } diff --git a/src/mongo/db/s/migration_source_manager.cpp b/src/mongo/db/s/migration_source_manager.cpp index 0ad841ea56f..b70617deee3 100644 --- a/src/mongo/db/s/migration_source_manager.cpp +++ b/src/mongo/db/s/migration_source_manager.cpp @@ -268,18 +268,18 @@ Status MigrationSourceManager::commitChunkOnRecipient(OperationContext* txn) { auto scopedGuard = MakeGuard([&] { cleanupOnError(txn); }); // Tell the recipient shard to fetch the latest changes. - Status commitCloneStatus = _cloneDriver->commitClone(txn); + auto commitCloneStatus = _cloneDriver->commitClone(txn); if (MONGO_FAIL_POINT(failMigrationCommit) && commitCloneStatus.isOK()) { commitCloneStatus = {ErrorCodes::InternalError, "Failing _recvChunkCommit due to failpoint."}; } - if (!commitCloneStatus.isOK()) { - return {commitCloneStatus.code(), - str::stream() << "commit clone failed due to " << commitCloneStatus.toString()}; + return commitCloneStatus.getStatus(); } + _recipientCloneCounts = commitCloneStatus.getValue()["counts"].Obj().getOwned(); + _state = kCloneCompleted; scopedGuard.Dismiss(); return Status::OK(); @@ -332,9 +332,13 @@ Status MigrationSourceManager::commitChunkMetadataOnConfig(OperationContext* txn ErrorCodes::InternalError, "Failpoint 'migrationCommitNetworkError' generated error"); } - const Status migrationCommitStatus = - (commitChunkMigrationResponse.isOK() ? commitChunkMigrationResponse.getValue().commandStatus - : commitChunkMigrationResponse.getStatus()); + Status migrationCommitStatus = commitChunkMigrationResponse.getStatus(); + if (migrationCommitStatus.isOK()) { + migrationCommitStatus = commitChunkMigrationResponse.getValue().commandStatus; + if (migrationCommitStatus.isOK()) { + migrationCommitStatus = commitChunkMigrationResponse.getValue().writeConcernStatus; + } + } if (!migrationCommitStatus.isOK()) { // Need to get the latest optime in case the refresh request goes to a secondary -- @@ -431,7 +435,9 @@ Status MigrationSourceManager::commitChunkMetadataOnConfig(OperationContext* txn << "from" << _args.getFromShardId() << "to" - << _args.getToShardId()), + << _args.getToShardId() + << "counts" + << _recipientCloneCounts), ShardingCatalogClient::kMajorityWriteConcern); return Status::OK(); diff --git a/src/mongo/db/s/migration_source_manager.h b/src/mongo/db/s/migration_source_manager.h index cb5ce4be792..27d4480a665 100644 --- a/src/mongo/db/s/migration_source_manager.h +++ b/src/mongo/db/s/migration_source_manager.h @@ -233,6 +233,9 @@ private: // callers don't have to hold collection lock in order to wait on it. Available after the // critical section stage has completed. std::shared_ptr<Notification<void>> _critSecSignal; + + // The statistics about a chunk migration to be included in moveChunk.commit + BSONObj _recipientCloneCounts; }; } // namespace mongo diff --git a/src/mongo/db/s/sharding_state.cpp b/src/mongo/db/s/sharding_state.cpp index e264c82b57f..02c75e287fe 100644 --- a/src/mongo/db/s/sharding_state.cpp +++ b/src/mongo/db/s/sharding_state.cpp @@ -117,8 +117,7 @@ const std::set<std::string> ShardingState::_commandsThatInitializeShardingAwaren ShardingState::ShardingState() : _initializationState(static_cast<uint32_t>(InitializationState::kNew)), _initializationStatus(Status(ErrorCodes::InternalError, "Uninitialized value")), - _globalInit(&initializeGlobalShardingStateForMongod), - _scheduleWorkFn([](NamespaceString nss) {}) {} + _globalInit(&initializeGlobalShardingStateForMongod) {} ShardingState::~ShardingState() = default; @@ -222,14 +221,6 @@ void ShardingState::setGlobalInitMethodForTest(GlobalInitFunc func) { _globalInit = func; } -void ShardingState::setScheduleCleanupFunctionForTest(RangeDeleterCleanupNotificationFunc fn) { - _scheduleWorkFn = fn; -} - -void ShardingState::scheduleCleanup(const NamespaceString& nss) { - _scheduleWorkFn(nss); -} - Status ShardingState::onStaleShardVersion(OperationContext* txn, const NamespaceString& nss, const ChunkVersion& expectedVersion) { @@ -416,8 +407,6 @@ Status ShardingState::initializeFromShardIdentity(OperationContext* txn, _shardName = shardIdentity.getShardName(); _clusterId = shardIdentity.getClusterId(); - _initializeRangeDeleterTaskExecutor(); - return status; } catch (const DBException& ex) { auto errorStatus = ex.toStatus(); @@ -446,8 +435,6 @@ void ShardingState::_initializeImpl(ConnectionString configSvr, string shardName &ShardRegistry::replicaSetChangeShardRegistryUpdateHook); ReplicaSetMonitor::setAsynchronousConfigChangeHook(&updateShardIdentityConfigStringCB); - _initializeRangeDeleterTaskExecutor(); - _shardName = shardName; } @@ -626,47 +613,76 @@ ChunkVersion ShardingState::_refreshMetadata(OperationContext* txn, const Namesp << " before shard name has been set", shardId.isValid()); - auto newCollectionMetadata = [&]() -> std::unique_ptr<CollectionMetadata> { - auto const catalogCache = Grid::get(txn)->catalogCache(); - catalogCache->invalidateShardedCollection(nss); + auto const catalogCache = Grid::get(txn)->catalogCache(); + catalogCache->invalidateShardedCollection(nss); - const auto routingInfo = uassertStatusOK(catalogCache->getCollectionRoutingInfo(txn, nss)); - const auto cm = routingInfo.cm(); - if (!cm) { - return nullptr; - } + auto routingInfo = uassertStatusOK(catalogCache->getCollectionRoutingInfo(txn, nss)); + const auto cm = routingInfo.cm(); - RangeMap shardChunksMap = - SimpleBSONObjComparator::kInstance.makeBSONObjIndexedMap<CachedChunkInfo>(); + if (!cm) { + // No chunk manager, so unsharded. - for (const auto& chunkMapEntry : cm->chunkMap()) { - const auto& chunk = chunkMapEntry.second; + // Exclusive collection lock needed since we're now changing the metadata + ScopedTransaction transaction(txn, MODE_IX); + AutoGetCollection autoColl(txn, nss, MODE_IX, MODE_X); - if (chunk->getShardId() != shardId) - continue; + auto css = CollectionShardingState::get(txn, nss); + css->refreshMetadata(txn, nullptr); - shardChunksMap.emplace_hint(shardChunksMap.end(), - chunk->getMin(), - CachedChunkInfo(chunk->getMax(), chunk->getLastmod())); - } + return ChunkVersion::UNSHARDED(); + } - return stdx::make_unique<CollectionMetadata>(cm->getShardKeyPattern().toBSON(), - cm->getVersion(), - cm->getVersion(shardId), - std::move(shardChunksMap)); - }(); + { + AutoGetCollection autoColl(txn, nss, MODE_IS); + auto css = CollectionShardingState::get(txn, nss); + + // We already have newer version + if (css->getMetadata() && + css->getMetadata()->getCollVersion().epoch() == cm->getVersion().epoch() && + css->getMetadata()->getCollVersion() >= cm->getVersion()) { + LOG(1) << "Skipping refresh of metadata for " << nss << " " + << css->getMetadata()->getCollVersion() << " with an older " << cm->getVersion(); + return css->getMetadata()->getShardVersion(); + } + } // Exclusive collection lock needed since we're now changing the metadata ScopedTransaction transaction(txn, MODE_IX); AutoGetCollection autoColl(txn, nss, MODE_IX, MODE_X); auto css = CollectionShardingState::get(txn, nss); - css->refreshMetadata(txn, std::move(newCollectionMetadata)); - if (!css->getMetadata()) { - return ChunkVersion::UNSHARDED(); + // We already have newer version + if (css->getMetadata() && + css->getMetadata()->getCollVersion().epoch() == cm->getVersion().epoch() && + css->getMetadata()->getCollVersion() >= cm->getVersion()) { + LOG(1) << "Skipping refresh of metadata for " << nss << " " + << css->getMetadata()->getCollVersion() << " with an older " << cm->getVersion(); + return css->getMetadata()->getShardVersion(); + } + + RangeMap shardChunksMap = + SimpleBSONObjComparator::kInstance.makeBSONObjIndexedMap<CachedChunkInfo>(); + + for (const auto& chunkMapEntry : cm->chunkMap()) { + const auto& chunk = chunkMapEntry.second; + + if (chunk->getShardId() != shardId) + continue; + + shardChunksMap.emplace_hint(shardChunksMap.end(), + chunk->getMin(), + CachedChunkInfo(chunk->getMax(), chunk->getLastmod())); } + std::unique_ptr<CollectionMetadata> newCollectionMetadata = + stdx::make_unique<CollectionMetadata>(cm->getShardKeyPattern().toBSON(), + cm->getVersion(), + cm->getVersion(shardId), + std::move(shardChunksMap)); + + css->refreshMetadata(txn, std::move(newCollectionMetadata)); + return css->getMetadata()->getShardVersion(); } @@ -756,19 +772,6 @@ Status ShardingState::updateShardIdentityConfigString(OperationContext* txn, return Status::OK(); } -void ShardingState::_initializeRangeDeleterTaskExecutor() { - invariant(!_rangeDeleterTaskExecutor); - auto net = - executor::makeNetworkInterface("NetworkInterfaceCollectionRangeDeleter-TaskExecutor"); - auto netPtr = net.get(); - _rangeDeleterTaskExecutor = stdx::make_unique<executor::ThreadPoolTaskExecutor>( - stdx::make_unique<executor::NetworkInterfaceThreadPool>(netPtr), std::move(net)); -} - -executor::ThreadPoolTaskExecutor* ShardingState::getRangeDeleterTaskExecutor() { - return _rangeDeleterTaskExecutor.get(); -} - /** * Global free function. */ diff --git a/src/mongo/db/s/sharding_state.h b/src/mongo/db/s/sharding_state.h index e53fee63515..f9e5a507446 100644 --- a/src/mongo/db/s/sharding_state.h +++ b/src/mongo/db/s/sharding_state.h @@ -35,7 +35,6 @@ #include "mongo/bson/oid.h" #include "mongo/db/namespace_string.h" #include "mongo/db/s/active_migrations_registry.h" -#include "mongo/db/s/collection_range_deleter.h" #include "mongo/db/s/migration_destination_manager.h" #include "mongo/executor/task_executor.h" #include "mongo/executor/thread_pool_task_executor.h" @@ -236,24 +235,6 @@ public: void setGlobalInitMethodForTest(GlobalInitFunc func); /** - * Schedules for the range to clean of the given namespace to be deleted. - * Behavior can be modified through setScheduleCleanupFunctionForTest. - */ - void scheduleCleanup(const NamespaceString& nss); - - /** - * Returns a pointer to the collection range deleter task executor. - */ - executor::ThreadPoolTaskExecutor* getRangeDeleterTaskExecutor(); - - /** - * Sets the function used by scheduleWorkOnRangeDeleterTaskExecutor to - * schedule work. Used for mocking the executor for testing. See the ShardingState - * for the default implementation of _scheduleWorkFn. - */ - void setScheduleCleanupFunctionForTest(RangeDeleterCleanupNotificationFunc fn); - - /** * If started with --shardsvr, initializes sharding awareness from the shardIdentity document * on disk, if there is one. * If started with --shardsvr in queryableBackupMode, initializes sharding awareness from the @@ -348,9 +329,6 @@ private: */ ChunkVersion _refreshMetadata(OperationContext* opCtx, const NamespaceString& nss); - // Initializes a TaskExecutor for cleaning up orphaned ranges - void _initializeRangeDeleterTaskExecutor(); - // Manages the state of the migration recipient shard MigrationDestinationManager _migrationDestManager; @@ -386,13 +364,6 @@ private: // Function for initializing the external sharding state components not owned here. GlobalInitFunc _globalInit; - - // Function for scheduling work on the _rangeDeleterTaskExecutor. - // Used in call to scheduleCleanup(NamespaceString). - RangeDeleterCleanupNotificationFunc _scheduleWorkFn; - - // Task executor for the collection range deleter. - std::unique_ptr<executor::ThreadPoolTaskExecutor> _rangeDeleterTaskExecutor; }; } // namespace mongo diff --git a/src/mongo/db/sorter/SConscript b/src/mongo/db/sorter/SConscript index 959f6cdb919..1ceb05088af 100644 --- a/src/mongo/db/sorter/SConscript +++ b/src/mongo/db/sorter/SConscript @@ -5,6 +5,6 @@ sorterEnv.InjectThirdPartyIncludePaths(libraries=['snappy']) sorterEnv.CppUnitTest('sorter_test', 'sorter_test.cpp', LIBDEPS=['$BUILD_DIR/mongo/db/service_context', - '$BUILD_DIR/mongo/db/storage/wiredtiger/storage_wiredtiger_customization_hooks', + '$BUILD_DIR/mongo/db/storage/encryption_hooks', '$BUILD_DIR/mongo/db/storage/storage_options', '$BUILD_DIR/third_party/shim_snappy']) diff --git a/src/mongo/db/sorter/sorter.cpp b/src/mongo/db/sorter/sorter.cpp index 7f6e8866562..0165e0adccc 100644 --- a/src/mongo/db/sorter/sorter.cpp +++ b/src/mongo/db/sorter/sorter.cpp @@ -55,8 +55,8 @@ #include "mongo/config.h" #include "mongo/db/jsobj.h" #include "mongo/db/service_context.h" +#include "mongo/db/storage/encryption_hooks.h" #include "mongo/db/storage/storage_options.h" -#include "mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.h" #include "mongo/platform/atomic_word.h" #include "mongo/s/mongos_options.h" #include "mongo/util/assert_util.h" @@ -226,15 +226,16 @@ private: read(_buffer.get(), blockSize); massert(16816, "file too short?", !_done); - auto hooks = WiredTigerCustomizationHooks::get(getGlobalServiceContext()); - if (hooks->enabled()) { + auto encryptionHooks = EncryptionHooks::get(getGlobalServiceContext()); + if (encryptionHooks->enabled()) { std::unique_ptr<char[]> out(new char[blockSize]); size_t outLen; - Status status = hooks->unprotectTmpData(reinterpret_cast<uint8_t*>(_buffer.get()), - blockSize, - reinterpret_cast<uint8_t*>(out.get()), - blockSize, - &outLen); + Status status = + encryptionHooks->unprotectTmpData(reinterpret_cast<uint8_t*>(_buffer.get()), + blockSize, + reinterpret_cast<uint8_t*>(out.get()), + blockSize, + &outLen); massert(28841, str::stream() << "Failed to unprotect data: " << status.toString(), status.isOK()); @@ -884,16 +885,16 @@ void SortedFileWriter<Key, Value>::spill() { } std::unique_ptr<char[]> out; - auto hooks = WiredTigerCustomizationHooks::get(getGlobalServiceContext()); - if (hooks->enabled()) { - size_t protectedSizeMax = size + hooks->additionalBytesForProtectedBuffer(); + auto encryptionHooks = EncryptionHooks::get(getGlobalServiceContext()); + if (encryptionHooks->enabled()) { + size_t protectedSizeMax = size + encryptionHooks->additionalBytesForProtectedBuffer(); out.reset(new char[protectedSizeMax]); size_t resultLen; - Status status = hooks->protectTmpData(reinterpret_cast<const uint8_t*>(outBuffer), - size, - reinterpret_cast<uint8_t*>(out.get()), - protectedSizeMax, - &resultLen); + Status status = encryptionHooks->protectTmpData(reinterpret_cast<const uint8_t*>(outBuffer), + size, + reinterpret_cast<uint8_t*>(out.get()), + protectedSizeMax, + &resultLen); massert(28842, str::stream() << "Failed to compress data: " << status.toString(), status.isOK()); diff --git a/src/mongo/db/stats/range_deleter_server_status.cpp b/src/mongo/db/stats/range_deleter_server_status.cpp index 817ffa444e4..e37143342f0 100644 --- a/src/mongo/db/stats/range_deleter_server_status.cpp +++ b/src/mongo/db/stats/range_deleter_server_status.cpp @@ -84,6 +84,11 @@ public: entryBuilder.append("deleteStart", (*it)->deleteStartTS); entryBuilder.append("deleteEnd", (*it)->deleteEndTS); + const auto waitForReplDurationMs = + durationCount<Milliseconds>((*it)->waitForReplDurationMs); + if (waitForReplDurationMs > 0) { + entryBuilder.append("waitForReplDurationMs", waitForReplDurationMs); + } if ((*it)->waitForReplEndTS > Date_t()) { entryBuilder.append("waitForReplStart", (*it)->waitForReplStartTS); entryBuilder.append("waitForReplEnd", (*it)->waitForReplEndTS); diff --git a/src/mongo/db/storage/SConscript b/src/mongo/db/storage/SConscript index d65046ed6fa..e887882df87 100644 --- a/src/mongo/db/storage/SConscript +++ b/src/mongo/db/storage/SConscript @@ -11,7 +11,6 @@ env.SConscript( ], ) - env.Library( target='journal_listener', source=[ @@ -41,7 +40,6 @@ env.Library( ], ) - env.Library( target='bson_collection_catalog_entry', source=[ @@ -75,6 +73,19 @@ env.Library( ) env.Library( + target='encryption_hooks', + source= [ + 'encryption_hooks.cpp', + ], + LIBDEPS= ['$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/db/service_context'], + PROGDEPS_DEPENDENTS=[ + '$BUILD_DIR/mongo/mongod', + '$BUILD_DIR/mongo/mongos', + ], + ) + +env.Library( target='storage_options', source=[ 'storage_options.cpp', diff --git a/src/mongo/db/storage/encryption_hooks.cpp b/src/mongo/db/storage/encryption_hooks.cpp new file mode 100644 index 00000000000..9ee9a317dbe --- /dev/null +++ b/src/mongo/db/storage/encryption_hooks.cpp @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/storage/encryption_hooks.h" + +#include <boost/filesystem/path.hpp> + +#include "mongo/base/init.h" +#include "mongo/db/service_context.h" +#include "mongo/db/storage/data_protector.h" +#include "mongo/stdx/memory.h" + +namespace mongo { + +/* Make a EncryptionHooks pointer a decoration on the global ServiceContext */ +MONGO_INITIALIZER_WITH_PREREQUISITES(SetEncryptionHooks, ("SetGlobalEnvironment")) +(InitializerContext* context) { + auto encryptionHooks = stdx::make_unique<EncryptionHooks>(); + EncryptionHooks::set(getGlobalServiceContext(), std::move(encryptionHooks)); + + return Status::OK(); +} + +namespace { +const auto getEncryptionHooks = + ServiceContext::declareDecoration<std::unique_ptr<EncryptionHooks>>(); +} // namespace + +void EncryptionHooks::set(ServiceContext* service, std::unique_ptr<EncryptionHooks> custHooks) { + auto& hooks = getEncryptionHooks(service); + invariant(custHooks); + hooks = std::move(custHooks); +} + +EncryptionHooks* EncryptionHooks::get(ServiceContext* service) { + return getEncryptionHooks(service).get(); +} + +EncryptionHooks::~EncryptionHooks() {} + +bool EncryptionHooks::enabled() const { + return false; +} + +bool EncryptionHooks::restartRequired() { + return false; +} + +std::unique_ptr<DataProtector> EncryptionHooks::getDataProtector() { + return std::unique_ptr<DataProtector>(); +} + +boost::filesystem::path EncryptionHooks::getProtectedPathSuffix() { + return ""; +} + +Status EncryptionHooks::protectTmpData( + const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen, size_t* resultLen) { + return Status(ErrorCodes::InternalError, + "Encryption hooks must be enabled to use preprocessTmpData."); +} + +Status EncryptionHooks::unprotectTmpData( + const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen, size_t* resultLen) { + return Status(ErrorCodes::InternalError, + "Encryption hooks must be enabled to use postprocessTmpData."); +} +} // namespace mongo diff --git a/src/mongo/db/storage/encryption_hooks.h b/src/mongo/db/storage/encryption_hooks.h new file mode 100644 index 00000000000..e1c9d553a10 --- /dev/null +++ b/src/mongo/db/storage/encryption_hooks.h @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#pragma once + +#include <memory> +#include <string> + +#include "mongo/base/disallow_copying.h" +#include "mongo/db/jsobj.h" + +namespace boost { +namespace filesystem { +class path; +} // namespace filesystem +} // namespace boost + +namespace mongo { +class DataProtector; +class ServiceContext; + +class EncryptionHooks { +public: + static void set(ServiceContext* service, std::unique_ptr<EncryptionHooks> custHooks); + + static EncryptionHooks* get(ServiceContext* service); + + virtual ~EncryptionHooks(); + + /** + * Returns true if the encryption hooks are enabled. + */ + virtual bool enabled() const; + + /** + * Perform any encryption engine initialization/sanity checking that needs to happen after + * storage engine initialization but before the server starts accepting incoming connections. + * + * Returns true if the server needs to be rebooted because of configuration changes. + */ + virtual bool restartRequired(); + + /** + * Returns the maximum size addition when doing transforming temp data. + */ + size_t additionalBytesForProtectedBuffer() { + return 33; + } + + /** + * Get the data protector object + */ + virtual std::unique_ptr<DataProtector> getDataProtector(); + + /** + * Get an implementation specific path suffix to tag files with + */ + virtual boost::filesystem::path getProtectedPathSuffix(); + + /** + * Transform temp data to non-readable form before writing it to disk. + */ + virtual Status protectTmpData( + const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen, size_t* resultLen); + + /** + * Tranforms temp data back to readable form, after reading from disk. + */ + virtual Status unprotectTmpData( + const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen, size_t* resultLen); +}; + +} // namespace mongo diff --git a/src/mongo/db/storage/ephemeral_for_test/ephemeral_for_test_record_store.cpp b/src/mongo/db/storage/ephemeral_for_test/ephemeral_for_test_record_store.cpp index a537a9c63c6..8c816d39f31 100644 --- a/src/mongo/db/storage/ephemeral_for_test/ephemeral_for_test_record_store.cpp +++ b/src/mongo/db/storage/ephemeral_for_test/ephemeral_for_test_record_store.cpp @@ -556,8 +556,15 @@ void EphemeralForTestRecordStore::temp_cappedTruncateAfter(OperationContext* txn Records::iterator it = inclusive ? _data->records.lower_bound(end) : _data->records.upper_bound(end); while (it != _data->records.end()) { - txn->recoveryUnit()->registerChange(new RemoveChange(txn, _data, it->first, it->second)); - _data->dataSize -= it->second.size; + RecordId id = it->first; + EphemeralForTestRecord record = it->second; + + if (_cappedCallback) { + uassertStatusOK(_cappedCallback->aboutToDeleteCapped(txn, id, record.toRecordData())); + } + + txn->recoveryUnit()->registerChange(new RemoveChange(txn, _data, id, record)); + _data->dataSize -= record.size; _data->records.erase(it++); } } diff --git a/src/mongo/db/storage/key_string.cpp b/src/mongo/db/storage/key_string.cpp index 624c87a2404..fc580ca111d 100644 --- a/src/mongo/db/storage/key_string.cpp +++ b/src/mongo/db/storage/key_string.cpp @@ -1366,7 +1366,8 @@ void toBsonValue(uint8_t ctype, Decimal128 dec(Decimal128::Value{lowbits, highbits}); if (isNegative) dec = dec.negate(); - dec = adjustDecimalExponent(typeBits, dec); + if (dec.isFinite()) + dec = adjustDecimalExponent(typeBits, dec); *stream << dec; break; } @@ -1385,7 +1386,8 @@ void toBsonValue(uint8_t ctype, Decimal128 dec(bin, Decimal128::kRoundTo34Digits, roundAwayFromZero); if (hasDecimalContinuation) dec = readDecimalContinuation(reader, inverted, dec); - dec = adjustDecimalExponent(typeBits, dec); + if (dec.isFinite()) + dec = adjustDecimalExponent(typeBits, dec); *stream << dec; } break; diff --git a/src/mongo/db/storage/key_string_test.cpp b/src/mongo/db/storage/key_string_test.cpp index b29d1b01da8..46a11c2592a 100644 --- a/src/mongo/db/storage/key_string_test.cpp +++ b/src/mongo/db/storage/key_string_test.cpp @@ -596,6 +596,15 @@ const std::vector<BSONObj>& getInterestingElements(KeyString::Version version) { // Something that needs multiple bytes of typeBits elements.push_back(BSON("" << BSON_ARRAY("" << BSONSymbol("") << 0 << 0ll << 0.0 << -0.0))); + if (version != KeyString::Version::V0) { + // Something with exceptional typeBits for Decimal + elements.push_back( + BSON("" << BSON_ARRAY("" << BSONSymbol("") << Decimal128::kNegativeInfinity + << Decimal128::kPositiveInfinity + << Decimal128::kPositiveNaN + << Decimal128("0.0000000") + << Decimal128("-0E1000")))); + } // // Interesting numeric cases @@ -605,6 +614,11 @@ const std::vector<BSONObj>& getInterestingElements(KeyString::Version version) { elements.push_back(BSON("" << 0ll)); elements.push_back(BSON("" << 0.0)); elements.push_back(BSON("" << -0.0)); + if (version != KeyString::Version::V0) { + Decimal128("0.0.0000000"); + Decimal128("-0E1000"); + } + elements.push_back(BSON("" << std::numeric_limits<double>::quiet_NaN())); elements.push_back(BSON("" << std::numeric_limits<double>::infinity())); elements.push_back(BSON("" << -std::numeric_limits<double>::infinity())); @@ -740,6 +754,11 @@ const std::vector<BSONObj>& getInterestingElements(KeyString::Version version) { elements.push_back(BSON("" << Decimal128("4.940656458412465441765687928682214E-324"))); elements.push_back(BSON("" << Decimal128("-4.940656458412465441765687928682214E-324"))); elements.push_back(BSON("" << Decimal128("-4.940656458412465441765687928682213E-324"))); + + // Non-finite values. Note: can't roundtrip negative NaNs, so not testing here. + elements.push_back(BSON("" << Decimal128::kPositiveNaN)); + elements.push_back(BSON("" << Decimal128::kNegativeInfinity)); + elements.push_back(BSON("" << Decimal128::kPositiveInfinity)); } // Tricky double precision number for binary/decimal conversion: very close to a decimal @@ -823,8 +842,36 @@ void testPermutation(KeyString::Version version, } } +namespace { +std::random_device rd; +std::mt19937_64 seedGen(rd()); + +// To be used by perf test for seeding, so that the entire test is repeatable in case of error. +unsigned newSeed() { + unsigned int seed = seedGen(); // Replace by the reported number to repeat test execution. + log() << "Initializing random number generator using seed " << seed; + return seed; +}; + +std::vector<BSONObj> thinElements(std::vector<BSONObj> elements, + unsigned seed, + size_t maxElements) { + std::mt19937_64 gen(seed); + + if (elements.size() <= maxElements) + return elements; + + log() << "only keeping " << maxElements << " of " << elements.size() + << " elements using random selection"; + std::shuffle(elements.begin(), elements.end(), gen); + elements.resize(maxElements); + return elements; +} +} // namespace + + TEST_F(KeyStringTest, AllPermCompare) { - const std::vector<BSONObj>& elements = getInterestingElements(version); + std::vector<BSONObj> elements = getInterestingElements(version); for (size_t i = 0; i < elements.size(); i++) { const BSONObj& o = elements[i]; @@ -839,19 +886,23 @@ TEST_F(KeyStringTest, AllPermCompare) { } TEST_F(KeyStringTest, AllPerm2Compare) { -#if !defined(MONGO_CONFIG_OPTIMIZED_BUILD) - log() << "\t\t\tskipping permutation testing on non-optimized build"; - return; -#endif + std::vector<BSONObj> baseElements = getInterestingElements(version); + auto seed = newSeed(); - const std::vector<BSONObj>& baseElements = getInterestingElements(version); + // Select only a small subset of elements, as the combination is quadratic. + // We want to select two subsets independently, so all combinations will get tested eventually. + // kMaxPermElements is the desired number of elements to pass to testPermutation. + const size_t kMaxPermElements = kDebugBuild ? 100000 : 500000; + size_t maxElements = sqrt(kMaxPermElements); + auto firstElements = thinElements(baseElements, seed, maxElements); + auto secondElements = thinElements(baseElements, seed + 1, maxElements); std::vector<BSONObj> elements; - for (size_t i = 0; i < baseElements.size(); i++) { - for (size_t j = 0; j < baseElements.size(); j++) { + for (size_t i = 0; i < firstElements.size(); i++) { + for (size_t j = 0; j < secondElements.size(); j++) { BSONObjBuilder b; - b.appendElements(baseElements[i]); - b.appendElements(baseElements[j]); + b.appendElements(firstElements[i]); + b.appendElements(secondElements[j]); BSONObj o = b.obj(); elements.push_back(o); } @@ -927,6 +978,27 @@ TEST_F(KeyStringTest, NaNs) { ASSERT(std::isnan(toBson(ks2a, ONE_ASCENDING)[""].Double())); ASSERT(std::isnan(toBson(ks1d, ONE_DESCENDING)[""].Double())); ASSERT(std::isnan(toBson(ks2d, ONE_DESCENDING)[""].Double())); + + if (version == KeyString::Version::V0) + return; + + const auto nan3 = Decimal128::kPositiveNaN; + const auto nan4 = Decimal128::kNegativeNaN; + // Since we only output a single NaN, we can only do ROUNDTRIP testing for nan1. + ROUNDTRIP(version, BSON("" << nan3)); + const KeyString ks3a(version, BSON("" << nan3), ONE_ASCENDING); + const KeyString ks3d(version, BSON("" << nan3), ONE_DESCENDING); + + const KeyString ks4a(version, BSON("" << nan4), ONE_ASCENDING); + const KeyString ks4d(version, BSON("" << nan4), ONE_DESCENDING); + + ASSERT_EQ(ks1a, ks4a); + ASSERT_EQ(ks1d, ks4d); + + ASSERT(toBson(ks3a, ONE_ASCENDING)[""].Decimal().isNaN()); + ASSERT(toBson(ks4a, ONE_ASCENDING)[""].Decimal().isNaN()); + ASSERT(toBson(ks3d, ONE_DESCENDING)[""].Decimal().isNaN()); + ASSERT(toBson(ks4d, ONE_DESCENDING)[""].Decimal().isNaN()); } TEST_F(KeyStringTest, NumberOrderLots) { std::vector<BSONObj> numbers; @@ -1074,16 +1146,6 @@ const uint64_t kMinPerfMicros = 20 * 1000; const uint64_t kMinPerfSamples = 50 * 1000; typedef std::vector<BSONObj> Numbers; -std::random_device rd; -std::mt19937 seedGen(rd()); - -// To be used by perf test for seeding, so that the entire test is repeatable in case of error. -unsigned newSeed() { - unsigned int seed = seedGen(); // Replace by the reported number to repeat test execution. - log() << "Initializing random number generator using seed " << seed; - return seed; -}; - /** * Evaluates ROUNDTRIP on all items in Numbers a sufficient number of times to take at least * kMinPerfMicros microseconds. Logs the elapsed time per ROUNDTRIP evaluation. diff --git a/src/mongo/db/storage/mmap_v1/mmap_v1_engine.cpp b/src/mongo/db/storage/mmap_v1/mmap_v1_engine.cpp index e185afe03c7..71b3ada3ed4 100644 --- a/src/mongo/db/storage/mmap_v1/mmap_v1_engine.cpp +++ b/src/mongo/db/storage/mmap_v1/mmap_v1_engine.cpp @@ -36,6 +36,10 @@ #include <boost/filesystem/path.hpp> #include <fstream> +#ifdef __linux__ +#include <sys/sysmacros.h> +#endif + #include "mongo/db/mongod_options.h" #include "mongo/db/storage/mmap_v1/data_file_sync.h" #include "mongo/db/storage/mmap_v1/dur.h" diff --git a/src/mongo/db/storage/wiredtiger/SConscript b/src/mongo/db/storage/wiredtiger/SConscript index 9d2a8fc8035..42eb3b4d41d 100644 --- a/src/mongo/db/storage/wiredtiger/SConscript +++ b/src/mongo/db/storage/wiredtiger/SConscript @@ -82,6 +82,7 @@ if wiredtiger: ], LIBDEPS=['storage_wiredtiger_core', 'storage_wiredtiger_customization_hooks', + '$BUILD_DIR/mongo/db/concurrency/lock_manager', '$BUILD_DIR/mongo/db/storage/kv/kv_engine', '$BUILD_DIR/mongo/db/storage/storage_engine_lock_file', '$BUILD_DIR/mongo/db/storage/storage_engine_metadata', diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.cpp index d14b33de5f7..e40d3a58edb 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.cpp @@ -31,12 +31,9 @@ #include "mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.h" -#include <boost/filesystem/path.hpp> - #include "mongo/base/init.h" #include "mongo/base/string_data.h" #include "mongo/db/service_context.h" -#include "mongo/db/storage/data_protector.h" #include "mongo/stdx/memory.h" namespace mongo { @@ -44,7 +41,7 @@ namespace mongo { /* Make a WiredTigerCustomizationHooks pointer a decoration on the global ServiceContext */ MONGO_INITIALIZER_WITH_PREREQUISITES(SetWiredTigerCustomizationHooks, ("SetGlobalEnvironment")) (InitializerContext* context) { - auto customizationHooks = stdx::make_unique<EmptyWiredTigerCustomizationHooks>(); + auto customizationHooks = stdx::make_unique<WiredTigerCustomizationHooks>(); WiredTigerCustomizationHooks::set(getGlobalServiceContext(), std::move(customizationHooks)); return Status::OK(); @@ -66,37 +63,14 @@ WiredTigerCustomizationHooks* WiredTigerCustomizationHooks::get(ServiceContext* return getCustomizationHooks(service).get(); } -EmptyWiredTigerCustomizationHooks::~EmptyWiredTigerCustomizationHooks() {} - -bool EmptyWiredTigerCustomizationHooks::enabled() const { - return false; -} +WiredTigerCustomizationHooks::~WiredTigerCustomizationHooks() {} -bool EmptyWiredTigerCustomizationHooks::restartRequired() { +bool WiredTigerCustomizationHooks::enabled() const { return false; } -std::string EmptyWiredTigerCustomizationHooks::getTableCreateConfig(StringData tableName) { - return ""; -} - -std::unique_ptr<DataProtector> EmptyWiredTigerCustomizationHooks::getDataProtector() { - return std::unique_ptr<DataProtector>(); -} - -boost::filesystem::path EmptyWiredTigerCustomizationHooks::getProtectedPathSuffix() { +std::string WiredTigerCustomizationHooks::getTableCreateConfig(StringData tableName) { return ""; } -Status EmptyWiredTigerCustomizationHooks::protectTmpData( - const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen, size_t* resultLen) { - return Status(ErrorCodes::InternalError, - "Customization hooks must be enabled to use preprocessTmpData."); -} - -Status EmptyWiredTigerCustomizationHooks::unprotectTmpData( - const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen, size_t* resultLen) { - return Status(ErrorCodes::InternalError, - "Customization hooks must be enabled to use postprocessTmpData."); -} } // namespace mongo diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.h b/src/mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.h index e78995481bd..1ff86a8799e 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.h +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_customization_hooks.h @@ -32,20 +32,11 @@ #include <memory> #include <string> -#include "mongo/base/disallow_copying.h" -#include "mongo/db/jsobj.h" - -namespace boost { -namespace filesystem { -class path; -} // namespace filesystem -} // namespace boost - namespace mongo { -class DataProtector; class StringData; class ServiceContext; +// Interface and default implementation for WiredTiger customization hooks class WiredTigerCustomizationHooks { public: static void set(ServiceContext* service, @@ -53,76 +44,18 @@ public: static WiredTigerCustomizationHooks* get(ServiceContext* service); - virtual ~WiredTigerCustomizationHooks() = default; + virtual ~WiredTigerCustomizationHooks(); /** * Returns true if the customization hooks are enabled. */ - virtual bool enabled() const = 0; - - /** - * Perform any encryption engine initialization/sanity checking that needs to happen after - * storage engine initialization but before the server starts accepting incoming connections. - * - * Returns true if the server needs to be rebooted because of configuration changes. - */ - virtual bool restartRequired() = 0; + virtual bool enabled() const; /** * Gets an additional configuration string for the provided table name on a * `WT_SESSION::create` call. */ - virtual std::string getTableCreateConfig(StringData tableName) = 0; - - /** - * Returns the maximum size addition when doing transforming temp data. - */ - size_t additionalBytesForProtectedBuffer() { - return 33; - } - - /** - * Get the data protector object - */ - virtual std::unique_ptr<DataProtector> getDataProtector() = 0; - - /** - * Get an implementation specific path suffix to tag files with - */ - virtual boost::filesystem::path getProtectedPathSuffix() = 0; - - /** - * Transform temp data to non-readable form before writing it to disk. - */ - virtual Status protectTmpData( - const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen, size_t* resultLen) = 0; - - /** - * Tranforms temp data back to readable form, after reading from disk. - */ - virtual Status unprotectTmpData( - const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen, size_t* resultLen) = 0; + virtual std::string getTableCreateConfig(StringData tableName); }; -// Empty default implementation of the abstract class WiredTigerCustomizationHooks -class EmptyWiredTigerCustomizationHooks : public WiredTigerCustomizationHooks { -public: - ~EmptyWiredTigerCustomizationHooks() override; - - bool enabled() const override; - - bool restartRequired() override; - - std::string getTableCreateConfig(StringData tableName) override; - - std::unique_ptr<DataProtector> getDataProtector() override; - - boost::filesystem::path getProtectedPathSuffix() override; - - Status protectTmpData( - const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen, size_t* resultLen) override; - - Status unprotectTmpData( - const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen, size_t* resultLen) override; -}; } // namespace mongo diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_index.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_index.cpp index 6331594a57c..ecf3c183996 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_index.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_index.cpp @@ -1102,6 +1102,7 @@ void WiredTigerIndexUnique::_unindex(WT_CURSOR* c, triggerWriteConflictAtPoint(c); return; } + invariantWTOK(ret); WT_ITEM value; invariantWTOK(c->get_value(c, &value)); BufReader br(value.data, value.size); diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp index f48fff37b0b..aafb52b30c0 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp @@ -228,6 +228,7 @@ WiredTigerKVEngine::WiredTigerKVEngine(const std::string& canonicalName, ss << "checkpoint=(wait=" << wiredTigerGlobalOptions.checkpointDelaySecs; ss << ",log_size=2GB),"; ss << "statistics_log=(wait=" << wiredTigerGlobalOptions.statisticsLogDelaySecs << "),"; + ss << "verbose=(recovery_progress),"; } ss << WiredTigerCustomizationHooks::get(getGlobalServiceContext()) ->getTableCreateConfig("system"); @@ -253,6 +254,13 @@ WiredTigerKVEngine::WiredTigerKVEngine(const std::string& canonicalName, msgassertedNoTrace(28718, s.reason()); } invariantWTOK(_conn->close(_conn, NULL)); + // After successful recovery, remove the journal directory. + try { + boost::filesystem::remove_all(journalPath); + } catch (std::exception& e) { + error() << "error removing journal dir " << journalPath.string() << ' ' << e.what(); + throw; + } } // This setting overrides the earlier setting because it is later in the config string. ss << ",log=(enabled=false),"; diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp index 0d3cf93a307..7761857bf90 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp @@ -909,7 +909,7 @@ int64_t WiredTigerRecordStore::storageSize(OperationContext* txn, if (_isEphemeral) { return dataSize(txn); } - WiredTigerSession* session = WiredTigerRecoveryUnit::get(txn)->getSession(txn); + WiredTigerSession* session = WiredTigerRecoveryUnit::get(txn)->getSessionNoTxn(txn); StatusWith<int64_t> result = WiredTigerUtil::getStatisticsValueAs<int64_t>(session->getSession(), "statistics:" + getURI(), @@ -1194,7 +1194,9 @@ bool WiredTigerRecordStore::yieldAndAwaitOplogDeletionRequest(OperationContext* // The top-level locks were freed, so also release any potential low-level (storage engine) // locks that might be held. - txn->recoveryUnit()->abandonSnapshot(); + WiredTigerRecoveryUnit* recoveryUnit = (WiredTigerRecoveryUnit*)txn->recoveryUnit(); + recoveryUnit->abandonSnapshot(); + recoveryUnit->beginIdle(); // Wait for an oplog deletion request, or for this record store to have been destroyed. oplogStones->awaitHasExcessStonesOrDead(); @@ -1219,15 +1221,22 @@ void WiredTigerRecordStore::reclaimOplog(OperationContext* txn) { try { WriteUnitOfWork wuow(txn); - WiredTigerCursor startwrap(_uri, _tableId, true, txn); - WT_CURSOR* start = startwrap.get(); - start->set_key(start, _makeKey(_oplogStones->firstRecord)); + WiredTigerCursor cwrap(_uri, _tableId, true, txn); + WT_CURSOR* cursor = cwrap.get(); - WiredTigerCursor endwrap(_uri, _tableId, true, txn); - WT_CURSOR* end = endwrap.get(); - end->set_key(end, _makeKey(stone->lastRecord)); + // The first record in the oplog should be within the truncate range. + int ret = WT_READ_CHECK(cursor->next(cursor)); + invariantWTOK(ret); + int64_t key; + invariantWTOK(cursor->get_key(cursor, &key)); + RecordId firstRecord = _fromKey(key); + if (firstRecord < _oplogStones->firstRecord || firstRecord > stone->lastRecord) { + warning() << "First oplog record " << firstRecord << " is not in truncation range (" + << _oplogStones->firstRecord << ", " << stone->lastRecord << ")"; + } - invariantWTOK(session->truncate(session, nullptr, start, end, nullptr)); + cursor->set_key(cursor, _makeKey(stone->lastRecord)); + invariantWTOK(session->truncate(session, nullptr, nullptr, cursor, nullptr)); _changeNumRecords(txn, -stone->records); _increaseDataSize(txn, -stone->bytes); @@ -1619,7 +1628,7 @@ void WiredTigerRecordStore::appendCustomStats(OperationContext* txn, result->appendIntOrLL("sleepCount", _cappedSleep.load()); result->appendIntOrLL("sleepMS", _cappedSleepMS.load()); } - WiredTigerSession* session = WiredTigerRecoveryUnit::get(txn)->getSession(txn); + WiredTigerSession* session = WiredTigerRecoveryUnit::get(txn)->getSessionNoTxn(txn); WT_SESSION* s = session->getSession(); BSONObjBuilder bob(result->subobjStart(_engineName)); { diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.cpp index 66fd5e0ddfa..fe35bd07651 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.cpp @@ -43,6 +43,7 @@ #include "mongo/util/concurrency/ticketholder.h" #include "mongo/util/log.h" #include "mongo/util/mongoutils/str.h" +#include "mongo/util/scopeguard.h" #include "mongo/util/stacktrace.h" namespace mongo { @@ -172,7 +173,12 @@ WiredTigerSession* WiredTigerRecoveryUnit::getSession(OperationContext* opCtx) { WiredTigerSession* WiredTigerRecoveryUnit::getSessionNoTxn(OperationContext* opCtx) { _ensureSession(); - return _session.get(); + WiredTigerSession* session = _session.get(); + + // Dropping the queued idents might block session, which is not desired for fastpath workflow + // like FTDC thread. Disable dropping of queued idents for such sessions. + session->dropQueuedIdentsAtSessionEndAllowed(false); + return session; } void WiredTigerRecoveryUnit::abandonSnapshot() { @@ -249,6 +255,13 @@ void WiredTigerRecoveryUnit::_txnOpen(OperationContext* opCtx) { _active = true; } +void WiredTigerRecoveryUnit::beginIdle() { + // Close all cursors, we don't want to keep any old cached cursors around. + if (_session) { + _session->closeAllCursors(""); + } +} + // --------------------- WiredTigerCursor::WiredTigerCursor(const std::string& uri, diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.h b/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.h index 86d9eece13d..695ad331240 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.h +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.h @@ -84,6 +84,12 @@ public: WiredTigerSession* getSession(OperationContext* opCtx); /** + * Enter a period of wait or computation during which there are no WT calls. + * Any non-relevant cached handles can be closed. + */ + void beginIdle(); + + /** * Returns a session without starting a new WT txn on the session. Will not close any already * running session. */ diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_server_status.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_server_status.cpp index 4967dfc2f86..3cb87349fdb 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_server_status.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_server_status.cpp @@ -35,6 +35,7 @@ #include "mongo/base/checked_cast.h" #include "mongo/bson/bsonobjbuilder.h" +#include "mongo/db/concurrency/d_concurrency.h" #include "mongo/db/storage/wiredtiger/wiredtiger_kv_engine.h" #include "mongo/db/storage/wiredtiger/wiredtiger_record_store.h" #include "mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.h" @@ -56,6 +57,8 @@ bool WiredTigerServerStatusSection::includeByDefault() const { BSONObj WiredTigerServerStatusSection::generateSection(OperationContext* txn, const BSONElement& configElement) const { + Lock::GlobalLock lk(txn->lockState(), LockMode::MODE_IS, UINT_MAX); + // The session does not open a transaction here as one is not needed and opening one would // mean that execution could become blocked when a new transaction cannot be allocated // immediately. diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_session_cache.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_session_cache.cpp index 4fccd06ed6d..9df6ba94651 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_session_cache.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_session_cache.cpp @@ -36,6 +36,7 @@ #include "mongo/db/storage/wiredtiger/wiredtiger_session_cache.h" #include "mongo/base/error_codes.h" +#include "mongo/db/server_parameters.h" #include "mongo/db/storage/journal_listener.h" #include "mongo/db/storage/wiredtiger/wiredtiger_kv_engine.h" #include "mongo/db/storage/wiredtiger/wiredtiger_util.h" @@ -46,13 +47,32 @@ namespace mongo { +std::atomic<std::int32_t> kWiredTigerCursorCacheSize(10000); // NOLINT + +class WiredTigerCursorCacheSize + : public ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime> { +public: + WiredTigerCursorCacheSize() + : ExportedServerParameter<std::int32_t, ServerParameterType::kStartupAndRuntime>( + ServerParameterSet::getGlobal(), + "wiredTigerCursorCacheSize", + &kWiredTigerCursorCacheSize) {} + + virtual Status validate(const std::int32_t& potentialNewValue) { + if (potentialNewValue < 0) { + return Status(ErrorCodes::BadValue, + str::stream() + << "wiredTigerCursorCacheSize must be greater than or equal " + << "to 0, but attempted to set to: " + << potentialNewValue); + } + + return Status::OK(); + } +} WiredTigerCursorCacheSizeSetting; + WiredTigerSession::WiredTigerSession(WT_CONNECTION* conn, uint64_t epoch, uint64_t cursorEpoch) - : _epoch(epoch), - _cursorEpoch(cursorEpoch), - _session(NULL), - _cursorGen(0), - _cursorsCached(0), - _cursorsOut(0) { + : _epoch(epoch), _cursorEpoch(cursorEpoch), _session(NULL), _cursorGen(0), _cursorsOut(0) { invariantWTOK(conn->open_session(conn, NULL, "isolation=snapshot", &_session)); } @@ -65,7 +85,6 @@ WiredTigerSession::WiredTigerSession(WT_CONNECTION* conn, _cache(cache), _session(NULL), _cursorGen(0), - _cursorsCached(0), _cursorsOut(0) { invariantWTOK(conn->open_session(conn, NULL, "isolation=snapshot", &_session)); } @@ -83,7 +102,6 @@ WT_CURSOR* WiredTigerSession::getCursor(const std::string& uri, uint64_t id, boo WT_CURSOR* c = i->_cursor; _cursors.erase(i); _cursorsOut++; - _cursorsCached--; return c; } } @@ -107,17 +125,11 @@ void WiredTigerSession::releaseCursor(uint64_t id, WT_CURSOR* cursor) { // Cursors are pushed to the front of the list and removed from the back _cursors.push_front(WiredTigerCachedCursor(id, _cursorGen++, cursor)); - _cursorsCached++; - - // "Old" is defined as not used in the last N**2 operations, if we have N cursors cached. - // The reasoning here is to imagine a workload with N tables performing operations randomly - // across all of them (i.e., each cursor has 1/N chance of used for each operation). We - // would like to cache N cursors in that case, so any given cursor could go N**2 operations - // in between use. - while (_cursorGen - _cursors.back()._gen > 10000) { + + std::uint64_t cursorCacheSize = static_cast<std::uint64_t>(kWiredTigerCursorCacheSize.load()); + while (!_cursors.empty() && _cursorGen - _cursors.back()._gen > cursorCacheSize) { cursor = _cursors.back()._cursor; _cursors.pop_back(); - _cursorsCached--; invariantWTOK(cursor->close(cursor)); } } @@ -125,9 +137,10 @@ void WiredTigerSession::releaseCursor(uint64_t id, WT_CURSOR* cursor) { void WiredTigerSession::closeAllCursors(const std::string& uri) { invariant(_session); + bool all = (uri == ""); for (auto i = _cursors.begin(); i != _cursors.end();) { WT_CURSOR* cursor = i->_cursor; - if (cursor && uri == cursor->uri) { + if (cursor && (all || uri == cursor->uri)) { invariantWTOK(cursor->close(cursor)); i = _cursors.erase(i); } else @@ -344,6 +357,11 @@ void WiredTigerSessionCache::releaseSession(WiredTigerSession* session) { bool returnedToCache = false; uint64_t currentEpoch = _epoch.load(); + bool dropQueuedIdentsAtSessionEnd = session->isDropQueuedIdentsAtSessionEndAllowed(); + + // Reset this session's flag for dropping queued idents to default, before returning it to + // session cache. + session->dropQueuedIdentsAtSessionEndAllowed(true); if (session->_getEpoch() == currentEpoch) { // check outside of lock to reduce contention stdx::lock_guard<stdx::mutex> lock(_cacheLock); @@ -357,7 +375,7 @@ void WiredTigerSessionCache::releaseSession(WiredTigerSession* session) { if (!returnedToCache) delete session; - if (_engine && _engine->haveDropsQueued()) + if (dropQueuedIdentsAtSessionEnd && _engine && _engine->haveDropsQueued()) _engine->dropSomeQueuedIdents(); } diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_session_cache.h b/src/mongo/db/storage/wiredtiger/wiredtiger_session_cache.h index e8fa9811796..34c05a3304b 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_session_cache.h +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_session_cache.h @@ -99,12 +99,24 @@ public: void closeCursorsForQueuedDrops(WiredTigerKVEngine* engine); + /** + * Closes all cached cursors matching the uri. If the uri is empty, + * all cached cursors are closed. + */ void closeAllCursors(const std::string& uri); int cursorsOut() const { return _cursorsOut; } + bool isDropQueuedIdentsAtSessionEndAllowed() const { + return _dropQueuedIdentsAtSessionEnd; + } + + void dropQueuedIdentsAtSessionEndAllowed(bool dropQueuedIdentsAtSessionEnd) { + _dropQueuedIdentsAtSessionEnd = dropQueuedIdentsAtSessionEnd; + } + static uint64_t genTableId(); /** @@ -134,7 +146,8 @@ private: WT_SESSION* _session; // owned CursorCache _cursors; // owned uint64_t _cursorGen; - int _cursorsCached, _cursorsOut; + int _cursorsOut; + bool _dropQueuedIdentsAtSessionEnd = true; }; /** @@ -174,8 +187,8 @@ public: void closeCursorsForQueuedDrops(); /** - * Closes all cached cursors and ensures that previously opened cursors will be closed on - * release. + * Closes all cached cursors matching the uri. If the uri is empty, + * all cached cursors are closed. */ void closeAllCursors(const std::string& uri); diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_size_storer.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_size_storer.cpp index 7783cea4cbf..bab41079a9b 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_size_storer.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_size_storer.cpp @@ -202,7 +202,7 @@ void WiredTigerSizeStorer::syncCache(bool syncToDisk) { WT_SESSION* session = _session.getSession(); invariantWTOK(session->begin_transaction(session, syncToDisk ? "sync=true" : "")); - ScopeGuard rollbacker = MakeGuard(session->rollback_transaction, session, ""); + auto rollbacker = MakeGuard(session->rollback_transaction, session, ""); for (Map::iterator it = myMap.begin(); it != myMap.end(); ++it) { string uriKey = it->first; diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.cpp index da0f618f1bd..e6000e62604 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.cpp @@ -38,6 +38,7 @@ #include "mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.h" #include "mongo/util/log.h" #include "mongo/util/mongoutils/str.h" +#include "mongo/util/scopeguard.h" namespace mongo { diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_util.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_util.cpp index f00b4a33804..c0f0907f8da 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_util.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_util.cpp @@ -103,9 +103,13 @@ void WiredTigerUtil::fetchTypeAndSourceURI(OperationContext* opCtx, StatusWith<std::string> WiredTigerUtil::getMetadata(OperationContext* opCtx, StringData uri) { invariant(opCtx); - WiredTigerCursor curwrap("metadata:create", WiredTigerSession::kMetadataTableId, false, opCtx); - WT_CURSOR* cursor = curwrap.get(); + + auto session = WiredTigerRecoveryUnit::get(opCtx)->getSessionNoTxn(opCtx); + WT_CURSOR* cursor = + session->getCursor("metadata:create", WiredTigerSession::kMetadataTableId, false); invariant(cursor); + ON_BLOCK_EXIT([&] { session->releaseCursor(WiredTigerSession::kMetadataTableId, cursor); }); + std::string strUri = uri.toString(); cursor->set_key(cursor, strUri.c_str()); int ret = cursor->search(cursor); diff --git a/src/mongo/db/views/resolved_view.cpp b/src/mongo/db/views/resolved_view.cpp index d5f4faa7f68..f497cbbb9bd 100644 --- a/src/mongo/db/views/resolved_view.cpp +++ b/src/mongo/db/views/resolved_view.cpp @@ -62,7 +62,27 @@ ResolvedView ResolvedView::fromBSON(BSONObj commandResponseObj) { pipeline.push_back(item.Obj().getOwned()); } - return {ResolvedView(NamespaceString(viewDef["ns"].valueStringData()), pipeline)}; + BSONObj collationSpec; + if (auto collationElt = viewDef["collation"]) { + uassert(40639, + "View definition 'collation' field must be an object", + collationElt.type() == BSONType::Object); + collationSpec = collationElt.embeddedObject().getOwned(); + } + + return {NamespaceString(viewDef["ns"].valueStringData()), + std::move(pipeline), + std::move(collationSpec)}; +} + +BSONObj ResolvedView::toBSON() const { + BSONObjBuilder builder; + builder.append("ns", _namespace.ns()); + builder.append("pipeline", _pipeline); + if (!_defaultCollation.isEmpty()) { + builder.append("collation", _defaultCollation); + } + return builder.obj(); } StatusWith<BSONObj> ResolvedView::asExpandedViewAggregation( @@ -104,6 +124,13 @@ StatusWith<BSONObj> ResolvedView::asExpandedViewAggregation( aggregationBuilder.append("allowDiskUse", true); } + // Operations on a view must always use the default collation of the view. We must have already + // checked that if the user's request specifies a collation, it matches the collation of the + // view. + if (!_defaultCollation.isEmpty()) { + aggregationBuilder.append("collation", _defaultCollation); + } + return aggregationBuilder.obj(); } diff --git a/src/mongo/db/views/resolved_view.h b/src/mongo/db/views/resolved_view.h index a5016ffc4b3..789576c737f 100644 --- a/src/mongo/db/views/resolved_view.h +++ b/src/mongo/db/views/resolved_view.h @@ -44,8 +44,12 @@ class AggregationRequest; */ class ResolvedView { public: - ResolvedView(const NamespaceString& collectionNs, const std::vector<BSONObj>& pipeline) - : _namespace(collectionNs), _pipeline(pipeline) {} + ResolvedView(const NamespaceString& collectionNs, + std::vector<BSONObj> pipeline, + BSONObj defaultCollation) + : _namespace(collectionNs), + _pipeline(std::move(pipeline)), + _defaultCollation(std::move(defaultCollation)) {} /** * Returns whether 'commandResponseObj' contains a CommandOnShardedViewNotSupportedOnMongod @@ -55,6 +59,8 @@ public: static ResolvedView fromBSON(BSONObj commandResponseObj); + BSONObj toBSON() const; + /** * Convert an aggregation command on a view to the equivalent command against the views * underlying collection. @@ -70,9 +76,21 @@ public: return _pipeline; } + const BSONObj& getDefaultCollation() const { + return _defaultCollation; + } + private: NamespaceString _namespace; std::vector<BSONObj> _pipeline; + + // The default collation associated with this view. An empty object means that the default is + // the simple collation. + // + // Currently all operations which run over a view must use the default collation. This means + // that operations on the view which do not specify a collation inherit the default. Operations + // on the view which specify any other collation fail with a user error. + BSONObj _defaultCollation; }; } // namespace mongo diff --git a/src/mongo/db/views/resolved_view_test.cpp b/src/mongo/db/views/resolved_view_test.cpp index eebcda01db5..d8b95308d45 100644 --- a/src/mongo/db/views/resolved_view_test.cpp +++ b/src/mongo/db/views/resolved_view_test.cpp @@ -33,6 +33,7 @@ #include "mongo/bson/bsonmisc.h" #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/json.h" #include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/aggregation_request.h" #include "mongo/db/views/resolved_view.h" @@ -50,9 +51,10 @@ namespace { const NamespaceString viewNss("testdb.testview"); const NamespaceString backingNss("testdb.testcoll"); const std::vector<BSONObj> emptyPipeline; +const BSONObj kSimpleCollation; TEST(ResolvedViewTest, ExpandingCmdObjWithEmptyPipelineOnNoOpViewYieldsEmptyPipeline) { - const ResolvedView resolvedView{backingNss, emptyPipeline}; + const ResolvedView resolvedView{backingNss, emptyPipeline, kSimpleCollation}; BSONObj cmdObj = BSON("aggregate" << viewNss.coll() << "pipeline" << BSONArray()); auto result = resolvedView.asExpandedViewAggregation(cmdObj); @@ -65,7 +67,7 @@ TEST(ResolvedViewTest, ExpandingCmdObjWithEmptyPipelineOnNoOpViewYieldsEmptyPipe TEST(ResolvedViewTest, ExpandingCmdObjWithNonemptyPipelineAppendsToViewPipeline) { std::vector<BSONObj> viewPipeline{BSON("skip" << 7)}; - const ResolvedView resolvedView{backingNss, viewPipeline}; + const ResolvedView resolvedView{backingNss, viewPipeline, kSimpleCollation}; BSONObj cmdObj = BSON("aggregate" << viewNss.coll() << "pipeline" << BSON_ARRAY(BSON("limit" << 3))); @@ -80,13 +82,13 @@ TEST(ResolvedViewTest, ExpandingCmdObjWithNonemptyPipelineAppendsToViewPipeline) } TEST(ResolvedViewTest, ExpandingCmdObjFailsIfCmdObjIsNotAValidAggregationCommand) { - const ResolvedView resolvedView{backingNss, emptyPipeline}; + const ResolvedView resolvedView{backingNss, emptyPipeline, kSimpleCollation}; BSONObj badCmdObj = BSON("invalid" << 0); ASSERT_NOT_OK(resolvedView.asExpandedViewAggregation(badCmdObj).getStatus()); } TEST(ResolvedViewTest, ExpandingAggRequestWithEmptyPipelineOnNoOpViewYieldsEmptyPipeline) { - const ResolvedView resolvedView{backingNss, emptyPipeline}; + const ResolvedView resolvedView{backingNss, emptyPipeline, kSimpleCollation}; AggregationRequest aggRequest(viewNss, {}); auto result = resolvedView.asExpandedViewAggregation(aggRequest); @@ -99,7 +101,7 @@ TEST(ResolvedViewTest, ExpandingAggRequestWithEmptyPipelineOnNoOpViewYieldsEmpty TEST(ResolvedViewTest, ExpandingAggRequestWithNonemptyPipelineAppendsToViewPipeline) { std::vector<BSONObj> viewPipeline{BSON("skip" << 7)}; - const ResolvedView resolvedView{backingNss, viewPipeline}; + const ResolvedView resolvedView{backingNss, viewPipeline, kSimpleCollation}; std::vector<BSONObj> userAggregationPipeline = {BSON("limit" << 3)}; AggregationRequest aggRequest(viewNss, userAggregationPipeline); @@ -115,7 +117,7 @@ TEST(ResolvedViewTest, ExpandingAggRequestWithNonemptyPipelineAppendsToViewPipel } TEST(ResolvedViewTest, ExpandingAggRequestPreservesExplain) { - const ResolvedView resolvedView{backingNss, emptyPipeline}; + const ResolvedView resolvedView{backingNss, emptyPipeline, kSimpleCollation}; AggregationRequest aggRequest(viewNss, {}); aggRequest.setExplain(true); @@ -130,7 +132,7 @@ TEST(ResolvedViewTest, ExpandingAggRequestPreservesExplain) { } TEST(ResolvedViewTest, ExpandingAggRequestPreservesBypassDocumentValidation) { - const ResolvedView resolvedView{backingNss, emptyPipeline}; + const ResolvedView resolvedView{backingNss, emptyPipeline, kSimpleCollation}; AggregationRequest aggRequest(viewNss, {}); aggRequest.setBypassDocumentValidation(true); @@ -145,7 +147,7 @@ TEST(ResolvedViewTest, ExpandingAggRequestPreservesBypassDocumentValidation) { } TEST(ResolvedViewTest, ExpandingAggRequestPreservesAllowDiskUse) { - const ResolvedView resolvedView{backingNss, emptyPipeline}; + const ResolvedView resolvedView{backingNss, emptyPipeline, kSimpleCollation}; AggregationRequest aggRequest(viewNss, {}); aggRequest.setAllowDiskUse(true); @@ -159,6 +161,27 @@ TEST(ResolvedViewTest, ExpandingAggRequestPreservesAllowDiskUse) { ASSERT_BSONOBJ_EQ(result.getValue(), expected); } +TEST(ResolvedViewTest, ExpandingAggRequestPreservesDefaultCollationOfView) { + const ResolvedView resolvedView{backingNss, + emptyPipeline, + BSON("locale" + << "fr_CA")}; + ASSERT_BSONOBJ_EQ(resolvedView.getDefaultCollation(), + BSON("locale" + << "fr_CA")); + AggregationRequest aggRequest(viewNss, {}); + + auto result = resolvedView.asExpandedViewAggregation(aggRequest); + ASSERT_OK(result.getStatus()); + + BSONObj expected = + BSON("aggregate" << backingNss.coll() << "pipeline" << BSONArray() << "cursor" << BSONObj() + << "collation" + << BSON("locale" + << "fr_CA")); + ASSERT_BSONOBJ_EQ(result.getValue(), expected); +} + TEST(ResolvedViewTest, FromBSONFailsIfMissingResolvedView) { BSONObj badCmdResponse = BSON("x" << 1); ASSERT_THROWS_CODE(ResolvedView::fromBSON(badCmdResponse), UserException, 40248); @@ -190,6 +213,13 @@ TEST(ResolvedViewTest, FromBSONFailsOnInvalidPipelineType) { ASSERT_THROWS_CODE(ResolvedView::fromBSON(badCmdResponse), UserException, 40251); } +TEST(ResolvedViewTest, FromBSONFailsOnInvalidCollationType) { + BSONObj badCmdResponse = + BSON("resolvedView" << BSON( + "ns" << backingNss.ns() << "pipeline" << BSONArray() << "collation" << 1)); + ASSERT_THROWS_CODE(ResolvedView::fromBSON(badCmdResponse), AssertionException, 40639); +} + TEST(ResolvedViewTest, FromBSONSuccessfullyParsesEmptyBSONArrayIntoEmptyVector) { BSONObj cmdResponse = BSON("resolvedView" << BSON("ns" << backingNss.ns() << "pipeline" << BSONArray())); @@ -222,6 +252,22 @@ TEST(ResolvedViewTest, FromBSONSuccessfullyParsesPopulatedBSONArrayIntoVector) { SimpleBSONObjComparator::kInstance.makeEqualTo())); } +TEST(ResolvedViewTest, FromBSONSuccessfullyParsesCollation) { + BSONObj cmdResponse = BSON( + "resolvedView" << BSON("ns" << backingNss.ns() << "pipeline" << BSONArray() << "collation" + << BSON("locale" + << "fil"))); + const ResolvedView result = ResolvedView::fromBSON(cmdResponse); + ASSERT_EQ(result.getNamespace(), backingNss); + ASSERT(std::equal(emptyPipeline.begin(), + emptyPipeline.end(), + result.getPipeline().begin(), + SimpleBSONObjComparator::kInstance.makeEqualTo())); + ASSERT_BSONOBJ_EQ(result.getDefaultCollation(), + BSON("locale" + << "fil")); +} + TEST(ResolvedViewTest, IsResolvedViewErrorResponseDetectsKickbackErrorCodeSuccessfully) { BSONObj errorResponse = BSON("ok" << 0 << "code" << ErrorCodes::CommandOnShardedViewNotSupportedOnMongod << "errmsg" @@ -235,5 +281,34 @@ TEST(ResolvedViewTest, IsResolvedViewErrorResponseReportsFalseOnNonKickbackError << "View nesting too deep or view cycle detected"); ASSERT_FALSE(ResolvedView::isResolvedViewErrorResponse(errorResponse)); } + +TEST(ResolvedViewTest, ToBSONSerializesCorrectly) { + const ResolvedView resolvedView{backingNss, + std::vector<BSONObj>{BSON("$match" << BSON("x" << 1))}, + BSON("locale" + << "fr_CA")}; + auto serialized = resolvedView.toBSON(); + ASSERT_BSONOBJ_EQ( + serialized, + fromjson( + "{ns: 'testdb.testcoll', pipeline: [{$match: {x: 1}}], collation: {locale: 'fr_CA'}}")); +} + +TEST(ResolvedViewTest, ToBSONOutputCanBeReparsed) { + const ResolvedView resolvedView{backingNss, + emptyPipeline, + BSON("locale" + << "fr_CA")}; + auto serialized = resolvedView.toBSON(); + auto reparsedResolvedView = ResolvedView::fromBSON(BSON("resolvedView" << serialized)); + ASSERT_EQ(reparsedResolvedView.getNamespace(), backingNss); + ASSERT(std::equal(emptyPipeline.begin(), + emptyPipeline.end(), + reparsedResolvedView.getPipeline().begin(), + SimpleBSONObjComparator::kInstance.makeEqualTo())); + ASSERT_BSONOBJ_EQ(reparsedResolvedView.getDefaultCollation(), + BSON("locale" + << "fr_CA")); +} } // namespace } // namespace mongo diff --git a/src/mongo/db/views/view_catalog.cpp b/src/mongo/db/views/view_catalog.cpp index 30cabf24935..d590c509a7e 100644 --- a/src/mongo/db/views/view_catalog.cpp +++ b/src/mongo/db/views/view_catalog.cpp @@ -394,6 +394,7 @@ StatusWith<ResolvedView> ViewCatalog::resolveView(OperationContext* txn, stdx::lock_guard<stdx::mutex> lk(_mutex); const NamespaceString* resolvedNss = &nss; std::vector<BSONObj> resolvedPipeline; + BSONObj collation; for (int i = 0; i < ViewGraph::kMaxViewDepth; i++) { auto view = _lookup_inlock(txn, resolvedNss->ns()); @@ -408,10 +409,13 @@ StatusWith<ResolvedView> ViewCatalog::resolveView(OperationContext* txn, str::stream() << "View pipeline exceeds maximum size; maximum size is " << ViewGraph::kMaxViewPipelineSizeBytes}; } - return StatusWith<ResolvedView>({*resolvedNss, resolvedPipeline}); + return StatusWith<ResolvedView>( + {*resolvedNss, std::move(resolvedPipeline), std::move(collation)}); } resolvedNss = &(view->viewOn()); + collation = view->defaultCollator() ? view->defaultCollator()->getSpec().toBSON() + : CollationSpec::kSimpleSpec; // Prepend the underlying view's pipeline to the current working pipeline. const std::vector<BSONObj>& toPrepend = view->pipeline(); @@ -419,7 +423,8 @@ StatusWith<ResolvedView> ViewCatalog::resolveView(OperationContext* txn, // If the first stage is a $collStats, then we return early with the viewOn namespace. if (toPrepend.size() > 0 && !toPrepend[0]["$collStats"].eoo()) { - return StatusWith<ResolvedView>({*resolvedNss, resolvedPipeline}); + return StatusWith<ResolvedView>( + {*resolvedNss, std::move(resolvedPipeline), std::move(collation)}); } } diff --git a/src/mongo/db/views/view_catalog_test.cpp b/src/mongo/db/views/view_catalog_test.cpp index 1ca54f809df..367b4f73b0c 100644 --- a/src/mongo/db/views/view_catalog_test.cpp +++ b/src/mongo/db/views/view_catalog_test.cpp @@ -38,6 +38,8 @@ #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/db/namespace_string.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/query/collation/collator_factory_interface.h" #include "mongo/db/query/query_test_service_context.h" #include "mongo/db/server_options.h" #include "mongo/db/service_context_noop.h" @@ -417,6 +419,42 @@ TEST_F(ViewCatalogFixture, ResolveViewCorrectPipeline) { } } +TEST_F(ViewCatalogFixture, ResolveViewCorrectlyExtractsDefaultCollation) { + const NamespaceString view1("db.view1"); + const NamespaceString view2("db.view2"); + const NamespaceString viewOn("db.coll"); + BSONArrayBuilder pipeline1; + BSONArrayBuilder pipeline2; + + pipeline1 << BSON("$match" << BSON("foo" << 1)); + pipeline2 << BSON("$match" << BSON("foo" << 2)); + + BSONObj collation = BSON("locale" + << "mock_reverse_string"); + + ASSERT_OK(viewCatalog.createView(opCtx.get(), view1, viewOn, pipeline1.arr(), collation)); + ASSERT_OK(viewCatalog.createView(opCtx.get(), view2, view1, pipeline2.arr(), collation)); + + auto resolvedView = viewCatalog.resolveView(opCtx.get(), view2); + ASSERT(resolvedView.isOK()); + + ASSERT_EQ(resolvedView.getValue().getNamespace(), viewOn); + + std::vector<BSONObj> expected = {BSON("$match" << BSON("foo" << 1)), + BSON("$match" << BSON("foo" << 2))}; + std::vector<BSONObj> result = resolvedView.getValue().getPipeline(); + ASSERT_EQ(expected.size(), result.size()); + for (uint32_t i = 0; i < expected.size(); i++) { + ASSERT(SimpleBSONObjComparator::kInstance.evaluate(expected[i] == result[i])); + } + + auto expectedCollation = + CollatorFactoryInterface::get(opCtx->getServiceContext())->makeFromBSON(collation); + ASSERT_OK(expectedCollation.getStatus()); + ASSERT_BSONOBJ_EQ(resolvedView.getValue().getDefaultCollation(), + expectedCollation.getValue()->getSpec().toBSON()); +} + TEST_F(ViewCatalogFixture, InvalidateThenReload) { const NamespaceString viewName("db.view"); const NamespaceString viewOn("db.coll"); diff --git a/src/mongo/db/views/view_sharding_check.cpp b/src/mongo/db/views/view_sharding_check.cpp index cc662ff2549..90835e03a07 100644 --- a/src/mongo/db/views/view_sharding_check.cpp +++ b/src/mongo/db/views/view_sharding_check.cpp @@ -69,11 +69,7 @@ StatusWith<BSONObj> ViewShardingCheck::getResolvedViewIfSharded(OperationContext return BSONObj(); } - BSONObjBuilder viewDetailBob; - viewDetailBob.append("ns", sourceNss.ns()); - viewDetailBob.append("pipeline", resolvedView.getValue().getPipeline()); - - return viewDetailBob.obj(); + return resolvedView.getValue().toBSON(); } void ViewShardingCheck::appendShardedViewStatus(const BSONObj& resolvedView, BSONObjBuilder* out) { diff --git a/src/mongo/dbtests/dbhelper_tests.cpp b/src/mongo/dbtests/dbhelper_tests.cpp index 1ed948092c7..7a4d745e764 100644 --- a/src/mongo/dbtests/dbhelper_tests.cpp +++ b/src/mongo/dbtests/dbhelper_tests.cpp @@ -74,8 +74,12 @@ public: KeyRange range(ns, BSON("_id" << _min), BSON("_id" << _max), BSON("_id" << 1)); mongo::WriteConcernOptions dummyWriteConcern; - Helpers::removeRange( - &txn, range, BoundInclusion::kIncludeStartKeyOnly, dummyWriteConcern); + Milliseconds dummyReplWaitDuration; + Helpers::removeRange(&txn, + range, + BoundInclusion::kIncludeStartKeyOnly, + dummyWriteConcern, + dummyReplWaitDuration); } // Check that the expected documents remain. diff --git a/src/mongo/dbtests/jstests.cpp b/src/mongo/dbtests/jstests.cpp index 13e71a0a74a..c0421910c2f 100644 --- a/src/mongo/dbtests/jstests.cpp +++ b/src/mongo/dbtests/jstests.cpp @@ -2388,6 +2388,49 @@ public: } }; +class RequiresOwnedObjects { +public: + void run() { + char buf[] = {5, 0, 0, 0, 0}; + BSONObj unowned(buf); + BSONObj owned = unowned.getOwned(); + + ASSERT(!unowned.isOwned()); + ASSERT(owned.isOwned()); + + // Ensure that by default we can bind owned and unowned + { + unique_ptr<Scope> s(getGlobalScriptEngine()->newScope()); + s->setObject("unowned", unowned, true); + s->setObject("owned", owned, true); + } + + // After we set the flag, we should only be able to set owned + { + unique_ptr<Scope> s(getGlobalScriptEngine()->newScope()); + s->requireOwnedObjects(); + s->setObject("owned", owned, true); + + bool threwException = false; + try { + s->setObject("unowned", unowned, true); + } catch (...) { + threwException = true; + + auto status = exceptionToStatus(); + + ASSERT_EQUALS(status.code(), ErrorCodes::BadValue); + } + + ASSERT(threwException); + + // after resetting, we can set unowned's again + s->reset(); + s->setObject("unowned", unowned, true); + } + } +}; + class All : public Suite { public: All() : Suite("js") {} @@ -2444,6 +2487,7 @@ public: add<RecursiveInvoke>(); add<ErrorCodeFromInvoke>(); + add<RequiresOwnedObjects>(); add<RoundTripTests::DBRefTest>(); add<RoundTripTests::DBPointerTest>(); diff --git a/src/mongo/dbtests/rollbacktests.cpp b/src/mongo/dbtests/rollbacktests.cpp index b842eb5a9bc..300d3f46aed 100644 --- a/src/mongo/dbtests/rollbacktests.cpp +++ b/src/mongo/dbtests/rollbacktests.cpp @@ -216,7 +216,7 @@ public: } }; -template <bool rollback, bool defaultIndexes> +template <bool rollback, bool defaultIndexes, bool capped> class RenameCollection { public: void run() { @@ -236,7 +236,8 @@ public: WriteUnitOfWork uow(&txn); ASSERT(!collectionExists(&ctx, source.ns())); ASSERT(!collectionExists(&ctx, target.ns())); - ASSERT_OK(userCreateNS(&txn, ctx.db(), source.ns(), BSONObj(), defaultIndexes)); + auto options = capped ? BSON("capped" << true << "size" << 1000) : BSONObj(); + ASSERT_OK(userCreateNS(&txn, ctx.db(), source.ns(), options, defaultIndexes)); uow.commit(); } ASSERT(collectionExists(&ctx, source.ns())); @@ -263,7 +264,7 @@ public: } }; -template <bool rollback, bool defaultIndexes> +template <bool rollback, bool defaultIndexes, bool capped> class RenameDropTargetCollection { public: void run() { @@ -288,8 +289,9 @@ public: WriteUnitOfWork uow(&txn); ASSERT(!collectionExists(&ctx, source.ns())); ASSERT(!collectionExists(&ctx, target.ns())); - ASSERT_OK(userCreateNS(&txn, ctx.db(), source.ns(), BSONObj(), defaultIndexes)); - ASSERT_OK(userCreateNS(&txn, ctx.db(), target.ns(), BSONObj(), defaultIndexes)); + auto options = capped ? BSON("capped" << true << "size" << 1000) : BSONObj(); + ASSERT_OK(userCreateNS(&txn, ctx.db(), source.ns(), options, defaultIndexes)); + ASSERT_OK(userCreateNS(&txn, ctx.db(), target.ns(), options, defaultIndexes)); insertRecord(&txn, source, sourceDoc); insertRecord(&txn, target, targetDoc); diff --git a/src/mongo/executor/connection_pool.cpp b/src/mongo/executor/connection_pool.cpp index cf8aadd4d3a..9bbf528725e 100644 --- a/src/mongo/executor/connection_pool.cpp +++ b/src/mongo/executor/connection_pool.cpp @@ -60,6 +60,45 @@ namespace executor { */ class ConnectionPool::SpecificPool { public: + /** + * These active client methods must be used whenever entering a specific pool outside of the + * shutdown background task. The presence of an active client will bump a counter on the + * specific pool which will prevent the shutdown thread from deleting it. + * + * The complexity comes from the need to hold a lock when writing to the + * _activeClients param on the specific pool. Because the code beneath the client needs to lock + * and unlock the parent mutex (and can leave unlocked), we want to start the client with the + * lock acquired, move it into the client, then re-acquire to decrement the counter on the way + * out. + * + * It's used like: + * + * pool.runWithActiveClient([](stdx::unique_lock<stdx::mutex> lk){ codeToBeProtected(); }); + */ + template <typename Callback> + void runWithActiveClient(Callback&& cb) { + runWithActiveClient(stdx::unique_lock<stdx::mutex>(_parent->_mutex), + std::forward<Callback>(cb)); + } + + template <typename Callback> + void runWithActiveClient(stdx::unique_lock<stdx::mutex> lk, Callback&& cb) { + invariant(lk.owns_lock()); + + _activeClients++; + + const auto guard = MakeGuard([&] { + invariant(!lk.owns_lock()); + stdx::lock_guard<stdx::mutex> lk(_parent->_mutex); + _activeClients--; + }); + + { + decltype(lk) localLk(std::move(lk)); + cb(std::move(localLk)); + } + } + SpecificPool(ConnectionPool* parent, const HostAndPort& hostAndPort); ~SpecificPool(); @@ -149,6 +188,7 @@ private: std::unique_ptr<TimerInterface> _requestTimer; Date_t _requestTimerExpiration; + size_t _activeClients; size_t _generation; bool _inFulfillRequests; bool _inSpawnConnections; @@ -206,8 +246,11 @@ void ConnectionPool::dropConnections(const HostAndPort& hostAndPort) { if (iter == _pools.end()) return; - iter->second.get()->processFailure( - Status(ErrorCodes::PooledConnectionsDropped, "Pooled connections dropped"), std::move(lk)); + iter->second->runWithActiveClient(std::move(lk), [&](decltype(lk) lk) { + iter->second->processFailure( + Status(ErrorCodes::PooledConnectionsDropped, "Pooled connections dropped"), + std::move(lk)); + }); } void ConnectionPool::get(const HostAndPort& hostAndPort, @@ -229,7 +272,9 @@ void ConnectionPool::get(const HostAndPort& hostAndPort, invariant(pool); - pool->getConnection(hostAndPort, timeout, std::move(lk), std::move(cb)); + pool->runWithActiveClient(std::move(lk), [&](decltype(lk) lk) { + pool->getConnection(hostAndPort, timeout, std::move(lk), std::move(cb)); + }); } void ConnectionPool::appendConnectionStats(ConnectionPoolStats* stats) const { @@ -264,13 +309,16 @@ void ConnectionPool::returnConnection(ConnectionInterface* conn) { invariant(iter != _pools.end()); - iter->second.get()->returnConnection(conn, std::move(lk)); + iter->second->runWithActiveClient(std::move(lk), [&](decltype(lk) lk) { + iter->second->returnConnection(conn, std::move(lk)); + }); } ConnectionPool::SpecificPool::SpecificPool(ConnectionPool* parent, const HostAndPort& hostAndPort) : _parent(parent), _hostAndPort(hostAndPort), _requestTimer(parent->_factory->makeTimer()), + _activeClients(0), _generation(0), _inFulfillRequests(false), _inSpawnConnections(false), @@ -362,47 +410,48 @@ void ConnectionPool::SpecificPool::returnConnection(ConnectionInterface* connPtr // Unlock in case refresh can occur immediately lk.unlock(); - connPtr->refresh(_parent->_options.refreshTimeout, - [this](ConnectionInterface* connPtr, Status status) { - connPtr->indicateUsed(); - - stdx::unique_lock<stdx::mutex> lk(_parent->_mutex); - - auto conn = takeFromProcessingPool(connPtr); - - // If the host and port were dropped, let this lapse - if (conn->getGeneration() != _generation) { - spawnConnections(lk); - return; - } - - // If we're in shutdown, we don't need refreshed connections - if (_state == State::kInShutdown) - return; - - // If the connection refreshed successfully, throw it back in the ready - // pool - if (status.isOK()) { - addToReady(lk, std::move(conn)); - spawnConnections(lk); - return; - } - - // If we've exceeded the time limit, start a new connect, rather than - // failing all operations. We do this because the various callers have - // their own time limit which is unrelated to our internal one. - if (status.code() == ErrorCodes::NetworkInterfaceExceededTimeLimit) { - log() << "Pending connection to host " << _hostAndPort - << " did not complete within the connection timeout," - << " retrying with a new connection;" << openConnections(lk) - << " connections to that host remain open"; - spawnConnections(lk); - return; - } - - // Otherwise pass the failure on through - processFailure(status, std::move(lk)); - }); + connPtr->refresh( + _parent->_options.refreshTimeout, [this](ConnectionInterface* connPtr, Status status) { + connPtr->indicateUsed(); + + runWithActiveClient([&](stdx::unique_lock<stdx::mutex> lk) { + auto conn = takeFromProcessingPool(connPtr); + + // If the host and port were dropped, let this lapse + if (conn->getGeneration() != _generation) { + spawnConnections(lk); + return; + } + + // If we're in shutdown, we don't need refreshed connections + if (_state == State::kInShutdown) + return; + + // If the connection refreshed successfully, throw it back in + // the ready pool + if (status.isOK()) { + addToReady(lk, std::move(conn)); + spawnConnections(lk); + return; + } + + // If we've exceeded the time limit, start a new connect, + // rather than failing all operations. We do this because the + // various callers have their own time limit which is unrelated + // to our internal one. + if (status.code() == ErrorCodes::NetworkInterfaceExceededTimeLimit) { + log() << "Pending connection to host " << _hostAndPort + << " did not complete within the connection timeout," + << " retrying with a new connection;" << openConnections(lk) + << " connections to that host remain open"; + spawnConnections(lk); + return; + } + + // Otherwise pass the failure on through + processFailure(status, std::move(lk)); + }); + }); lk.lock(); } else { // If it's fine as it is, just put it in the ready queue @@ -425,25 +474,25 @@ void ConnectionPool::SpecificPool::addToReady(stdx::unique_lock<stdx::mutex>& lk connPtr->setTimeout(_parent->_options.refreshRequirement, [this, connPtr]() { OwnedConnection conn; - stdx::unique_lock<stdx::mutex> lk(_parent->_mutex); - - if (!_readyPool.count(connPtr)) { - // We've already been checked out. We don't need to refresh - // ourselves. - return; - } + runWithActiveClient([&](stdx::unique_lock<stdx::mutex> lk) { + if (!_readyPool.count(connPtr)) { + // We've already been checked out. We don't need to refresh + // ourselves. + return; + } - conn = takeFromPool(_readyPool, connPtr); + conn = takeFromPool(_readyPool, connPtr); - // If we're in shutdown, we don't need to refresh connections - if (_state == State::kInShutdown) - return; + // If we're in shutdown, we don't need to refresh connections + if (_state == State::kInShutdown) + return; - _checkedOutPool[connPtr] = std::move(conn); + _checkedOutPool[connPtr] = std::move(conn); - connPtr->indicateSuccess(); + connPtr->indicateSuccess(); - returnConnection(connPtr, std::move(lk)); + returnConnection(connPtr, std::move(lk)); + }); }); fulfillRequests(lk); @@ -586,26 +635,26 @@ void ConnectionPool::SpecificPool::spawnConnections(stdx::unique_lock<stdx::mute _parent->_options.refreshTimeout, [this](ConnectionInterface* connPtr, Status status) { connPtr->indicateUsed(); - stdx::unique_lock<stdx::mutex> lk(_parent->_mutex); - - auto conn = takeFromProcessingPool(connPtr); - - if (conn->getGeneration() != _generation) { - // If the host and port was dropped, let the - // connection lapse - spawnConnections(lk); - } else if (status.isOK()) { - addToReady(lk, std::move(conn)); - spawnConnections(lk); - } else if (status.code() == ErrorCodes::NetworkInterfaceExceededTimeLimit) { - // If we've exceeded the time limit, restart the connect, rather than - // failing all operations. We do this because the various callers - // have their own time limit which is unrelated to our internal one. - spawnConnections(lk); - } else { - // If the setup failed, cascade the failure edge - processFailure(status, std::move(lk)); - } + runWithActiveClient([&](stdx::unique_lock<stdx::mutex> lk) { + auto conn = takeFromProcessingPool(connPtr); + + if (conn->getGeneration() != _generation) { + // If the host and port was dropped, let the + // connection lapse + spawnConnections(lk); + } else if (status.isOK()) { + addToReady(lk, std::move(conn)); + spawnConnections(lk); + } else if (status.code() == ErrorCodes::NetworkInterfaceExceededTimeLimit) { + // If we've exceeded the time limit, restart the connect, rather than + // failing all operations. We do this because the various callers + // have their own time limit which is unrelated to our internal one. + spawnConnections(lk); + } else { + // If the setup failed, cascade the failure edge + processFailure(status, std::move(lk)); + } + }); }); // Note that this assumes that the refreshTimeout is sound for the // setupTimeout @@ -641,7 +690,7 @@ void ConnectionPool::SpecificPool::shutdown() { // If we have processing connections, wait for them to finish or timeout // before shutdown - if (_processingPool.size() || _droppedProcessingPool.size()) { + if (_processingPool.size() || _droppedProcessingPool.size() || _activeClients) { _requestTimer->setTimeout(Seconds(1), [this]() { shutdown(); }); return; @@ -693,27 +742,27 @@ void ConnectionPool::SpecificPool::updateStateInLock() { // We set a timer for the most recent request, then invoke each timed // out request we couldn't service _requestTimer->setTimeout(timeout, [this]() { - stdx::unique_lock<stdx::mutex> lk(_parent->_mutex); - - auto now = _parent->_factory->now(); - - while (_requests.size()) { - auto& x = _requests.top(); - - if (x.first <= now) { - auto cb = std::move(x.second); - _requests.pop(); - - lk.unlock(); - cb(Status(ErrorCodes::NetworkInterfaceExceededTimeLimit, - "Couldn't get a connection within the time limit")); - lk.lock(); - } else { - break; + runWithActiveClient([&](stdx::unique_lock<stdx::mutex> lk) { + auto now = _parent->_factory->now(); + + while (_requests.size()) { + auto& x = _requests.top(); + + if (x.first <= now) { + auto cb = std::move(x.second); + _requests.pop(); + + lk.unlock(); + cb(Status(ErrorCodes::NetworkInterfaceExceededTimeLimit, + "Couldn't get a connection within the time limit")); + lk.lock(); + } else { + break; + } } - } - updateStateInLock(); + updateStateInLock(); + }); }); } else if (_checkedOutPool.size()) { // If we have no requests, but someone's using a connection, we just diff --git a/src/mongo/executor/network_interface_asio_auth.cpp b/src/mongo/executor/network_interface_asio_auth.cpp index 1f2039dbd1e..571a1be1f81 100644 --- a/src/mongo/executor/network_interface_asio_auth.cpp +++ b/src/mongo/executor/network_interface_asio_auth.cpp @@ -38,6 +38,7 @@ #include "mongo/db/auth/authorization_manager_global.h" #include "mongo/db/auth/internal_user_auth.h" #include "mongo/db/commands.h" +#include "mongo/db/commands/feature_compatibility_version_command_parser.h" #include "mongo/db/server_options.h" #include "mongo/db/wire_version.h" #include "mongo/rpc/factory.h" diff --git a/src/mongo/executor/task_executor.h b/src/mongo/executor/task_executor.h index 2d558512f91..ebf7c447ef0 100644 --- a/src/mongo/executor/task_executor.h +++ b/src/mongo/executor/task_executor.h @@ -196,6 +196,8 @@ public: /** * Schedules "work" to be run by the executor no sooner than "when". * + * If "when" is <= now(), then it schedules the "work" to be run ASAP. + * * Returns a handle for waiting on or canceling the callback, or * ErrorCodes::ShutdownInProgress. * diff --git a/src/mongo/executor/task_executor_test_common.cpp b/src/mongo/executor/task_executor_test_common.cpp index 57c12813250..d7a384231b8 100644 --- a/src/mongo/executor/task_executor_test_common.cpp +++ b/src/mongo/executor/task_executor_test_common.cpp @@ -328,14 +328,22 @@ COMMON_EXECUTOR_TEST(ScheduleWorkAt) { Status status1 = getDetectableErrorStatus(); Status status2 = getDetectableErrorStatus(); Status status3 = getDetectableErrorStatus(); + Status status4 = getDetectableErrorStatus(); + const Date_t now = net->now(); const TaskExecutor::CallbackHandle cb1 = unittest::assertGet(executor.scheduleWorkAt( now + Milliseconds(100), stdx::bind(setStatus, stdx::placeholders::_1, &status1))); + const TaskExecutor::CallbackHandle cb4 = unittest::assertGet(executor.scheduleWorkAt( + now - Milliseconds(50), stdx::bind(setStatus, stdx::placeholders::_1, &status4))); unittest::assertGet(executor.scheduleWorkAt( now + Milliseconds(5000), stdx::bind(setStatus, stdx::placeholders::_1, &status3))); const TaskExecutor::CallbackHandle cb2 = unittest::assertGet(executor.scheduleWorkAt( now + Milliseconds(200), stdx::bind(setStatusAndShutdown, stdx::placeholders::_1, &status2))); + + executor.wait(cb4); + ASSERT_OK(status4); + const Date_t startTime = net->now(); net->enterNetwork(); net->runUntil(startTime + Milliseconds(200)); diff --git a/src/mongo/rpc/get_status_from_command_result.cpp b/src/mongo/rpc/get_status_from_command_result.cpp index 2b4adf94aae..13c176f44df 100644 --- a/src/mongo/rpc/get_status_from_command_result.cpp +++ b/src/mongo/rpc/get_status_from_command_result.cpp @@ -30,6 +30,7 @@ #include "mongo/rpc/get_status_from_command_result.h" +#include "mongo/base/error_codes.h" #include "mongo/base/status.h" #include "mongo/bson/util/bson_extract.h" #include "mongo/db/jsobj.h" @@ -40,6 +41,7 @@ namespace mongo { namespace { const std::string kCmdResponseWriteConcernField = "writeConcernError"; +const std::string kCmdResponseWriteErrorsField = "writeErrors"; } // namespace Status getStatusFromCommandResult(const BSONObj& result) { @@ -107,4 +109,45 @@ Status getWriteConcernStatusFromCommandResult(const BSONObj& obj) { return wcError.toStatus(); } +Status getFirstWriteErrorStatusFromCommandResult(const BSONObj& cmdResponse) { + BSONElement writeErrorElem; + auto status = bsonExtractTypedField( + cmdResponse, kCmdResponseWriteErrorsField, BSONType::Array, &writeErrorElem); + if (!status.isOK()) { + if (status == ErrorCodes::NoSuchKey) { + return Status::OK(); + } else { + return status; + } + } + + auto firstWriteErrorElem = writeErrorElem.Obj().firstElement(); + if (!firstWriteErrorElem) { + return Status::OK(); + } + + if (firstWriteErrorElem.type() != BSONType::Object) { + return Status(ErrorCodes::UnsupportedFormat, + str::stream() << "writeErrors should be an array of objects, found " + << typeName(firstWriteErrorElem.type())); + } + + auto firstWriteErrorObj = firstWriteErrorElem.Obj(); + + return Status(ErrorCodes::fromInt(firstWriteErrorObj["code"].Int()), + firstWriteErrorObj["errmsg"].String()); +} + +Status getStatusFromWriteCommandReply(const BSONObj& cmdResponse) { + auto status = getStatusFromCommandResult(cmdResponse); + if (!status.isOK()) { + return status; + } + status = getFirstWriteErrorStatusFromCommandResult(cmdResponse); + if (!status.isOK()) { + return status; + } + return getWriteConcernStatusFromCommandResult(cmdResponse); +} + } // namespace mongo diff --git a/src/mongo/rpc/get_status_from_command_result.h b/src/mongo/rpc/get_status_from_command_result.h index 971805ee088..423b656a0ed 100644 --- a/src/mongo/rpc/get_status_from_command_result.h +++ b/src/mongo/rpc/get_status_from_command_result.h @@ -51,4 +51,17 @@ Status getStatusFromCommandResult(const BSONObj& result); */ Status getWriteConcernStatusFromCommandResult(const BSONObj& cmdResponse); + +/** + * Extracts the first write error from a command response and converts it into a status. This + * ignores all errors after the first and does not preserve the write error index, so it should not + * be used with bulk writes. + */ +Status getFirstWriteErrorStatusFromCommandResult(const BSONObj& cmdResponse); + +/** + * Extracts any type of error from a write command response. + */ +Status getStatusFromWriteCommandReply(const BSONObj& cmdResponse); + } // namespace mongo diff --git a/src/mongo/s/catalog/type_tags.cpp b/src/mongo/s/catalog/type_tags.cpp index 59d28cc1cc4..b0c72053cca 100644 --- a/src/mongo/s/catalog/type_tags.cpp +++ b/src/mongo/s/catalog/type_tags.cpp @@ -34,6 +34,7 @@ #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/bson/util/bson_extract.h" +#include "mongo/s/catalog/type_chunk.h" #include "mongo/util/assert_util.h" #include "mongo/util/mongoutils/str.h" @@ -55,8 +56,9 @@ StatusWith<TagsType> TagsType::fromBSON(const BSONObj& source) { { std::string tagsNs; Status status = bsonExtractStringField(source, ns.name(), &tagsNs); - if (!status.isOK()) + if (!status.isOK()) { return status; + } tags._ns = tagsNs; } @@ -64,28 +66,21 @@ StatusWith<TagsType> TagsType::fromBSON(const BSONObj& source) { { std::string tagsTag; Status status = bsonExtractStringField(source, tag.name(), &tagsTag); - if (!status.isOK()) + if (!status.isOK()) { return status; + } tags._tag = tagsTag; } { - BSONElement tagsMinKey; - Status status = bsonExtractTypedField(source, min.name(), Object, &tagsMinKey); - if (!status.isOK()) - return status; - - tags._minKey = tagsMinKey.Obj().getOwned(); - } - - { - BSONElement tagsMaxKey; - Status status = bsonExtractTypedField(source, max.name(), Object, &tagsMaxKey); - if (!status.isOK()) - return status; + auto tagRangeStatus = ChunkRange::fromBSON(source); + if (!tagRangeStatus.isOK()) + return tagRangeStatus.getStatus(); - tags._maxKey = tagsMaxKey.Obj().getOwned(); + const auto tagRange = std::move(tagRangeStatus.getValue()); + tags._minKey = tagRange.getMin().getOwned(); + tags._maxKey = tagRange.getMax().getOwned(); } return tags; diff --git a/src/mongo/s/catalog/type_tags_test.cpp b/src/mongo/s/catalog/type_tags_test.cpp index 78af4d5fe03..16a7c6dc2ee 100644 --- a/src/mongo/s/catalog/type_tags_test.cpp +++ b/src/mongo/s/catalog/type_tags_test.cpp @@ -117,9 +117,8 @@ TEST(TagsType, KeysNotAscending) { BSON(TagsType::tag("tag") << TagsType::ns("test.mycol") << TagsType::min(BSON("a" << 20)) << TagsType::max(BSON("a" << 10))); - StatusWith<TagsType> status = TagsType::fromBSON(obj); - const TagsType& tag = status.getValue(); - ASSERT_EQUALS(ErrorCodes::BadValue, tag.validate()); + StatusWith<TagsType> tagStatus = TagsType::fromBSON(obj); + ASSERT_EQUALS(ErrorCodes::FailedToParse, tagStatus.getStatus()); } TEST(TagsType, BadType) { diff --git a/src/mongo/s/client/shard_remote.cpp b/src/mongo/s/client/shard_remote.cpp index 85e0bc7678b..1e0ea51ed4b 100644 --- a/src/mongo/s/client/shard_remote.cpp +++ b/src/mongo/s/client/shard_remote.cpp @@ -340,7 +340,8 @@ StatusWith<Shard::QueryResponse> ShardRemote::_exhaustiveFindOnConfig( findCmdBuilder.done(), fetcherCallback, _appendMetadataForCommand(txn, readPrefWithMinOpTime), - maxTimeMS); + maxTimeMS /* find network timeout */, + maxTimeMS /* getMore network timeout */); Status scheduleStatus = fetcher.schedule(); if (!scheduleStatus.isOK()) { return scheduleStatus; diff --git a/src/mongo/s/commands/SConscript b/src/mongo/s/commands/SConscript index 800f8fe6005..b70eb831ac4 100644 --- a/src/mongo/s/commands/SConscript +++ b/src/mongo/s/commands/SConscript @@ -83,6 +83,7 @@ env.Library( LIBDEPS=[ '$BUILD_DIR/mongo/db/commands/apply_ops_cmd_common', '$BUILD_DIR/mongo/db/commands/killcursors_common', + '$BUILD_DIR/mongo/db/ftdc/ftdc_server', '$BUILD_DIR/mongo/db/pipeline/aggregation', '$BUILD_DIR/mongo/db/views/views', '$BUILD_DIR/mongo/rpc/client_metadata', diff --git a/src/mongo/s/commands/cluster_ftdc_commands.cpp b/src/mongo/s/commands/cluster_ftdc_commands.cpp index 23903e92984..12021e98f6d 100644 --- a/src/mongo/s/commands/cluster_ftdc_commands.cpp +++ b/src/mongo/s/commands/cluster_ftdc_commands.cpp @@ -67,6 +67,28 @@ public: Status checkAuthForCommand(Client* client, const std::string& dbname, const BSONObj& cmdObj) override { + + if (!AuthorizationSession::get(client)->isAuthorizedForActionsOnResource( + ResourcePattern::forClusterResource(), ActionType::serverStatus)) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + + if (!AuthorizationSession::get(client)->isAuthorizedForActionsOnResource( + ResourcePattern::forClusterResource(), ActionType::replSetGetStatus)) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + + if (!AuthorizationSession::get(client)->isAuthorizedForActionsOnResource( + ResourcePattern::forClusterResource(), ActionType::connPoolStats)) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + + if (!AuthorizationSession::get(client)->isAuthorizedForActionsOnResource( + ResourcePattern::forExactNamespace(NamespaceString("local", "oplog.rs")), + ActionType::collStats)) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + return Status::OK(); } @@ -77,9 +99,10 @@ public: std::string& errmsg, BSONObjBuilder& result) override { - errmsg = "getDiagnosticData not allowed through mongos"; + result.append( + "data", FTDCController::get(txn->getServiceContext())->getMostRecentPeriodicDocument()); - return false; + return true; } }; diff --git a/src/mongo/s/commands/cluster_shard_collection_cmd.cpp b/src/mongo/s/commands/cluster_shard_collection_cmd.cpp index f1ba15396a9..eac3e2a24e0 100644 --- a/src/mongo/s/commands/cluster_shard_collection_cmd.cpp +++ b/src/mongo/s/commands/cluster_shard_collection_cmd.cpp @@ -52,6 +52,7 @@ #include "mongo/db/write_concern_options.h" #include "mongo/s/balancer_configuration.h" #include "mongo/s/catalog/sharding_catalog_client.h" +#include "mongo/s/catalog/type_database.h" #include "mongo/s/catalog_cache.h" #include "mongo/s/client/shard_registry.h" #include "mongo/s/commands/cluster_write.h" @@ -186,12 +187,17 @@ public: auto const shardRegistry = Grid::get(opCtx)->shardRegistry(); auto const catalogCache = Grid::get(opCtx)->catalogCache(); - auto dbInfo = uassertStatusOK(catalogCache->getDatabase(opCtx, nss.db())); - - // Ensure sharding is allowed on the database - uassert(ErrorCodes::IllegalOperation, - str::stream() << "sharding not enabled for db " << nss.db(), - dbInfo.shardingEnabled()); + // Ensure sharding is allowed on the database by reading directly from the config server, + // because reading through the cache might produce a stale entry if the "enableSharding" was + // called through a different mongos + { + const auto opTimeWithDbt = + uassertStatusOK(catalogClient->getDatabase(opCtx, nss.db().toString())); + const auto& dbt = opTimeWithDbt.value; + uassert(ErrorCodes::IllegalOperation, + str::stream() << "sharding not enabled for db " << nss.db(), + dbt.getSharded()); + } auto routingInfo = uassertStatusOK(catalogCache->getCollectionRoutingInfo(opCtx, nss)); diff --git a/src/mongo/s/commands/cluster_write.cpp b/src/mongo/s/commands/cluster_write.cpp index b17035993d9..565a1ddc4d7 100644 --- a/src/mongo/s/commands/cluster_write.cpp +++ b/src/mongo/s/commands/cluster_write.cpp @@ -66,28 +66,6 @@ void toBatchError(const Status& status, BatchedCommandResponse* response) { } /** - * Given a maxChunkSize configuration and the number of chunks in a particular sharded collection, - * returns an optimal chunk size to use in order to achieve a good ratio between number of chunks - * and their size. - */ -uint64_t calculateDesiredChunkSize(uint64_t maxChunkSizeBytes, uint64_t numChunks) { - // Splitting faster in early chunks helps spread out an initial load better - const uint64_t minChunkSize = 1 << 20; // 1 MBytes - - if (numChunks <= 1) { - return 1024; - } else if (numChunks < 3) { - return minChunkSize / 2; - } else if (numChunks < 10) { - return std::max(maxChunkSizeBytes / 4, minChunkSize); - } else if (numChunks < 20) { - return std::max(maxChunkSizeBytes / 2, minChunkSize); - } else { - return maxChunkSizeBytes; - } -} - -/** * Returns the split point that will result in one of the chunk having exactly one document. Also * returns an empty document if the split point cannot be determined. * @@ -341,8 +319,7 @@ void updateChunkWriteStatsAndSplitIfNeeded(OperationContext* opCtx, const uint64_t chunkBytesWritten = chunk->addBytesWritten(dataWritten); - const uint64_t desiredChunkSize = - calculateDesiredChunkSize(balancerConfig->getMaxChunkSizeBytes(), manager->numChunks()); + const uint64_t desiredChunkSize = balancerConfig->getMaxChunkSizeBytes(); // If this chunk is at either end of the range, trigger auto-split at 10% less data written in // order to trigger the top-chunk optimization. diff --git a/src/mongo/s/move_chunk_request.cpp b/src/mongo/s/move_chunk_request.cpp index 4622f26fd29..104587ef751 100644 --- a/src/mongo/s/move_chunk_request.cpp +++ b/src/mongo/s/move_chunk_request.cpp @@ -195,7 +195,8 @@ bool MoveChunkRequest::operator==(const MoveChunkRequest& other) const { return false; if (_range != other._range) return false; - + if (_waitForDelete != other._waitForDelete) + return false; return true; } diff --git a/src/mongo/s/server.cpp b/src/mongo/s/server.cpp index 9177cccdb73..0b8a9a17651 100644 --- a/src/mongo/s/server.cpp +++ b/src/mongo/s/server.cpp @@ -51,6 +51,7 @@ #include "mongo/db/auth/user_cache_invalidator_job.h" #include "mongo/db/client.h" #include "mongo/db/dbwebserver.h" +#include "mongo/db/ftdc/ftdc_mongos.h" #include "mongo/db/initialize_server_global_state.h" #include "mongo/db/instance.h" #include "mongo/db/lasterror.h" @@ -152,6 +153,9 @@ static void cleanupTask() { if (auto catalog = Grid::get(txn)->catalogClient(txn)) { catalog->shutDown(txn); } + + // Shutdown Full-Time Data Capture + stopMongoSFTDC(); } audit::logShutdown(Client::getCurrent()); @@ -293,6 +297,8 @@ static ExitCode runMongosServer() { web.detach(); } + startMongoSFTDC(); + Status status = getGlobalAuthorizationManager()->initialize(NULL); if (!status.isOK()) { error() << "Initializing authorization data failed: " << status; diff --git a/src/mongo/s/sharding_initialization.cpp b/src/mongo/s/sharding_initialization.cpp index 2bf84b02d18..1c28829a696 100644 --- a/src/mongo/s/sharding_initialization.cpp +++ b/src/mongo/s/sharding_initialization.cpp @@ -182,6 +182,30 @@ Status initializeGlobalShardingState(OperationContext* txn, connPoolOptions.refreshRequirement = Milliseconds(ShardingTaskExecutorPoolRefreshRequirementMS); connPoolOptions.refreshTimeout = Milliseconds(ShardingTaskExecutorPoolRefreshTimeoutMS); + if (connPoolOptions.refreshRequirement <= connPoolOptions.refreshTimeout) { + auto newRefreshTimeout = connPoolOptions.refreshRequirement - Milliseconds(1); + warning() << "ShardingTaskExecutorPoolRefreshRequirementMS (" + << connPoolOptions.refreshRequirement + << ") set below ShardingTaskExecutorPoolRefreshTimeoutMS (" + << connPoolOptions.refreshTimeout + << "). Adjusting ShardingTaskExecutorPoolRefreshTimeoutMS to " + << newRefreshTimeout; + connPoolOptions.refreshTimeout = newRefreshTimeout; + } + + if (connPoolOptions.hostTimeout <= + connPoolOptions.refreshRequirement + connPoolOptions.refreshTimeout) { + auto newHostTimeout = + connPoolOptions.refreshRequirement + connPoolOptions.refreshTimeout + Milliseconds(1); + warning() << "ShardingTaskExecutorPoolHostTimeoutMS (" << connPoolOptions.hostTimeout + << ") set below ShardingTaskExecutorPoolRefreshRequirementMS (" + << connPoolOptions.refreshRequirement + << ") + ShardingTaskExecutorPoolRefreshTimeoutMS (" + << connPoolOptions.refreshTimeout + << "). Adjusting ShardingTaskExecutorPoolHostTimeoutMS to " << newHostTimeout; + connPoolOptions.hostTimeout = newHostTimeout; + } + auto network = executor::makeNetworkInterface("NetworkInterfaceASIO-ShardRegistry", stdx::make_unique<ShardingNetworkConnectionHook>(), diff --git a/src/mongo/scripting/deadline_monitor.cpp b/src/mongo/scripting/deadline_monitor.cpp index f75c34a0cc2..5eb0f52e5de 100644 --- a/src/mongo/scripting/deadline_monitor.cpp +++ b/src/mongo/scripting/deadline_monitor.cpp @@ -34,7 +34,7 @@ namespace mongo { -MONGO_EXPORT_SERVER_PARAMETER(scriptingEngineInterruptIntervalMS, int, 0); +MONGO_EXPORT_SERVER_PARAMETER(scriptingEngineInterruptIntervalMS, int, 1000); int getScriptingEngineInterruptInterval() { return scriptingEngineInterruptIntervalMS.load(); diff --git a/src/mongo/scripting/deadline_monitor.h b/src/mongo/scripting/deadline_monitor.h index b3c5007c72d..9081d7366e6 100644 --- a/src/mongo/scripting/deadline_monitor.h +++ b/src/mongo/scripting/deadline_monitor.h @@ -141,10 +141,10 @@ private: const Date_t now = Date_t::now(); const auto interruptInterval = Milliseconds{getScriptingEngineInterruptInterval()}; - if ((interruptInterval.count() > 0) && (now - lastInterruptCycle > interruptInterval)) { + if (now - lastInterruptCycle > interruptInterval) { for (const auto& task : _tasks) { - if (task.second > now) - task.first->interrupt(); + if (task.first->isKillPending()) + task.first->kill(); } lastInterruptCycle = now; } diff --git a/src/mongo/scripting/deadline_monitor_test.cpp b/src/mongo/scripting/deadline_monitor_test.cpp index 71daefbce8f..d802673bd3d 100644 --- a/src/mongo/scripting/deadline_monitor_test.cpp +++ b/src/mongo/scripting/deadline_monitor_test.cpp @@ -73,8 +73,12 @@ public: _group->noteKill(); } void interrupt() {} + const bool isKillPending() { + return killPending; + } TaskGroup* _group; uint64_t _killed; + bool killPending = false; }; // single task expires before stopping the deadline @@ -175,4 +179,13 @@ TEST(DeadlineMonitor, MultipleTasksExpireOrComplete) { } } +TEST(DeadlineMonitor, IsKillPendingKills) { + DeadlineMonitor<Task> dm; + TaskGroup group; + Task task(&group); + dm.startDeadline(&task, -1); + task.killPending = true; + group.waitForKillCount(1); + ASSERT(task._killed); +} } // namespace mongo diff --git a/src/mongo/scripting/engine.cpp b/src/mongo/scripting/engine.cpp index 3e9a5c542cf..1848bccfba2 100644 --- a/src/mongo/scripting/engine.cpp +++ b/src/mongo/scripting/engine.cpp @@ -42,6 +42,7 @@ #include "mongo/db/service_context.h" #include "mongo/platform/unordered_set.h" #include "mongo/scripting/dbdirectclient_factory.h" +#include "mongo/util/fail_point_service.h" #include "mongo/util/file.h" #include "mongo/util/log.h" #include "mongo/util/text.h" @@ -55,7 +56,10 @@ using std::unique_ptr; AtomicInt64 Scope::_lastVersion(1); + namespace { + +MONGO_FP_DECLARE(mr_killop_test_fp); // 2 GB is the largest support Javascript file size. const fileofs kMaxJsFileLength = fileofs(2) * 1024 * 1024 * 1024; @@ -231,6 +235,15 @@ void Scope::loadStored(OperationContext* txn, bool ignoreNotConnected) { uassert(10209, str::stream() << "name has to be a string: " << n, n.type() == String); uassert(10210, "value has to be set", v.type() != EOO); + if (MONGO_FAIL_POINT(mr_killop_test_fp)) { + + /* This thread sleep makes the interrupts in the test come in at a time + * where the js misses the interrupt and throw an exception instead of + * being interrupted + */ + stdx::this_thread::sleep_for(stdx::chrono::seconds(1)); + } + try { setElement(n.valuestr(), v, o); thisTime.insert(n.valuestr()); @@ -427,6 +440,9 @@ public: void advanceGeneration() { _real->advanceGeneration(); } + void requireOwnedObjects() override { + _real->requireOwnedObjects(); + } bool isKillPending() const { return _real->isKillPending(); } diff --git a/src/mongo/scripting/engine.h b/src/mongo/scripting/engine.h index 2b2ab0ff14d..e773dec5f60 100644 --- a/src/mongo/scripting/engine.h +++ b/src/mongo/scripting/engine.h @@ -44,7 +44,7 @@ class OperationContext; struct JSFile { const char* name; - const StringData& source; + const StringData source; }; class Scope { @@ -103,6 +103,8 @@ public: virtual void advanceGeneration() = 0; + virtual void requireOwnedObjects() = 0; + virtual ScriptingFunction createFunction(const char* code); /** diff --git a/src/mongo/scripting/mozjs/PosixNSPR.cpp b/src/mongo/scripting/mozjs/PosixNSPR.cpp index ed1a3d5a49d..9054e930443 100644 --- a/src/mongo/scripting/mozjs/PosixNSPR.cpp +++ b/src/mongo/scripting/mozjs/PosixNSPR.cpp @@ -24,6 +24,7 @@ #include "mongo/stdx/chrono.h" #include "mongo/stdx/condition_variable.h" +#include "mongo/stdx/memory.h" #include "mongo/stdx/mutex.h" #include "mongo/stdx/thread.h" #include "mongo/util/concurrency/thread_name.h" @@ -98,9 +99,13 @@ PRThread* PR_CreateThread(PRThreadType type, MOZ_ASSERT(priority == PR_PRIORITY_NORMAL); try { - std::unique_ptr<nspr::Thread, void (*)(nspr::Thread*)> t( - js_new<nspr::Thread>(start, arg, state != PR_UNJOINABLE_THREAD), - js_delete_nonconst<nspr::Thread>); + // We can't use the nspr allocator to allocate this thread, because under asan we + // instrument the allocator so that asan can track the pointers correctly. This + // instrumentation + // requires that pointers be deleted in the same thread that they were allocated in. + // The threads created in PR_CreateThread are not always freed in the same thread + // that they were created in. So, we use the standard allocator here. + auto t = mongo::stdx::make_unique<nspr::Thread>(start, arg, state != PR_UNJOINABLE_THREAD); t->thread() = mongo::stdx::thread(&nspr::Thread::ThreadRoutine, t.get()); @@ -118,7 +123,7 @@ PRStatus PR_JoinThread(PRThread* thread) { try { thread->thread().join(); - js_delete(thread); + delete thread; return PR_SUCCESS; } catch (...) { diff --git a/src/mongo/scripting/mozjs/bson.cpp b/src/mongo/scripting/mozjs/bson.cpp index 5a2ebd0dfed..a2881c44664 100644 --- a/src/mongo/scripting/mozjs/bson.cpp +++ b/src/mongo/scripting/mozjs/bson.cpp @@ -59,13 +59,17 @@ namespace { * the appearance of mutable state on the read/write versions. */ struct BSONHolder { - BSONHolder(const BSONObj& obj, const BSONObj* parent, std::size_t generation, bool ro) + BSONHolder(const BSONObj& obj, const BSONObj* parent, const MozJSImplScope* scope, bool ro) : _obj(obj), - _generation(generation), + _generation(scope->getGeneration()), _isOwned(obj.isOwned() || (parent && parent->isOwned())), _resolved(false), _readOnly(ro), _altered(false) { + uassert( + ErrorCodes::BadValue, + "Attempt to bind an unowned BSON Object to a JS scope marked as requiring ownership", + _isOwned || (!scope->requiresOwnedObjects())); if (parent) { _parent.emplace(*parent); } @@ -107,7 +111,7 @@ void BSONInfo::make( auto scope = getScope(cx); scope->getProto<BSONInfo>().newObject(obj); - JS_SetPrivate(obj, scope->trackedNew<BSONHolder>(bson, parent, scope->getGeneration(), ro)); + JS_SetPrivate(obj, scope->trackedNew<BSONHolder>(bson, parent, scope, ro)); } void BSONInfo::finalize(JSFreeOp* fop, JSObject* obj) { diff --git a/src/mongo/scripting/mozjs/db.cpp b/src/mongo/scripting/mozjs/db.cpp index 7fa2f179241..7aa73337237 100644 --- a/src/mongo/scripting/mozjs/db.cpp +++ b/src/mongo/scripting/mozjs/db.cpp @@ -45,38 +45,17 @@ namespace mozjs { const char* const DBInfo::className = "DB"; -void DBInfo::getProperty(JSContext* cx, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue vp) { - // 2nd look into real values, may be cached collection object - if (!vp.isUndefined()) { - auto scope = getScope(cx); - auto opContext = scope->getOpContext(); - - if (opContext && vp.isObject()) { - ObjectWrapper o(cx, vp); - - if (o.hasOwnField(InternedString::_fullName)) { - // need to check every time that the collection did not get sharded - if (haveLocalShardingInfo(opContext, o.getString(InternedString::_fullName))) - uasserted(ErrorCodes::BadValue, "can't use sharded collection from db.eval"); - } - } +void DBInfo::resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp) { + *resolvedp = false; - return; - } + JS::RootedValue coll(cx); JS::RootedObject parent(cx); if (!JS_GetPrototype(cx, obj, &parent)) uasserted(ErrorCodes::JSInterpreterFailure, "Couldn't get prototype"); ObjectWrapper parentWrapper(cx, parent); - - if (parentWrapper.hasOwnField(id)) { - parentWrapper.getValue(id, vp); - return; - } + ObjectWrapper o(cx, obj); IdWrapper idw(cx, id); @@ -87,21 +66,39 @@ void DBInfo::getProperty(JSContext* cx, if (sname.size() == 0 || sname[0] == '_') { return; } + + // SpiderMonkey will call resolve even for __proto__ so acknowledge it exists. + if (sname == "__proto__"_sd) { + *resolvedp = true; + return; + } + } + + // Check if this exists on the parent, ie. DBCollection::resolve case + if (parentWrapper.hasOwnField(id)) { + parentWrapper.getValue(id, &coll); + + o.defineProperty(id, coll, 0); + + *resolvedp = true; + return; } // no hit, create new collection JS::RootedValue getCollection(cx); parentWrapper.getValue(InternedString::getCollection, &getCollection); + // Check if getCollection has been installed yet + // It is undefined if the user has a db name the same as one of methods/properties of the DB + // object. if (!(getCollection.isObject() && JS_ObjectIsFunction(cx, getCollection.toObjectOrNull()))) { - uasserted(ErrorCodes::BadValue, "getCollection is not a function"); + return; } JS::AutoValueArray<1> args(cx); idw.toValue(args[0]); - JS::RootedValue coll(cx); ObjectWrapper(cx, obj).callMethod(getCollection, args, &coll); uassert(16861, @@ -111,7 +108,7 @@ void DBInfo::getProperty(JSContext* cx, // cache collection for reuse, don't enumerate ObjectWrapper(cx, obj).defineProperty(id, coll, 0); - vp.set(coll); + *resolvedp = true; } void DBInfo::construct(JSContext* cx, JS::CallArgs args) { diff --git a/src/mongo/scripting/mozjs/db.h b/src/mongo/scripting/mozjs/db.h index c752953236f..8f080265ef0 100644 --- a/src/mongo/scripting/mozjs/db.h +++ b/src/mongo/scripting/mozjs/db.h @@ -44,10 +44,7 @@ namespace mozjs { */ struct DBInfo : public BaseInfo { static void construct(JSContext* cx, JS::CallArgs args); - static void getProperty(JSContext* cx, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue vp); + static void resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp); static const char* const className; }; diff --git a/src/mongo/scripting/mozjs/dbcollection.cpp b/src/mongo/scripting/mozjs/dbcollection.cpp index e0f903dd1fc..b1983bd367c 100644 --- a/src/mongo/scripting/mozjs/dbcollection.cpp +++ b/src/mongo/scripting/mozjs/dbcollection.cpp @@ -44,11 +44,11 @@ namespace mozjs { const char* const DBCollectionInfo::className = "DBCollection"; -void DBCollectionInfo::getProperty(JSContext* cx, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue vp) { - DBInfo::getProperty(cx, obj, id, vp); +void DBCollectionInfo::resolve(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + bool* resolvedp) { + DBInfo::resolve(cx, obj, id, resolvedp); } void DBCollectionInfo::construct(JSContext* cx, JS::CallArgs args) { diff --git a/src/mongo/scripting/mozjs/dbcollection.h b/src/mongo/scripting/mozjs/dbcollection.h index 34be422dfd4..ed3c39ff0f2 100644 --- a/src/mongo/scripting/mozjs/dbcollection.h +++ b/src/mongo/scripting/mozjs/dbcollection.h @@ -44,10 +44,7 @@ namespace mozjs { */ struct DBCollectionInfo : public BaseInfo { static void construct(JSContext* cx, JS::CallArgs args); - static void getProperty(JSContext* cx, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue vp); + static void resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp); static const char* const className; }; diff --git a/src/mongo/scripting/mozjs/dbquery.cpp b/src/mongo/scripting/mozjs/dbquery.cpp index c2543814505..15bd45bde3c 100644 --- a/src/mongo/scripting/mozjs/dbquery.cpp +++ b/src/mongo/scripting/mozjs/dbquery.cpp @@ -105,13 +105,8 @@ void DBQueryInfo::construct(JSContext* cx, JS::CallArgs args) { args.rval().setObjectOrNull(thisv); } -void DBQueryInfo::getProperty(JSContext* cx, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue vp) { - if (!vp.isUndefined()) { - return; - } +void DBQueryInfo::resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp) { + *resolvedp = false; IdWrapper wid(cx, id); @@ -134,9 +129,19 @@ void DBQueryInfo::getProperty(JSContext* cx, args[0].setInt32(wid.toInt32()); - ObjectWrapper(cx, obj).callMethod(arrayAccess, args, vp); - } else { - uasserted(ErrorCodes::BadValue, "arrayAccess is not a function"); + JS::RootedValue vp(cx); + + ObjectWrapper(cx, obj).callMethod(arrayAccess, args, &vp); + + if (!vp.isNullOrUndefined()) { + ObjectWrapper o(cx, obj); + + // Assumes the user won't modify the contents of what DBQuery::arrayAccess returns + // otherwise we need to install a getter. + o.defineProperty(id, vp, 0); + } + + *resolvedp = true; } } diff --git a/src/mongo/scripting/mozjs/dbquery.h b/src/mongo/scripting/mozjs/dbquery.h index dc844c3084c..590185c55ee 100644 --- a/src/mongo/scripting/mozjs/dbquery.h +++ b/src/mongo/scripting/mozjs/dbquery.h @@ -41,10 +41,8 @@ namespace mozjs { */ struct DBQueryInfo : public BaseInfo { static void construct(JSContext* cx, JS::CallArgs args); - static void getProperty(JSContext* cx, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue vp); + static void resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp); + static const char* const className; }; diff --git a/src/mongo/scripting/mozjs/implscope.cpp b/src/mongo/scripting/mozjs/implscope.cpp index c37ebc0cb2a..c31d5b940a4 100644 --- a/src/mongo/scripting/mozjs/implscope.cpp +++ b/src/mongo/scripting/mozjs/implscope.cpp @@ -258,7 +258,6 @@ MozJSImplScope::ASANHandles::ASANHandles() { } MozJSImplScope::ASANHandles::~ASANHandles() { - invariant(_handles.empty()); invariant(kCurrentASANHandles == this); kCurrentASANHandles = nullptr; } @@ -342,6 +341,14 @@ MozJSImplScope::MozRuntime::MozRuntime(const MozJSScriptEngine* engine) { .setIon(true) .setAsyncStack(false) .setNativeRegExp(true); + } else { + JS::RuntimeOptionsRef(_runtime.get()) + .setAsmJS(false) + .setThrowOnAsmJSValidationFailure(false) + .setBaseline(false) + .setIon(false) + .setAsyncStack(false) + .setNativeRegExp(false); } const StackLocator locator; @@ -403,6 +410,7 @@ MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine) _connectState(ConnectState::Not), _status(Status::OK()), _generation(0), + _requireOwnedObjects(false), _hasOutOfMemoryException(false), _binDataProto(_context), _bsonProto(_context), @@ -491,82 +499,81 @@ void MozJSImplScope::init(const BSONObj* data) { } } -void MozJSImplScope::setNumber(const char* field, double val) { - MozJSEntry entry(this); +template <typename ImplScopeFunction> +auto MozJSImplScope::_runSafely(ImplScopeFunction&& functionToRun) -> decltype(functionToRun()) { + try { + MozJSEntry entry(this); + return functionToRun(); + } catch (...) { + _error = _status.reason(); - ObjectWrapper(_context, _global).setNumber(field, val); + // Clear the status state + auto status = std::move(_status); + uassertStatusOK(status); + throw; + } } -void MozJSImplScope::setString(const char* field, StringData val) { - MozJSEntry entry(this); +void MozJSImplScope::setNumber(const char* field, double val) { + _runSafely([this, &field, &val] { ObjectWrapper(_context, _global).setNumber(field, val); }); +} - ObjectWrapper(_context, _global).setString(field, val); +void MozJSImplScope::setString(const char* field, StringData val) { + _runSafely([this, &field, &val] { ObjectWrapper(_context, _global).setString(field, val); }); } void MozJSImplScope::setBoolean(const char* field, bool val) { - MozJSEntry entry(this); - - ObjectWrapper(_context, _global).setBoolean(field, val); + _runSafely([this, &field, &val] { ObjectWrapper(_context, _global).setBoolean(field, val); }); } void MozJSImplScope::setElement(const char* field, const BSONElement& e, const BSONObj& parent) { - MozJSEntry entry(this); + _runSafely([this, &field, &e, &parent] { - ObjectWrapper(_context, _global).setBSONElement(field, e, parent, false); + ObjectWrapper(_context, _global).setBSONElement(field, e, parent, false); + }); } void MozJSImplScope::setObject(const char* field, const BSONObj& obj, bool readOnly) { - MozJSEntry entry(this); + _runSafely([this, &field, &obj, &readOnly] { - ObjectWrapper(_context, _global).setBSON(field, obj, readOnly); + ObjectWrapper(_context, _global).setBSON(field, obj, readOnly); + }); } int MozJSImplScope::type(const char* field) { - MozJSEntry entry(this); - - return ObjectWrapper(_context, _global).type(field); + return _runSafely([this, &field] { return ObjectWrapper(_context, _global).type(field); }); } double MozJSImplScope::getNumber(const char* field) { - MozJSEntry entry(this); - - return ObjectWrapper(_context, _global).getNumber(field); + return _runSafely([this, &field] { return ObjectWrapper(_context, _global).getNumber(field); }); } int MozJSImplScope::getNumberInt(const char* field) { - MozJSEntry entry(this); - - return ObjectWrapper(_context, _global).getNumberInt(field); + return _runSafely( + [this, &field] { return ObjectWrapper(_context, _global).getNumberInt(field); }); } long long MozJSImplScope::getNumberLongLong(const char* field) { - MozJSEntry entry(this); - - return ObjectWrapper(_context, _global).getNumberLongLong(field); + return _runSafely( + [this, &field] { return ObjectWrapper(_context, _global).getNumberLongLong(field); }); } Decimal128 MozJSImplScope::getNumberDecimal(const char* field) { - MozJSEntry entry(this); - - return ObjectWrapper(_context, _global).getNumberDecimal(field); + return _runSafely( + [this, &field] { return ObjectWrapper(_context, _global).getNumberDecimal(field); }); } std::string MozJSImplScope::getString(const char* field) { - MozJSEntry entry(this); - - return ObjectWrapper(_context, _global).getString(field); + return _runSafely([this, &field] { return ObjectWrapper(_context, _global).getString(field); }); } bool MozJSImplScope::getBoolean(const char* field) { - MozJSEntry entry(this); - - return ObjectWrapper(_context, _global).getBoolean(field); + return _runSafely( + [this, &field] { return ObjectWrapper(_context, _global).getBoolean(field); }); } BSONObj MozJSImplScope::getObject(const char* field) { - MozJSEntry entry(this); - - return ObjectWrapper(_context, _global).getObject(field); + return _runSafely([this, &field] { return ObjectWrapper(_context, _global).getObject(field); }); } void MozJSImplScope::newFunction(StringData raw, JS::MutableHandleValue out) { @@ -642,17 +649,15 @@ ScriptingFunction MozJSImplScope::_createFunction(const char* raw) { } void MozJSImplScope::setFunction(const char* field, const char* code) { - MozJSEntry entry(this); - - JS::RootedValue fun(_context); - _MozJSCreateFunction(code, &fun); - ObjectWrapper(_context, _global).setValue(field, fun); + _runSafely([this, &field, &code] { + JS::RootedValue fun(_context); + _MozJSCreateFunction(code, &fun); + ObjectWrapper(_context, _global).setValue(field, fun); + }); } void MozJSImplScope::rename(const char* from, const char* to) { - MozJSEntry entry(this); - - ObjectWrapper(_context, _global).rename(from, to); + _runSafely([this, &from, &to] { ObjectWrapper(_context, _global).rename(from, to); }); } int MozJSImplScope::invoke(ScriptingFunction func, @@ -764,15 +769,15 @@ bool MozJSImplScope::exec(StringData code, } void MozJSImplScope::injectNative(const char* field, NativeFunction func, void* data) { - MozJSEntry entry(this); - - JS::RootedObject obj(_context); + _runSafely([this, &field, &func, &data] { + JS::RootedObject obj(_context); - NativeFunctionInfo::make(_context, &obj, func, data); + NativeFunctionInfo::make(_context, &obj, func, data); - JS::RootedValue value(_context); - value.setObjectOrNull(obj); - ObjectWrapper(_context, _global).setValue(field, value); + JS::RootedValue value(_context); + value.setObjectOrNull(obj); + ObjectWrapper(_context, _global).setValue(field, value); + }); } void MozJSImplScope::gc() { @@ -781,63 +786,65 @@ void MozJSImplScope::gc() { } void MozJSImplScope::localConnectForDbEval(OperationContext* txn, const char* dbName) { - MozJSEntry entry(this); - - if (_connectState == ConnectState::External) - uasserted(12510, "externalSetup already called, can't call localConnect"); - if (_connectState == ConnectState::Local) { - if (_localDBName == dbName) - return; - uasserted(12511, - str::stream() << "localConnect previously called with name " << _localDBName); - } + _runSafely([this, &txn, &dbName] { + if (_connectState == ConnectState::External) + uasserted(12510, "externalSetup already called, can't call localConnect"); + if (_connectState == ConnectState::Local) { + if (_localDBName == dbName) + return; + uasserted(12511, + str::stream() << "localConnect previously called with name " << _localDBName); + } - // NOTE: order is important here. the following methods must be called after - // the above conditional statements. + // NOTE: order is important here. the following methods must be called after + // the above conditional statements. - _connectState = ConnectState::Local; - _localDBName = dbName; + _connectState = ConnectState::Local; + _localDBName = dbName; - loadStored(txn); + loadStored(txn); - // install db access functions in the global object - installDBAccess(); + // install db access functions in the global object + installDBAccess(); - // install the Mongo function object and instantiate the 'db' global - _mongoLocalProto.install(_global); - execCoreFiles(); + // install the Mongo function object and instantiate the 'db' global + _mongoLocalProto.install(_global); + execCoreFiles(); - const char* const makeMongo = "const _mongo = new Mongo()"; - exec(makeMongo, "local connect 2", false, true, true, 0); + const char* const makeMongo = "const _mongo = new Mongo()"; + exec(makeMongo, "local connect 2", false, true, true, 0); - std::string makeDB = str::stream() << "const db = _mongo.getDB(\"" << dbName << "\");"; - exec(makeDB, "local connect 3", false, true, true, 0); + std::string makeDB = str::stream() << "const db = _mongo.getDB(\"" << dbName << "\");"; + exec(makeDB, "local connect 3", false, true, true, 0); + }); } void MozJSImplScope::externalSetup() { - MozJSEntry entry(this); - if (_connectState == ConnectState::External) - return; - if (_connectState == ConnectState::Local) - uasserted(12512, "localConnect already called, can't call externalSetup"); + _runSafely([&] { + if (_connectState == ConnectState::External) + return; + if (_connectState == ConnectState::Local) + uasserted(12512, "localConnect already called, can't call externalSetup"); - // install db access functions in the global object - installDBAccess(); + // install db access functions in the global object + installDBAccess(); - // install thread-related functions (e.g. _threadInject) - installFork(); + // install thread-related functions (e.g. _threadInject) + installFork(); - // install the Mongo function object - _mongoExternalProto.install(_global); - execCoreFiles(); - _connectState = ConnectState::External; + // install the Mongo function object + _mongoExternalProto.install(_global); + execCoreFiles(); + _connectState = ConnectState::External; + }); } void MozJSImplScope::reset() { unregisterOperation(); _pendingKill.store(false); _pendingGC.store(false); + _requireOwnedObjects = false; advanceGeneration(); } @@ -951,6 +958,14 @@ void MozJSImplScope::advanceGeneration() { _generation++; } +void MozJSImplScope::requireOwnedObjects() { + _requireOwnedObjects = true; +} + +bool MozJSImplScope::requiresOwnedObjects() const { + return _requireOwnedObjects; +} + const std::string& MozJSImplScope::getParentStack() const { return _parentStack; } diff --git a/src/mongo/scripting/mozjs/implscope.h b/src/mongo/scripting/mozjs/implscope.h index df37553e181..2de309e87ab 100644 --- a/src/mongo/scripting/mozjs/implscope.h +++ b/src/mongo/scripting/mozjs/implscope.h @@ -309,6 +309,10 @@ public: void advanceGeneration() override; + void requireOwnedObjects() override; + + bool requiresOwnedObjects() const; + JS::HandleId getInternedStringId(InternedString name) { return _internedStrings.getInternedString(name); } @@ -341,6 +345,9 @@ public: }; private: + template <typename ImplScopeFunction> + auto _runSafely(ImplScopeFunction&& functionToRun) -> decltype(functionToRun()); + void _MozJSCreateFunction(StringData raw, JS::MutableHandleValue fun); /** @@ -402,6 +409,7 @@ private: Status _status; std::string _parentStack; std::size_t _generation; + bool _requireOwnedObjects; bool _hasOutOfMemoryException; WrapType<BinDataInfo> _binDataProto; diff --git a/src/mongo/scripting/mozjs/mongohelpers.js b/src/mongo/scripting/mozjs/mongohelpers.js index b0b35bb2fe8..d3f743623a3 100644 --- a/src/mongo/scripting/mozjs/mongohelpers.js +++ b/src/mongo/scripting/mozjs/mongohelpers.js @@ -33,6 +33,14 @@ exportToMongoHelpers = { // This function accepts an expression or function body and returns a function definition 'functionExpressionParser': function functionExpressionParser(fnSrc) { + + // Ensure that a provided expression or function body is not terminated with a ';'. + // This ensures we interpret the input as a single expression, rather than a sequence + // of expressions, and can wrap it in parentheses. + while (fnSrc.endsWith(";") || fnSrc != fnSrc.trimRight()) { + fnSrc = fnSrc.slice(0, -1).trimRight(); + } + var parseTree; try { parseTree = this.Reflect.parse(fnSrc); diff --git a/src/mongo/scripting/mozjs/proxyscope.cpp b/src/mongo/scripting/mozjs/proxyscope.cpp index 6a6d20f4448..05697468f07 100644 --- a/src/mongo/scripting/mozjs/proxyscope.cpp +++ b/src/mongo/scripting/mozjs/proxyscope.cpp @@ -122,6 +122,10 @@ void MozJSProxyScope::advanceGeneration() { run([&] { _implScope->advanceGeneration(); }); } +void MozJSProxyScope::requireOwnedObjects() { + run([&] { _implScope->requireOwnedObjects(); }); +} + double MozJSProxyScope::getNumber(const char* field) { double out; run([&] { out = _implScope->getNumber(field); }); diff --git a/src/mongo/scripting/mozjs/proxyscope.h b/src/mongo/scripting/mozjs/proxyscope.h index 451981330a1..4dd69a3ebe9 100644 --- a/src/mongo/scripting/mozjs/proxyscope.h +++ b/src/mongo/scripting/mozjs/proxyscope.h @@ -129,6 +129,8 @@ public: void advanceGeneration() override; + void requireOwnedObjects() override; + double getNumber(const char* field) override; int getNumberInt(const char* field) override; long long getNumberLongLong(const char* field) override; diff --git a/src/mongo/shell/bench.cpp b/src/mongo/shell/bench.cpp index 040002f5c6e..338477ebe56 100644 --- a/src/mongo/shell/bench.cpp +++ b/src/mongo/shell/bench.cpp @@ -674,7 +674,7 @@ void BenchRunWorker::generateLoadOnConnection(DBClientBase* conn) { invariant(bsonTemplateEvaluator.setId(_id) == BsonTemplateEvaluator::StatusSuccess); if (_config->username != "") { - string errmsg; + std::string errmsg; if (!conn->auth("admin", _config->username, _config->password, errmsg)) { uasserted(15931, "Authenticating to connection for _benchThread failed: " + errmsg); } @@ -918,7 +918,7 @@ void BenchRunWorker::generateLoadOnConnection(DBClientBase* conn) { if (!result["err"].eoo() && result["err"].type() == String && (_config->throwGLE || op.throwGLE)) - throw DBException((string) "From benchRun GLE" + + throw DBException((std::string) "From benchRun GLE" + causedBy(result["err"].String()), result["code"].eoo() ? 0 : result["code"].Int()); } @@ -984,7 +984,7 @@ void BenchRunWorker::generateLoadOnConnection(DBClientBase* conn) { if (!result["err"].eoo() && result["err"].type() == String && (_config->throwGLE || op.throwGLE)) - throw DBException((string) "From benchRun GLE" + + throw DBException((std::string) "From benchRun GLE" + causedBy(result["err"].String()), result["code"].eoo() ? 0 : result["code"].Int()); } @@ -1031,7 +1031,7 @@ void BenchRunWorker::generateLoadOnConnection(DBClientBase* conn) { if (!result["err"].eoo() && result["err"].type() == String && (_config->throwGLE || op.throwGLE)) - throw DBException((string) "From benchRun GLE " + + throw DBException((std::string) "From benchRun GLE " + causedBy(result["err"].String()), result["code"].eoo() ? 0 : result["code"].Int()); } @@ -1133,7 +1133,7 @@ void BenchRunWorker::run() { try { std::unique_ptr<DBClientBase> conn(_config->createConnection()); if (!_config->username.empty()) { - string errmsg; + std::string errmsg; if (!conn->auth("admin", _config->username, _config->password, errmsg)) { uasserted(15932, "Authenticating to connection for benchThread failed: " + errmsg); } @@ -1165,7 +1165,7 @@ void BenchRunner::start() { std::unique_ptr<DBClientBase> conn(_config->createConnection()); // Must authenticate to admin db in order to run serverStatus command if (_config->username != "") { - string errmsg; + std::string errmsg; if (!conn->auth("admin", _config->username, _config->password, errmsg)) { uasserted( 16704, @@ -1201,7 +1201,7 @@ void BenchRunner::stop() { { std::unique_ptr<DBClientBase> conn(_config->createConnection()); if (_config->username != "") { - string errmsg; + std::string errmsg; // this can only fail if admin access was revoked since start of run if (!conn->auth("admin", _config->username, _config->password, errmsg)) { uasserted( diff --git a/src/mongo/shell/dbshell.cpp b/src/mongo/shell/dbshell.cpp index 801130d07c5..90c59f0ee55 100644 --- a/src/mongo/shell/dbshell.cpp +++ b/src/mongo/shell/dbshell.cpp @@ -43,6 +43,7 @@ #include "mongo/base/initializer.h" #include "mongo/base/status.h" #include "mongo/client/dbclientinterface.h" +#include "mongo/client/mongo_uri.h" #include "mongo/client/sasl_client_authenticate.h" #include "mongo/db/client.h" #include "mongo/db/log_process_details.h" @@ -215,57 +216,151 @@ void setupSignals() { signal(SIGINT, quitNicely); } -string getURIFromArgs(const std::string& url, const std::string& host, const std::string& port) { - if (host.size() == 0 && port.size() == 0) { - return url.size() == 0 ? kDefaultMongoURL.toString() : url; +string getURIFromArgs(const std::string& arg, const std::string& host, const std::string& port) { + if (host.empty() && arg.empty() && port.empty()) { + // Nothing provided, just play the default. + return kDefaultMongoURL.toString(); } - // The name URL is misleading; really it's just a positional argument that wasn't a file. The - // check for "/" means "this 'URL' is probably a real URL and not the db name (e.g.)". - if (url.find("/") != string::npos) { - cerr << "if a full URI is provided, you cannot also specify host or port" << endl; + if (str::startsWith(arg, "mongodb://") && host.empty() && port.empty()) { + // mongo mongodb://blah + return arg; + } + if (str::startsWith(host, "mongodb://") && arg.empty() && port.empty()) { + // mongo --host mongodb://blah + return host; + } + + // We expect a positional arg to be a plain dbname or plain hostname at this point + // since we have separate host/port args. + if ((arg.find('/') != string::npos) && (host.size() || port.size())) { + cerr << "If a full URI is provided, you cannot also specify --host or --port" << endl; quickExit(-1); } - bool hostEndsInSock = str::endsWith(host, ".sock"); + const auto parseDbHost = [port](const std::string& db, const std::string& host) -> std::string { + // Parse --host as a connection string. + // e.g. rs0/host0:27000,host1:27001 + const auto slashPos = host.find('/'); + const auto hasReplSet = (slashPos > 0) && (slashPos != std::string::npos); + + std::ostringstream ss; + ss << "mongodb://"; + + // Handle each sub-element of the connection string individually. + // Comma separated list of host elements. + // Each host element may be: + // * /unix/domain.sock + // * hostname + // * hostname:port + // If --port is specified and port is included in connection string, + // then they must match exactly. + auto start = hasReplSet ? slashPos + 1 : 0; + while (start < host.size()) { + // Encode each host component. + auto end = host.find(',', start); + if (end == std::string::npos) { + end = host.size(); + } + if ((end - start) == 0) { + // Ignore empty components. + start = end + 1; + continue; + } + + const auto hostElem = host.substr(start, end - start); + if ((hostElem.find('/') != std::string::npos) && str::endsWith(hostElem, ".sock")) { + // Unix domain socket, ignore --port. + ss << uriEncode(hostElem); + + } else { + auto colon = hostElem.find(':'); + if ((colon != std::string::npos) && + (hostElem.find(':', colon + 1) != std::string::npos)) { + // Looks like an IPv6 numeric address. + const auto close = hostElem.find(']'); + if ((hostElem[0] == '[') && (close != std::string::npos)) { + // Encapsulated already. + ss << '[' << uriEncode(hostElem.substr(1, close - 1), ":") << ']'; + colon = hostElem.find(':', close + 1); + } else { + // Not encapsulated yet. + ss << '[' << uriEncode(hostElem, ":") << ']'; + colon = std::string::npos; + } + } else if (colon != std::string::npos) { + // Not IPv6 numeric, but does have a port. + ss << uriEncode(hostElem.substr(0, colon)); + } else { + // Raw hostname/IPv4 without port. + ss << uriEncode(hostElem); + } + + if (colon != std::string::npos) { + // Have a port in our host element, verify it. + const auto myport = hostElem.substr(colon + 1); + if (port.size() && (port != myport)) { + cerr << "connection string bears different port than provided by --port" + << endl; + quickExit(-1); + } + ss << ':' << uriEncode(myport); + } else if (port.size()) { + ss << ':' << uriEncode(port); + } else { + ss << ":27017"; + } + } + start = end + 1; + if (start < host.size()) { + ss << ','; + } + } + + ss << '/' << uriEncode(db); - // If host looks like a full URI (i.e. has a slash and isn't a unix socket) and the other fields - // are empty, then just return host. - std::string::size_type slashPos; - if (url.size() == 0 && port.size() == 0 && - (!hostEndsInSock && ((slashPos = host.find("/")) != string::npos))) { - if (str::startsWith(host, "mongodb://")) { - return host; + if (hasReplSet) { + // Remap included replica set name to URI option + ss << "?replicaSet=" << uriEncode(host.substr(0, slashPos)); } - // If there's a slash in the host field, then it's the replica set name, not a database name - stringstream ss; - ss << "mongodb://" << host.substr(slashPos + 1) - << "/?replicaSet=" << host.substr(0, slashPos); + return ss.str(); - } + }; + + if (host.size()) { + // --host provided, treat it as the connect string and get db from positional arg. + return parseDbHost(arg, host); + } else if (arg.size()) { + // --host missing, but we have a potential host/db positional arg. + const auto slashPos = arg.find('/'); + if (slashPos != std::string::npos) { + // host/db pair. + return parseDbHost(arg.substr(slashPos + 1), arg.substr(0, slashPos)); + } - stringstream ss; - if (host.size() == 0) { - ss << "mongodb://127.0.0.1"; - } else { - if (!str::startsWith(host, "mongodb://")) { - ss << "mongodb://"; + // Compatability formats. + // * Any arg with a dot is assumed to be a hostname or IPv4 numeric address. + // * Any arg with a colon followed by a digit assumed to be host or IP followed by port. + // * Anything else is assumed to be a db. + + if (arg.find('.') != std::string::npos) { + // Assume IPv4 or hostnameish. + return parseDbHost("test", arg); } - ss << host; - } - if (!hostEndsInSock) { - if (port.size() > 0) { - ss << ":" << port; - } else if (host.find(':') == string::npos || str::endsWith(host, "]")) { - // Default the port to 27017 if the host did not provide one (i.e. the host has no - // colons or ends in ']' like an IPv6 address). - ss << ":27017"; + const auto colonPos = arg.find(':'); + if ((colonPos != std::string::npos) && ((colonPos + 1) < arg.size()) && + isdigit(arg[colonPos + 1])) { + // Assume IPv4 or hostname with port. + return parseDbHost("test", arg); } + + // db, assume localhost. + return parseDbHost(arg, "127.0.0.1"); } - ss << "/" << url; - return ss.str(); + // --host empty, position arg empty, fallback on localhost without a dbname. + return parseDbHost("", "127.0.0.1"); } static string OpSymbols = "~!%^&*-+=|:,<>/?."; diff --git a/src/mongo/shell/mongo.js b/src/mongo/shell/mongo.js index 69971609bb8..92fc83b38b9 100644 --- a/src/mongo/shell/mongo.js +++ b/src/mongo/shell/mongo.js @@ -223,6 +223,9 @@ connect = function(url, user, pass) { if (!url.startsWith("mongodb://")) { const colon = url.lastIndexOf(":"); const slash = url.lastIndexOf("/"); + if (url.split("/").length > 1) { + url = url.substring(0, slash).replace(/\//g, "%2F") + url.substring(slash); + } if (slash == 0) { throw Error("Failed to parse mongodb:// URL: " + url); } @@ -235,7 +238,7 @@ connect = function(url, user, pass) { chatty("connecting to: " + url); var m = new Mongo(url); - db = m.getDB(m.defaultDB); + var db = m.getDB(m.defaultDB); if (user && pass) { if (!db.auth(user, pass)) { diff --git a/src/mongo/shell/replsettest.js b/src/mongo/shell/replsettest.js index 0e05a1bfa4a..9d6a4378009 100644 --- a/src/mongo/shell/replsettest.js +++ b/src/mongo/shell/replsettest.js @@ -427,8 +427,10 @@ var ReplSetTest = function(opts) { var member = {}; member._id = i; - var port = this.ports[i]; - member.host = this.host + ":" + port; + member.host = this.host; + if (!member.host.contains('/')) { + member.host += ":" + this.ports[i]; + } var nodeOpts = this.nodeOptions["n" + i]; if (nodeOpts) { @@ -470,7 +472,7 @@ var ReplSetTest = function(opts) { * * @param options - The options passed to {@link MongoRunner.runMongod} */ - this.startSet = function(options) { + this.startSet = function(options, restart) { print("ReplSetTest starting set"); if (options && options.keyFile) { @@ -482,7 +484,7 @@ var ReplSetTest = function(opts) { var nodes = []; for (var n = 0; n < this.ports.length; n++) { - nodes.push(this.start(n, options)); + nodes.push(this.start(n, options, restart)); } this.nodes = nodes; @@ -1883,7 +1885,11 @@ var ReplSetTest = function(opts) { * Constructor, which instantiates the ReplSetTest object from an existing set. */ function _constructFromExistingSeedNode(seedNode) { - var conf = _replSetGetConfig(new Mongo(seedNode)); + const conn = new Mongo(seedNode); + if (jsTest.options().keyFile) { + self.keyFile = jsTest.options().keyFile; + } + var conf = asCluster(conn, () => _replSetGetConfig(conn)); print('Recreating replica set from config ' + tojson(conf)); var existingNodes = conf.members.map(member => member.host); diff --git a/src/mongo/shell/servers_misc.js b/src/mongo/shell/servers_misc.js index fdb49deb76a..f419428e2d0 100644 --- a/src/mongo/shell/servers_misc.js +++ b/src/mongo/shell/servers_misc.js @@ -214,11 +214,21 @@ function startParallelShell(jsCode, port, noConnect) { var args = ["mongo"]; if (typeof db == "object") { - var hostAndPort = db.getMongo().host.split(':'); - var host = hostAndPort[0]; - args.push("--host", host); - if (!port && hostAndPort.length >= 2) { - var port = hostAndPort[1]; + if (!port) { + // If no port override specified, just passthrough connect string. + args.push("--host", db.getMongo().host); + } else { + // Strip port numbers from connect string. + const uri = new MongoURI(db.getMongo().host); + var connString = uri.servers + .map(function(server) { + return server.host; + }) + .join(','); + if (uri.setName.length > 0) { + connString = uri.setName + '/' + connString; + } + args.push("--host", connString); } } if (port) { diff --git a/src/mongo/shell/shardingtest.js b/src/mongo/shell/shardingtest.js index 9b06dcd89ce..7a0fdae70f3 100644 --- a/src/mongo/shell/shardingtest.js +++ b/src/mongo/shell/shardingtest.js @@ -370,10 +370,14 @@ var ShardingTest = function(params) { } }; - this.stop = function(opts) { + this.stopAllMongos = function(opts) { for (var i = 0; i < this._mongos.length; i++) { this.stopMongos(i, opts); } + }; + + this.stop = function(opts) { + this.stopAllMongos(opts); for (var i = 0; i < this._connections.length; i++) { if (this._rs[i]) { diff --git a/src/mongo/shell/utils.js b/src/mongo/shell/utils.js index 9794a60df4e..148eacc1c89 100644 --- a/src/mongo/shell/utils.js +++ b/src/mongo/shell/utils.js @@ -158,6 +158,18 @@ print.captureAllOutput = function(fn, args) { return res; }; +var indentStr = function(indent, s) { + if (typeof(s) === "undefined") { + s = indent; + indent = 0; + } + if (indent > 0) { + indent = (new Array(indent + 1)).join(" "); + s = indent + s.replace(/\n/g, "\n" + indent); + } + return s; +}; + if (typeof TestData == "undefined") { TestData = undefined; } @@ -230,7 +242,8 @@ jsTestOptions = function() { networkMessageCompressors: TestData.networkMessageCompressors, skipValidationOnInvalidViewDefinitions: TestData.skipValidationOnInvalidViewDefinitions, forceValidationWithFeatureCompatibilityVersion: - TestData.forceValidationWithFeatureCompatibilityVersion + TestData.forceValidationWithFeatureCompatibilityVersion, + skipValidationNamespaces: TestData.skipValidationNamespaces || [], }); } return _jsTestOptions; diff --git a/src/mongo/shell/utils_sh.js b/src/mongo/shell/utils_sh.js index ef8ae2dfa36..6e9c61a32e5 100644 --- a/src/mongo/shell/utils_sh.js +++ b/src/mongo/shell/utils_sh.js @@ -632,6 +632,18 @@ sh.getRecentMigrations = function(configDB) { return result; }; +sh._shardingStatusStr = function(indent, s) { + // convert from logical indentation to actual num of chars + if (indent == 0) { + indent = 0; + } else if (indent == 1) { + indent = 2; + } else { + indent = (indent - 1) * 8; + } + return indentStr(indent, s) + "\n"; +}; + function printShardingStatus(configDB, verbose) { // configDB is a DB object that contains the sharding metadata of interest. // Defaults to the db named "config" on the current connection. @@ -646,15 +658,15 @@ function printShardingStatus(configDB, verbose) { } var raw = ""; - var output = function(s) { - raw += s + "\n"; + var output = function(indent, s) { + raw += sh._shardingStatusStr(indent, s); }; - output("--- Sharding Status --- "); - output(" sharding version: " + tojson(configDB.getCollection("version").findOne())); + output(0, "--- Sharding Status --- "); + output(1, "sharding version: " + tojson(configDB.getCollection("version").findOne())); - output(" shards:"); + output(1, "shards:"); configDB.shards.find().sort({_id: 1}).forEach(function(z) { - output("\t" + tojsononeline(z)); + output(2, tojsononeline(z)); }); // (most recently) active mongoses @@ -672,9 +684,9 @@ function printShardingStatus(configDB, verbose) { } } - output(" " + mongosAdjective + " mongoses:"); + output(1, mongosAdjective + " mongoses:"); if (mostRecentMongosTime === null) { - output("\tnone"); + output(2, "none"); } else { var recentMongosQuery = { ping: { @@ -688,7 +700,7 @@ function printShardingStatus(configDB, verbose) { if (verbose) { configDB.mongos.find(recentMongosQuery).sort({ping: -1}).forEach(function(z) { - output("\t" + tojsononeline(z)); + output(2, tojsononeline(z)); }); } else { configDB.mongos @@ -698,23 +710,23 @@ function printShardingStatus(configDB, verbose) { {$sort: {num: -1}} ]) .forEach(function(z) { - output("\t" + tojson(z._id) + " : " + z.num); + output(2, tojson(z._id) + " : " + z.num); }); } } - output(" autosplit:"); + output(1, "autosplit:"); // Is autosplit currently enabled - output("\tCurrently enabled: " + (sh.getShouldAutoSplit(configDB) ? "yes" : "no")); + output(2, "Currently enabled: " + (sh.getShouldAutoSplit(configDB) ? "yes" : "no")); - output(" balancer:"); + output(1, "balancer:"); // Is the balancer currently enabled - output("\tCurrently enabled: " + (sh.getBalancerState(configDB) ? "yes" : "no")); + output(2, "Currently enabled: " + (sh.getBalancerState(configDB) ? "yes" : "no")); // Is the balancer currently active - output("\tCurrently running: " + (sh.isBalancerRunning(configDB) ? "yes" : "no")); + output(2, "Currently running: " + (sh.isBalancerRunning(configDB) ? "yes" : "no")); // Output details of the current balancer round var balLock = sh.getBalancerLockDetails(configDB); @@ -725,16 +737,17 @@ function printShardingStatus(configDB, verbose) { // Output the balancer window var balSettings = sh.getBalancerWindow(configDB); if (balSettings) { - output("\t\tBalancer active window is set between " + balSettings.start + " and " + - balSettings.stop + " server local time"); + output(3, + "Balancer active window is set between " + balSettings.start + " and " + + balSettings.stop + " server local time"); } // Output the list of active migrations var activeMigrations = sh.getActiveMigrations(configDB); if (activeMigrations.length > 0) { - output("\tCollections with active migrations: "); + output(2, "Collections with active migrations: "); activeMigrations.forEach(function(migration) { - output("\t\t" + migration._id + " started at " + migration.when); + output(3, migration._id + " started at " + migration.when); }); } @@ -755,31 +768,32 @@ function printShardingStatus(configDB, verbose) { // Review config.actionlog for errors var actionReport = sh.getRecentFailedRounds(configDB); // Always print the number of failed rounds - output("\tFailed balancer rounds in last 5 attempts: " + actionReport.count); + output(2, "Failed balancer rounds in last 5 attempts: " + actionReport.count); // Only print the errors if there are any if (actionReport.count > 0) { - output("\tLast reported error: " + actionReport.lastErr); - output("\tTime of Reported error: " + actionReport.lastTime); + output(2, "Last reported error: " + actionReport.lastErr); + output(2, "Time of Reported error: " + actionReport.lastTime); } - output("\tMigration Results for the last 24 hours: "); + output(2, "Migration Results for the last 24 hours: "); var migrations = sh.getRecentMigrations(configDB); if (migrations.length > 0) { migrations.forEach(function(x) { if (x._id === "Success") { - output("\t\t" + x.count + " : " + x._id); + output(3, x.count + " : " + x._id); } else { - output("\t\t" + x.count + " : Failed with error '" + x._id + "', from " + - x.from + " to " + x.to); + output(3, + x.count + " : Failed with error '" + x._id + "', from " + x.from + + " to " + x.to); } }); } else { - output("\t\tNo recent migrations"); + output(3, "No recent migrations"); } } - output(" databases:"); + output(1, "databases:"); configDB.databases.find().sort({name: 1}).forEach(function(db) { var truthy = function(value) { return !!value; @@ -795,20 +809,22 @@ function printShardingStatus(configDB, verbose) { return s; }; - output("\t" + tojsononeline(db, "", true)); + output(2, tojsononeline(db, "", true)); if (db.partitioned) { configDB.collections.find({_id: new RegExp("^" + RegExp.escape(db._id) + "\\.")}) .sort({_id: 1}) .forEach(function(coll) { if (!coll.dropped) { - output("\t\t" + coll._id); - output("\t\t\tshard key: " + tojson(coll.key)); - output("\t\t\tunique: " + truthy(coll.unique) + - nonBooleanNote("unique", coll.unique)); - output("\t\t\tbalancing: " + !truthy(coll.noBalance) + - nonBooleanNote("noBalance", coll.noBalance)); - output("\t\t\tchunks:"); + output(3, coll._id); + output(4, "shard key: " + tojson(coll.key)); + output(4, + "unique: " + truthy(coll.unique) + + nonBooleanNote("unique", coll.unique)); + output(4, + "balancing: " + !truthy(coll.noBalance) + + nonBooleanNote("noBalance", coll.noBalance)); + output(4, "chunks:"); res = configDB.chunks .aggregate({$match: {ns: coll._id}}, @@ -819,26 +835,29 @@ function printShardingStatus(configDB, verbose) { var totalChunks = 0; res.forEach(function(z) { totalChunks += z.nChunks; - output("\t\t\t\t" + z.shard + "\t" + z.nChunks); + output(5, z.shard + "\t" + z.nChunks); }); if (totalChunks < 20 || verbose) { configDB.chunks.find({"ns": coll._id}) .sort({min: 1}) .forEach(function(chunk) { - output("\t\t\t" + tojson(chunk.min) + " -->> " + - tojson(chunk.max) + " on : " + chunk.shard + " " + - tojson(chunk.lastmod) + " " + - (chunk.jumbo ? "jumbo " : "")); + output(4, + tojson(chunk.min) + " -->> " + tojson(chunk.max) + + " on : " + chunk.shard + " " + + tojson(chunk.lastmod) + " " + + (chunk.jumbo ? "jumbo " : "")); }); } else { output( - "\t\t\ttoo many chunks to print, use verbose if you want to force print"); + 4, + "too many chunks to print, use verbose if you want to force print"); } configDB.tags.find({ns: coll._id}).sort({min: 1}).forEach(function(tag) { - output("\t\t\t tag: " + tag.tag + " " + tojson(tag.min) + " -->> " + - tojson(tag.max)); + output(4, + " tag: " + tag.tag + " " + tojson(tag.min) + " -->> " + + tojson(tag.max)); }); } }); @@ -861,29 +880,29 @@ function printShardingSizes(configDB) { } var raw = ""; - var output = function(s) { - raw += s + "\n"; + var output = function(indent, s) { + raw += sh._shardingStatusStr(indent, s); }; - output("--- Sharding Status --- "); - output(" sharding version: " + tojson(configDB.getCollection("version").findOne())); + output(0, "--- Sharding Sizes --- "); + output(1, "sharding version: " + tojson(configDB.getCollection("version").findOne())); - output(" shards:"); + output(1, "shards:"); var shards = {}; configDB.shards.find().forEach(function(z) { shards[z._id] = new Mongo(z.host); - output(" " + tojson(z)); + output(2, tojson(z)); }); var saveDB = db; - output(" databases:"); + output(1, "databases:"); configDB.databases.find().sort({name: 1}).forEach(function(db) { - output("\t" + tojson(db, "", true)); + output(2, tojson(db, "", true)); if (db.partitioned) { configDB.collections.find({_id: new RegExp("^" + RegExp.escape(db._id) + "\.")}) .sort({_id: 1}) .forEach(function(coll) { - output("\t\t" + coll._id + " chunks:"); + output(3, coll._id + " chunks:"); configDB.chunks.find({"ns": coll._id}).sort({min: 1}).forEach(function(chunk) { var mydb = shards[chunk.shard].getDB(db._id); var out = mydb.runCommand({ @@ -895,8 +914,9 @@ function printShardingSizes(configDB) { delete out.millis; delete out.ok; - output("\t\t\t" + tojson(chunk.min) + " -->> " + tojson(chunk.max) + - " on : " + chunk.shard + " " + tojson(out)); + output(4, + tojson(chunk.min) + " -->> " + tojson(chunk.max) + " on : " + + chunk.shard + " " + tojson(out)); }); }); diff --git a/src/mongo/transport/SConscript b/src/mongo/transport/SConscript index 8e414610b2c..e82b6bd28fa 100644 --- a/src/mongo/transport/SConscript +++ b/src/mongo/transport/SConscript @@ -100,7 +100,9 @@ env.CppUnitTest( ], ) -env.Library( +messageCompressorEnv = env.Clone() +messageCompressorEnv.InjectThirdPartyIncludePaths(libraries=['snappy']) +messageCompressorEnv.Library( target='message_compressor', source=[ 'message_compressor_manager.cpp', diff --git a/src/mongo/transport/message_compressor_manager.cpp b/src/mongo/transport/message_compressor_manager.cpp index 669b091f422..9a5b5ca81a9 100644 --- a/src/mongo/transport/message_compressor_manager.cpp +++ b/src/mongo/transport/message_compressor_manager.cpp @@ -51,23 +51,31 @@ struct CompressionHeader { uint8_t compressorId; void serialize(DataRangeCursor* cursor) { - cursor->writeAndAdvance<LittleEndian<int32_t>>(originalOpCode); - cursor->writeAndAdvance<LittleEndian<int32_t>>(uncompressedSize); - cursor->writeAndAdvance<LittleEndian<uint8_t>>(compressorId); + uassertStatusOK(cursor->writeAndAdvance<LittleEndian<int32_t>>(originalOpCode)); + uassertStatusOK(cursor->writeAndAdvance<LittleEndian<int32_t>>(uncompressedSize)); + uassertStatusOK(cursor->writeAndAdvance<LittleEndian<uint8_t>>(compressorId)); } CompressionHeader(int32_t _opcode, int32_t _size, uint8_t _id) : originalOpCode{_opcode}, uncompressedSize{_size}, compressorId{_id} {} CompressionHeader(ConstDataRangeCursor* cursor) { - originalOpCode = cursor->readAndAdvance<LittleEndian<std::int32_t>>().getValue(); - uncompressedSize = cursor->readAndAdvance<LittleEndian<std::int32_t>>().getValue(); - compressorId = cursor->readAndAdvance<LittleEndian<uint8_t>>().getValue(); + originalOpCode = _readWithChecking<LittleEndian<std::int32_t>>(cursor); + uncompressedSize = _readWithChecking<LittleEndian<std::int32_t>>(cursor); + compressorId = _readWithChecking<LittleEndian<uint8_t>>(cursor); } static size_t size() { return sizeof(originalOpCode) + sizeof(uncompressedSize) + sizeof(compressorId); } + +private: + template <typename T> + T _readWithChecking(ConstDataRangeCursor* cursor) { + auto sw = cursor->readAndAdvance<T>(); + uassertStatusOK(sw.getStatus()); + return sw.getValue(); + } }; } // namespace @@ -124,6 +132,9 @@ StatusWith<Message> MessageCompressorManager::compressMessage(const Message& msg StatusWith<Message> MessageCompressorManager::decompressMessage(const Message& msg) { auto inputHeader = msg.header(); ConstDataRangeCursor input(inputHeader.data(), inputHeader.data() + inputHeader.dataLen()); + if (input.length() < CompressionHeader::size()) { + return {ErrorCodes::BadValue, "Invalid compressed message header"}; + } CompressionHeader compressionHeader(&input); auto compressor = _registry->getCompressor(compressionHeader.compressorId); @@ -132,7 +143,12 @@ StatusWith<Message> MessageCompressorManager::decompressMessage(const Message& m "Compression algorithm specified in message is not available"}; } - auto bufferSize = compressionHeader.uncompressedSize + MsgData::MsgDataHeaderSize; + size_t bufferSize = compressionHeader.uncompressedSize + MsgData::MsgDataHeaderSize; + if (bufferSize > MaxMessageSizeBytes) { + return {ErrorCodes::BadValue, + "Decompressed message would be larger than maximum message size"}; + } + auto outputMessageBuffer = SharedBuffer::allocate(bufferSize); MsgData::View outMessage(outputMessageBuffer.get()); outMessage.setId(inputHeader.getId()); diff --git a/src/mongo/transport/message_compressor_manager_test.cpp b/src/mongo/transport/message_compressor_manager_test.cpp index 383bb1e3260..5c533e45e75 100644 --- a/src/mongo/transport/message_compressor_manager_test.cpp +++ b/src/mongo/transport/message_compressor_manager_test.cpp @@ -26,6 +26,8 @@ * it in the license file. */ +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kNetwork + #include "mongo/platform/basic.h" #include "mongo/bson/bsonobjbuilder.h" @@ -33,7 +35,9 @@ #include "mongo/transport/message_compressor_manager.h" #include "mongo/transport/message_compressor_noop.h" #include "mongo/transport/message_compressor_registry.h" +#include "mongo/transport/message_compressor_snappy.h" #include "mongo/unittest/unittest.h" +#include "mongo/util/log.h" #include "mongo/util/net/message.h" #include <string> @@ -120,6 +124,41 @@ void checkFidelity(const Message& msg, std::unique_ptr<MessageCompressorBase> co ASSERT_EQ(memcmp(decompressedMsgView.data(), originalView.data(), originalView.dataLen()), 0); } +void checkOverflow(std::unique_ptr<MessageCompressorBase> compressor) { + // This is our test data that we're going to try to compress/decompress into a buffer that's + // way too small. + const std::string data = + "We embrace reality. We apply high-quality thinking and rigor." + "We have courage in our convictions but work hard to ensure biases " + "or personal beliefs do not get in the way of finding the best solution."; + ConstDataRange input(data.data(), data.size()); + + // This is our tiny buffer that should cause an error. + std::array<char, 16> smallBuffer; + DataRange smallOutput(smallBuffer.data(), smallBuffer.size()); + + // This is a normal sized buffer that we can store a compressed version of our test data safely + std::vector<char> normalBuffer; + normalBuffer.resize(compressor->getMaxCompressedSize(data.size())); + auto sws = compressor->compressData(input, DataRange(normalBuffer.data(), normalBuffer.size())); + ASSERT_OK(sws); + DataRange normalRange = DataRange(normalBuffer.data(), sws.getValue()); + + // Check that compressing the test data into a small buffer fails + ASSERT_NOT_OK(compressor->compressData(input, smallOutput)); + + // Check that decompressing compressed test data into a small buffer fails + ASSERT_NOT_OK(compressor->decompressData(normalRange, smallOutput)); + + // Check that decompressing a valid buffer that's missing data doesn't overflow the + // source buffer. + std::vector<char> scratch; + scratch.resize(data.size()); + ConstDataRange tooSmallRange(normalBuffer.data(), normalBuffer.size() / 2); + ASSERT_NOT_OK( + compressor->decompressData(tooSmallRange, DataRange(scratch.data(), scratch.size()))); +} + Message buildMessage() { const auto data = std::string{"Hello, world!"}; const auto bufferSize = MsgData::MsgDataHeaderSize + data.size(); @@ -179,8 +218,54 @@ TEST(NoopMessageCompressor, Fidelity) { TEST(SnappyMessageCompressor, Fidelity) { auto testMessage = buildMessage(); - checkFidelity(testMessage, stdx::make_unique<NoopMessageCompressor>()); + checkFidelity(testMessage, stdx::make_unique<SnappyMessageCompressor>()); +} + +TEST(SnappyMessageCompressor, Overflow) { + checkOverflow(stdx::make_unique<SnappyMessageCompressor>()); +} + +TEST(MessageCompressorManager, MessageSizeTooLarge) { + auto registry = buildRegistry(); + MessageCompressorManager compManager(®istry); + + auto badMessageBuffer = SharedBuffer::allocate(128); + MsgData::View badMessage(badMessageBuffer.get()); + badMessage.setId(1); + badMessage.setResponseToMsgId(0); + badMessage.setOperation(dbCompressed); + badMessage.setLen(128); + + DataRangeCursor cursor(badMessage.data(), badMessage.data() + badMessage.dataLen()); + uassertStatusOK(cursor.writeAndAdvance<LittleEndian<int32_t>>(dbQuery)); + uassertStatusOK(cursor.writeAndAdvance<LittleEndian<int32_t>>(MaxMessageSizeBytes + 1)); + uassertStatusOK( + cursor.writeAndAdvance<LittleEndian<uint8_t>>(registry.getCompressor("noop")->getId())); + + auto status = compManager.decompressMessage(Message(badMessageBuffer)).getStatus(); + ASSERT_NOT_OK(status); +} + +TEST(MessageCompressorManager, RuntMessage) { + auto registry = buildRegistry(); + MessageCompressorManager compManager(®istry); + + auto badMessageBuffer = SharedBuffer::allocate(128); + MsgData::View badMessage(badMessageBuffer.get()); + badMessage.setId(1); + badMessage.setResponseToMsgId(0); + badMessage.setOperation(dbCompressed); + badMessage.setLen(MsgData::MsgDataHeaderSize + 8); + + // This is a totally bogus compression header of just the orginal opcode + 0 byte uncompressed + // size + DataRangeCursor cursor(badMessage.data(), badMessage.data() + badMessage.dataLen()); + uassertStatusOK(cursor.writeAndAdvance<LittleEndian<int32_t>>(dbQuery)); + uassertStatusOK(cursor.writeAndAdvance<LittleEndian<int32_t>>(0)); + + auto status = compManager.decompressMessage(Message(badMessageBuffer)).getStatus(); + ASSERT_NOT_OK(status); } -} // namespace mongo } // namespace +} // namespace mongo diff --git a/src/mongo/transport/message_compressor_snappy.cpp b/src/mongo/transport/message_compressor_snappy.cpp index db1e0c9dfca..9d523fca661 100644 --- a/src/mongo/transport/message_compressor_snappy.cpp +++ b/src/mongo/transport/message_compressor_snappy.cpp @@ -30,12 +30,13 @@ #include "mongo/platform/basic.h" +#include "mongo/base/data_range_cursor.h" #include "mongo/base/init.h" #include "mongo/stdx/memory.h" #include "mongo/transport/message_compressor_registry.h" #include "mongo/transport/message_compressor_snappy.h" -#include "third_party/snappy-1.1.3/snappy.h" +#include <snappy.h> namespace mongo { @@ -48,7 +49,10 @@ std::size_t SnappyMessageCompressor::getMaxCompressedSize(size_t inputSize) { StatusWith<std::size_t> SnappyMessageCompressor::compressData(ConstDataRange input, DataRange output) { - size_t outLength; + size_t outLength = output.length(); + if (output.length() < getMaxCompressedSize(input.length())) { + return {ErrorCodes::BadValue, "Output too small for max size of compressed input"}; + } snappy::RawCompress(input.data(), input.length(), const_cast<char*>(output.data()), &outLength); counterHitCompress(input.length(), outLength); @@ -57,10 +61,13 @@ StatusWith<std::size_t> SnappyMessageCompressor::compressData(ConstDataRange inp StatusWith<std::size_t> SnappyMessageCompressor::decompressData(ConstDataRange input, DataRange output) { - bool ret = - snappy::RawUncompress(input.data(), input.length(), const_cast<char*>(output.data())); + size_t expectedLength = 0; + if (!snappy::GetUncompressedLength(input.data(), input.length(), &expectedLength) || + expectedLength != output.length()) { + return {ErrorCodes::BadValue, "Compressed message was invalid or corrupted"}; + } - if (!ret) { + if (!snappy::RawUncompress(input.data(), input.length(), const_cast<char*>(output.data()))) { return Status{ErrorCodes::BadValue, "Compressed message was invalid or corrupted"}; } diff --git a/src/mongo/util/SConscript b/src/mongo/util/SConscript index 3346a596763..fe0380c1345 100644 --- a/src/mongo/util/SConscript +++ b/src/mongo/util/SConscript @@ -500,6 +500,17 @@ env.CppUnitTest( ) env.CppUnitTest( + target='producer_consumer_queue_test', + source=[ + 'producer_consumer_queue_test.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/db/service_context', + ] +) + +env.CppUnitTest( target='duration_test', source=[ 'duration_test.cpp', diff --git a/src/mongo/util/assert_util.cpp b/src/mongo/util/assert_util.cpp index 9d40b1f5d62..500a8cbb5eb 100644 --- a/src/mongo/util/assert_util.cpp +++ b/src/mongo/util/assert_util.cpp @@ -232,10 +232,9 @@ NOINLINE_DECL void msgassertedWithLocation(int msgid, const char* msg, const char* file, unsigned line) { - assertionCount.condrollover(++assertionCount.warning); + assertionCount.condrollover(++assertionCount.msg); log() << "Assertion: " << msgid << ":" << redact(msg) << ' ' << file << ' ' << dec << line << endl; - logContext(); throw MsgAssertionException(msgid, msg); } @@ -243,7 +242,7 @@ NOINLINE_DECL void msgassertedNoTraceWithLocation(int msgid, const char* msg, const char* file, unsigned line) { - assertionCount.condrollover(++assertionCount.warning); + assertionCount.condrollover(++assertionCount.msg); log() << "Assertion: " << msgid << ":" << redact(msg) << ' ' << file << ' ' << dec << line << endl; throw MsgAssertionException(msgid, msg); diff --git a/src/mongo/util/cmdline_utils/censor_cmdline.cpp b/src/mongo/util/cmdline_utils/censor_cmdline.cpp index 044de375e56..e903242ac75 100644 --- a/src/mongo/util/cmdline_utils/censor_cmdline.cpp +++ b/src/mongo/util/cmdline_utils/censor_cmdline.cpp @@ -45,6 +45,7 @@ static bool _isPasswordArgument(const char* argumentName) { "net.ssl.clusterPassword", "processManagement.windowsService.servicePassword", "security.kmip.clientCertificatePassword", + "security.ldap.bind.queryPassword", NULL // Last entry sentinel. }; for (const char* const* current = passwordArguments; *current; ++current) { @@ -60,6 +61,7 @@ static bool _isPasswordSwitch(const char* switchName) { "sslClusterPassword", "servicePassword", "kmipClientCertificatePassword", + "ldapQueryPassword", NULL // Last entry sentinel. }; diff --git a/src/mongo/util/concurrency/with_lock.h b/src/mongo/util/concurrency/with_lock.h new file mode 100644 index 00000000000..e1607e361cf --- /dev/null +++ b/src/mongo/util/concurrency/with_lock.h @@ -0,0 +1,111 @@ +/** Copyright 2017 MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#pragma once + +#include "mongo/stdx/mutex.h" +#include "mongo/util/assert_util.h" + +#include <utility> + +namespace mongo { + +/** + * WithLock is an attestation to pass as an argument to functions that must be called only while + * holding a lock, as a rigorous alternative to an unchecked naming convention and/or stern + * comments. It helps prevent a common usage error. + * + * It may be used to modernize code from (something like) this + * + * // Member _mutex MUST be held when calling this: + * void _clobber_inlock(OperationContext* opCtx) { + * _stuff = makeStuff(opCtx); + * } + * + * into + * + * void _clobber(WithLock, OperationContext* opCtx) { + * _stuff = makeStuff(opCtx); + * } + * + * A call to such a function looks like this: + * + * stdx::lock_guard<stdx::mutex> lk(_mutex); + * _clobber(lk, opCtx); // instead of _clobber_inlock(opCtx) + * + * Note that the formal argument need not (and should not) be named unless it is needed to pass + * the attestation along to another function: + * + * void _clobber(WithLock lock, OperationContext* opCtx) { + * _really_clobber(lock, opCtx); + * } + * + */ +struct WithLock { + template <typename Mutex> + WithLock(stdx::lock_guard<Mutex> const&) noexcept {} + + template <typename Mutex> + WithLock(stdx::unique_lock<Mutex> const& lock) noexcept { + invariant(lock.owns_lock()); + } + + // Add constructors from any other lock types here. + + // Pass by value is OK. + WithLock(WithLock const&) noexcept {} + WithLock(WithLock&&) noexcept {} + + // No assigning WithLocks. + void operator=(WithLock const&) = delete; + void operator=(WithLock&&) = delete; + + // No moving a lock_guard<> or unique_lock<> in. + template <typename Mutex> + WithLock(stdx::lock_guard<Mutex>&&) = delete; + template <typename Mutex> + WithLock(stdx::unique_lock<Mutex>&&) = delete; + + /* + * Produces a WithLock without benefit of any actual lock, for use in cases where a lock is not + * really needed, such as in many (but not all!) constructors. + */ + static WithLock withoutLock() noexcept { + return {}; + } + +private: + WithLock() noexcept = default; +}; + +} // namespace mongo + +namespace std { +// No moving a WithLock: +template <> +mongo::WithLock&& move<mongo::WithLock>(mongo::WithLock&&) noexcept = delete; +} // namespace std diff --git a/src/mongo/util/net/hostandport.cpp b/src/mongo/util/net/hostandport.cpp index abd8f2d6ade..4b46938fbdb 100644 --- a/src/mongo/util/net/hostandport.cpp +++ b/src/mongo/util/net/hostandport.cpp @@ -95,7 +95,9 @@ void HostAndPort::append(StringBuilder& ss) const { } else { ss << host(); } - ss << ':' << port(); + if (host().find('/') == std::string::npos) { + ss << ':' << port(); + } } bool HostAndPort::empty() const { diff --git a/src/mongo/util/net/miniwebserver.cpp b/src/mongo/util/net/miniwebserver.cpp index 5f4165d42f4..9fe847168ee 100644 --- a/src/mongo/util/net/miniwebserver.cpp +++ b/src/mongo/util/net/miniwebserver.cpp @@ -45,6 +45,7 @@ namespace mongo { using std::shared_ptr; +using std::string; using std::stringstream; using std::vector; diff --git a/src/mongo/util/net/ssl_manager.cpp b/src/mongo/util/net/ssl_manager.cpp index 187c0c105e2..4bb0befb4b0 100644 --- a/src/mongo/util/net/ssl_manager.cpp +++ b/src/mongo/util/net/ssl_manager.cpp @@ -37,6 +37,7 @@ #include <boost/thread/thread.hpp> #include <iostream> #include <sstream> +#include <stack> #include <string> #include <vector> @@ -171,21 +172,28 @@ const STACK_OF(X509_EXTENSION) * X509_get0_extensions(const X509* peerCert) { * OpenSSL before version 1.1.0 requires applications provide a callback which emits a thread * identifier. This ID is used to store thread specific ERR information. When a thread is * terminated, it must call ERR_remove_state or ERR_remove_thread_state. These functions may - * themselves invoke the application provided callback. + * themselves invoke the application provided callback. These IDs are stored in a hashtable with + * a questionable hash function. They must be uniformly distributed to prevent collisions. */ class SSLThreadInfo { public: static unsigned long getID() { - enforceCleanupOnShutdown(); + struct ThreadLocalState { + bool firstCall; + unsigned long id; + }; + static MONGO_TRIVIALLY_CONSTRUCTIBLE_THREAD_LOCAL ThreadLocalState state{true, 0}; + if (state.firstCall) { + state.id = _idManager.reserveID(); + unsigned long id = state.id; + boost::this_thread::at_thread_exit([id] { + ERR_remove_state(0); + _idManager.releaseID(id); + }); + state.firstCall = false; + } -#ifdef _WIN32 - return GetCurrentThreadId(); -#else - static_assert(sizeof(void*) == sizeof(unsigned long), - "OpenSSL needs the address of a thread-unique object to be castable to" - "unsigned long"); - return reinterpret_cast<unsigned long>(&errno); -#endif + return state.id; } static void lockingCallback(int mode, int type, const char* file, int line) { @@ -208,21 +216,39 @@ public: private: SSLThreadInfo() = delete; - // When called, ensures that this thread will, on termination, call ERR_remove_state. - static void enforceCleanupOnShutdown() { - static MONGO_TRIVIALLY_CONSTRUCTIBLE_THREAD_LOCAL bool firstCall = true; - if (firstCall) { - boost::this_thread::at_thread_exit([] { ERR_remove_state(0); }); - firstCall = false; - } - } - // Note: see SERVER-8734 for why we are using a recursive mutex here. // Once the deadlock fix in OpenSSL is incorporated into most distros of // Linux, this can be changed back to a nonrecursive mutex. static std::vector<std::unique_ptr<stdx::recursive_mutex>> _mutex; + + class ThreadIDManager { + public: + unsigned long reserveID() { + stdx::unique_lock<stdx::mutex> lock(_idMutex); + if (!_idLast.empty()) { + unsigned long ret = _idLast.top(); + _idLast.pop(); + return ret; + } + return ++_idNext; + } + + void releaseID(unsigned long id) { + stdx::unique_lock<stdx::mutex> lock(_idMutex); + _idLast.push(id); + } + + private: + // Machinery for producing IDs that are unique for the life of a thread. + stdx::mutex _idMutex; // Protects _idNext and _idLast. + unsigned long _idNext = 0; // Stores the next thread ID to use, if none already allocated. + std::stack<unsigned long, std::vector<unsigned long>> + _idLast; // Stores old thread IDs, for reuse. + }; + static ThreadIDManager _idManager; }; std::vector<std::unique_ptr<stdx::recursive_mutex>> SSLThreadInfo::_mutex; +SSLThreadInfo::ThreadIDManager SSLThreadInfo::_idManager; // We only want to free SSL_CTX objects if they have been populated. OpenSSL seems to perform this // check before freeing them, but because it does not document this, we should protect ourselves. diff --git a/src/mongo/util/options_parser/options_parser.cpp b/src/mongo/util/options_parser/options_parser.cpp index 5d03910a390..63bf184ec85 100644 --- a/src/mongo/util/options_parser/options_parser.cpp +++ b/src/mongo/util/options_parser/options_parser.cpp @@ -341,6 +341,11 @@ Status YAMLNodeToValue(const YAML::Node& YAMLNode, return Status::OK(); } + if (!YAMLNode.IsScalar() && !YAMLNode.IsNull()) { + return Status(ErrorCodes::BadValue, + str::stream() << "Scalar option '" << key << "' must be a single value"); + } + // Our YAML parser reads everything as a string, so we need to parse it ourselves. std::string stringVal = YAMLNode.Scalar(); return stringToValue(stringVal, type, key, value); diff --git a/src/mongo/util/options_parser/options_parser_test.cpp b/src/mongo/util/options_parser/options_parser_test.cpp index a0d97c512bb..aeb4a875914 100644 --- a/src/mongo/util/options_parser/options_parser_test.cpp +++ b/src/mongo/util/options_parser/options_parser_test.cpp @@ -1834,6 +1834,82 @@ TEST(Parsing, BadConfigFileOption) { ASSERT_NOT_OK(parser.run(testOpts, argv, env_map, &environment)); } +TEST(Parsing, MapForScalarMismatch) { + OptionsParserTester parser; + moe::Environment environment; + moe::OptionSection testOpts; + + testOpts.addOptionChaining("config", "config", moe::Int, "Config file to parse"); + testOpts.addOptionChaining("str", "str", moe::String, ""); + + std::vector<std::string> argv; + argv.push_back("binaryname"); + argv.push_back("--config"); + argv.push_back("config.json"); + std::map<std::string, std::string> env_map; + + parser.setConfig("config.json", R"cfg({ str: { elem: "val" } })cfg"); + + ASSERT_NOT_OK(parser.run(testOpts, argv, env_map, &environment)); +} + +TEST(Parsing, ScalarForMapMismatch) { + OptionsParserTester parser; + moe::Environment environment; + moe::OptionSection testOpts; + + testOpts.addOptionChaining("config", "config", moe::Int, "Config file to parse"); + testOpts.addOptionChaining("strmap", "strmap", moe::StringMap, ""); + + std::vector<std::string> argv; + argv.push_back("binaryname"); + argv.push_back("--config"); + argv.push_back("config.json"); + std::map<std::string, std::string> env_map; + + parser.setConfig("config.json", R"cfg({ str: "val" })cfg"); + + ASSERT_NOT_OK(parser.run(testOpts, argv, env_map, &environment)); +} + +TEST(Parsing, ListForScalarMismatch) { + OptionsParserTester parser; + moe::Environment environment; + moe::OptionSection testOpts; + + testOpts.addOptionChaining("config", "config", moe::Int, "Config file to parse"); + testOpts.addOptionChaining("str", "str", moe::String, ""); + + std::vector<std::string> argv; + argv.push_back("binaryname"); + argv.push_back("--config"); + argv.push_back("config.json"); + std::map<std::string, std::string> env_map; + + parser.setConfig("config.json", R"cfg({ str: ["val"] })cfg"); + + ASSERT_NOT_OK(parser.run(testOpts, argv, env_map, &environment)); +} + +TEST(Parsing, ScalarForListMismatch) { + OptionsParserTester parser; + moe::Environment environment; + moe::OptionSection testOpts; + + testOpts.addOptionChaining("config", "config", moe::Int, "Config file to parse"); + testOpts.addOptionChaining("strlist", "strlist", moe::StringVector, ""); + + std::vector<std::string> argv; + argv.push_back("binaryname"); + argv.push_back("--config"); + argv.push_back("config.json"); + std::map<std::string, std::string> env_map; + + parser.setConfig("config.json", R"cfg({ str: "val" })cfg"); + + ASSERT_NOT_OK(parser.run(testOpts, argv, env_map, &environment)); +} + TEST(ConfigFromFilesystem, JSONGood) { moe::OptionsParser parser; moe::Environment environment; diff --git a/src/mongo/util/password.cpp b/src/mongo/util/password.cpp index 11c936bac45..4814cbb3b12 100644 --- a/src/mongo/util/password.cpp +++ b/src/mongo/util/password.cpp @@ -46,7 +46,7 @@ namespace mongo { string askPassword() { std::string password; - cout << "Enter password: "; + cerr << "Enter password: "; #ifndef _WIN32 const int stdinfd = 0; termios termio; @@ -102,7 +102,7 @@ string askPassword() { return string(); } #endif - cout << "\n"; + cerr << "\n"; return password; } } diff --git a/src/mongo/util/producer_consumer_queue.h b/src/mongo/util/producer_consumer_queue.h new file mode 100644 index 00000000000..f0e942a7d94 --- /dev/null +++ b/src/mongo/util/producer_consumer_queue.h @@ -0,0 +1,559 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#pragma once + +#include <boost/optional.hpp> +#include <deque> +#include <list> +#include <queue> +#include <stack> + +#include "mongo/db/operation_context.h" +#include "mongo/stdx/condition_variable.h" +#include "mongo/stdx/mutex.h" +#include "mongo/stdx/type_traits.h" +#include "mongo/util/concurrency/with_lock.h" +#include "mongo/util/scopeguard.h" + +namespace mongo { + +namespace producer_consumer_queue_detail { + +/** + * The default cost function for the producer consumer queue. + * + * By default, all items in the queue have equal weight. + */ +struct DefaultCostFunction { + template <typename T> + size_t operator()(const T&) const { + return 1; + } +}; + +// Various helpers to tighten down whether the args getting passed are valid interruption args. +// +// Whatever the caller passes in the interruption args, they need to be invocable on one of +// these helpers. std::is_invocable would do the job in C++17 +std::false_type areInterruptionArgsHelper(...) { + return {}; +} + +std::true_type areInterruptionArgsHelper(OperationContext*) { + return {}; +} + +std::true_type areInterruptionArgsHelper(OperationContext*, Date_t) { + return {}; +} + +std::true_type areInterruptionArgsHelper(Milliseconds) { + return {}; +} + +std::true_type areInterruptionArgsHelper(Date_t) { + return {}; +} + +template <typename U, typename... InterruptionArgs> +auto areInterruptionArgs(U&& u, InterruptionArgs&&... args) + -> decltype(areInterruptionArgsHelper(std::forward<U>(u), + std::forward<InterruptionArgs>(args)...)) { + return areInterruptionArgsHelper(std::forward<U>(u), std::forward<InterruptionArgs>(args)...); +} + +std::true_type areInterruptionArgs() { + return {}; +} + +} // namespace producer_consumer_queue_detail + +/** + * A bounded, blocking, thread safe, cost parametrizable, single producer, multi-consumer queue. + * + * Properties: + * bounded - the queue can be limited in the number of items it can hold + * blocking - when the queue is full, or has no entries, callers block + * thread safe - the queue can be accessed safely from multiple threads at the same time + * cost parametrizable - the cost of items in the queue need not be equal. I.e. your items could + * be discrete byte buffers and the queue depth measured in bytes, so that + * the queue could hold one large buffer, or many smaller ones + * single producer - Only one thread may push work into the queue + * multi-consumer - Any number of threads may pop work out of the queue + * + * Interruptibility: + * All of the blocking methods on this type allow for 6 kinds of interruptibility. The matrix is + * parameterized by (void|OperationContext*)|(void|Milliseconds|Date_t). These provide different + * kinds of waiting based on whether the method should be interruptible via opCtx, and then + * whether they should timeout via deadline or duration + * + * A contrived example: pcq.pop(opCtx, Minutes(1)) would be warranted if: + * - The caller is blocking on a client thread. (opCtx) + * - The caller needs to act periodically on inactivity. (the duration) + * + * Exceptions include: + * timeouts + * ErrorCodes::ExceededTimeLimit exceptions + * opCtx interrupts + * ErrorCodes::Interrupted exceptions + * closure of queue endpoints + * ErrorCodes::ProducerConsumerQueueEndClosed + * pushes with batches that exceed the max queue size + * ErrorCodes::ProducerConsumerQueueBatchTooLarge + * + * Cost Function: + * The cost function must have a call operator which takes a const T& and returns the cost in + * size_t units. It must be pure across moves for a given T and never return zero. The intent of + * the cost function is to express the kind of bounds the queue provides, rather than to + * specialize behavior for a type. I.e. you should not specialize the default cost function and + * the cost function should always be explicit in the type. + */ +template <typename T, typename CostFunc = producer_consumer_queue_detail::DefaultCostFunction> +class ProducerConsumerQueue { + +public: + // By default the queue depth is unlimited + ProducerConsumerQueue() + : ProducerConsumerQueue(std::numeric_limits<size_t>::max(), CostFunc{}) {} + + // Or it can be measured in whatever units your size function returns + explicit ProducerConsumerQueue(size_t size) : ProducerConsumerQueue(size, CostFunc{}) {} + + // If your cost function has meaningful state, you may also pass a non-default constructed + // instance + explicit ProducerConsumerQueue(size_t size, CostFunc costFunc) + : _max(size), _costFunc(std::move(costFunc)) {} + + ProducerConsumerQueue(const ProducerConsumerQueue&) = delete; + ProducerConsumerQueue& operator=(const ProducerConsumerQueue&) = delete; + + ProducerConsumerQueue(ProducerConsumerQueue&&) = delete; + ProducerConsumerQueue& operator=(ProducerConsumerQueue&&) = delete; + + ~ProducerConsumerQueue() { + invariant(!_producerWants); + invariant(!_consumers); + } + + // Pushes the passed T into the queue + // + // Leaves T unchanged if an interrupt exception is thrown while waiting for space + template < + typename... InterruptionArgs, + typename = stdx::enable_if_t<decltype(producer_consumer_queue_detail::areInterruptionArgs( + std::declval<InterruptionArgs>()...))::value>> + void push(T&& t, InterruptionArgs&&... interruptionArgs) { + _pushRunner([&](stdx::unique_lock<stdx::mutex>& lk) { + auto cost = _invokeCostFunc(t, lk); + uassert(ErrorCodes::ProducerConsumerQueueBatchTooLarge, + str::stream() << "cost of item (" << cost + << ") larger than maximum queue size (" + << _max + << ")", + cost <= _max); + + _waitForSpace(lk, cost, std::forward<InterruptionArgs>(interruptionArgs)...); + _push(lk, std::move(t)); + }); + } + + // Pushes all Ts into the queue + // + // Blocks until all of the Ts can be pushed at once + // + // StartIterator must be ForwardIterator + // + // Leaves the values underneath the iterators unchanged if an interrupt exception is thrown + // while waiting for space + // + // Lifecycle methods of T must not throw if you want to use this method, as there's no obvious + // mechanism to see what was and was not pushed if those do throw + template < + typename StartIterator, + typename EndIterator, + typename... InterruptionArgs, + typename = stdx::enable_if_t<decltype(producer_consumer_queue_detail::areInterruptionArgs( + std::declval<InterruptionArgs>()...))::value>> + void pushMany(StartIterator start, EndIterator last, InterruptionArgs&&... interruptionArgs) { + return _pushRunner([&](stdx::unique_lock<stdx::mutex>& lk) { + size_t cost = 0; + for (auto iter = start; iter != last; ++iter) { + cost += _invokeCostFunc(*iter, lk); + } + + uassert(ErrorCodes::ProducerConsumerQueueBatchTooLarge, + str::stream() << "cost of items in batch (" << cost + << ") larger than maximum queue size (" + << _max + << ")", + cost <= _max); + + _waitForSpace(lk, cost, std::forward<InterruptionArgs>(interruptionArgs)...); + + for (auto iter = start; iter != last; ++iter) { + _push(lk, std::move(*iter)); + } + }); + } + + // Attempts a non-blocking push of a value + // + // Leaves T unchanged if it fails + bool tryPush(T&& t) { + return _pushRunner( + [&](stdx::unique_lock<stdx::mutex>& lk) { return _tryPush(lk, std::move(t)); }); + } + + // Pops one T out of the queue + template < + typename... InterruptionArgs, + typename = stdx::enable_if_t<decltype(producer_consumer_queue_detail::areInterruptionArgs( + std::declval<InterruptionArgs>()...))::value>> + T pop(InterruptionArgs&&... interruptionArgs) { + return _popRunner([&](stdx::unique_lock<stdx::mutex>& lk) { + _waitForNonEmpty(lk, std::forward<InterruptionArgs>(interruptionArgs)...); + return _pop(lk); + }); + } + + // Waits for at least one item in the queue, then pops items out of the queue until it would + // block + // + // OutputIterator must not throw on move assignment to *iter or popped values may be lost + // TODO: add sfinae to check to enforce + // + // Returns the cost value of the items extracted, along with the updated output iterator + template < + typename OutputIterator, + typename... InterruptionArgs, + typename = stdx::enable_if_t<decltype(producer_consumer_queue_detail::areInterruptionArgs( + std::declval<InterruptionArgs>()...))::value>> + std::pair<size_t, OutputIterator> popMany(OutputIterator iterator, + InterruptionArgs&&... interruptionArgs) { + return popManyUpTo(_max, iterator, std::forward<InterruptionArgs>(interruptionArgs)...); + } + + // Waits for at least one item in the queue, then pops items out of the queue until it would + // block, or we've exceeded our budget + // + // OutputIterator must not throw on move assignment to *iter or popped values may be lost + // TODO: add sfinae to check to enforce + // + // Returns the cost value of the items extracted, along with the updated output iterator + template < + typename OutputIterator, + typename... InterruptionArgs, + typename = stdx::enable_if_t<decltype(producer_consumer_queue_detail::areInterruptionArgs( + std::declval<InterruptionArgs>()...))::value>> + std::pair<size_t, OutputIterator> popManyUpTo(size_t budget, + OutputIterator iterator, + InterruptionArgs&&... interruptionArgs) { + return _popRunner([&](stdx::unique_lock<stdx::mutex>& lk) { + size_t cost = 0; + + _waitForNonEmpty(lk, std::forward<InterruptionArgs>(interruptionArgs)...); + + while (auto out = _tryPop(lk)) { + cost += _invokeCostFunc(*out, lk); + *iterator = std::move(*out); + ++iterator; + + if (cost >= budget) { + break; + } + } + + return std::make_pair(cost, iterator); + }); + } + + // Attempts a non-blocking pop of a value + boost::optional<T> tryPop() { + return _popRunner([&](stdx::unique_lock<stdx::mutex>& lk) { return _tryPop(lk); }); + } + + // Closes the producer end. Consumers will continue to consume until the queue is exhausted, at + // which time they will begin to throw with an interruption dbexception + void closeProducerEnd() { + stdx::lock_guard<stdx::mutex> lk(_mutex); + + _producerEndClosed = true; + + _notifyIfNecessary(lk); + } + + // Closes the consumer end. This causes all callers to throw with an interruption dbexception + void closeConsumerEnd() { + stdx::lock_guard<stdx::mutex> lk(_mutex); + + _consumerEndClosed = true; + _producerEndClosed = true; + + _notifyIfNecessary(lk); + } + + // TEST ONLY FUNCTIONS + + // Returns the current depth of the queue in CostFunction units + size_t sizeForTest() const { + stdx::lock_guard<stdx::mutex> lk(_mutex); + + return _current; + } + + // Returns true if the queue is empty + bool emptyForTest() const { + return sizeForTest() == 0; + } + +private: + size_t _invokeCostFunc(const T& t, WithLock) { + auto cost = _costFunc(t); + invariant(cost); + return cost; + } + + void _checkProducerClosed(WithLock) { + uassert( + ErrorCodes::ProducerConsumerQueueEndClosed, "Producer end closed", !_producerEndClosed); + uassert( + ErrorCodes::ProducerConsumerQueueEndClosed, "Consumer end closed", !_consumerEndClosed); + } + + void _checkConsumerClosed(WithLock) { + uassert( + ErrorCodes::ProducerConsumerQueueEndClosed, "Consumer end closed", !_consumerEndClosed); + uassert(ErrorCodes::ProducerConsumerQueueEndClosed, + "Producer end closed and values exhausted", + !(_producerEndClosed && _queue.empty())); + } + + void _notifyIfNecessary(WithLock) { + // If we've closed the consumer end, or if the production end is closed and we've exhausted + // the queue, wake everyone up and get out of here + if (_consumerEndClosed || (_queue.empty() && _producerEndClosed)) { + if (_consumers) { + _condvarConsumer.notify_all(); + } + + if (_producerWants) { + _condvarProducer.notify_one(); + } + + return; + } + + // If a producer is queued, and we have enough space for it to push its work + if (_producerWants && _current + _producerWants <= _max) { + _condvarProducer.notify_one(); + + return; + } + + // If we have consumers and anything in the queue, notify consumers + if (_consumers && _queue.size()) { + _condvarConsumer.notify_one(); + + return; + } + } + + template <typename Callback> + auto _pushRunner(Callback&& cb) -> decltype( + cb(std::declval<std::add_lvalue_reference<stdx::unique_lock<stdx::mutex>>::type>())) { + stdx::unique_lock<stdx::mutex> lk(_mutex); + + _checkProducerClosed(lk); + + const auto guard = MakeGuard([&] { _notifyIfNecessary(lk); }); + + return cb(lk); + } + + template <typename Callback> + auto _popRunner(Callback&& cb) -> decltype( + cb(std::declval<std::add_lvalue_reference<stdx::unique_lock<stdx::mutex>>::type>())) { + stdx::unique_lock<stdx::mutex> lk(_mutex); + + _checkConsumerClosed(lk); + + const auto guard = MakeGuard([&] { _notifyIfNecessary(lk); }); + + return cb(lk); + } + + bool _tryPush(WithLock wl, T&& t) { + size_t cost = _invokeCostFunc(t, wl); + if (_current + cost <= _max) { + _queue.emplace(std::move(t)); + _current += cost; + return true; + } + + return false; + } + + void _push(WithLock wl, T&& t) { + size_t cost = _invokeCostFunc(t, wl); + invariant(_current + cost <= _max); + + _queue.emplace(std::move(t)); + _current += cost; + } + + boost::optional<T> _tryPop(WithLock wl) { + boost::optional<T> out; + + if (!_queue.empty()) { + out.emplace(std::move(_queue.front())); + _queue.pop(); + _current -= _invokeCostFunc(*out, wl); + } + + return out; + } + + T _pop(WithLock wl) { + invariant(_queue.size()); + + auto t = std::move(_queue.front()); + _queue.pop(); + + _current -= _invokeCostFunc(t, wl); + + return t; + } + + template <typename... InterruptionArgs> + void _waitForSpace(stdx::unique_lock<stdx::mutex>& lk, + size_t cost, + InterruptionArgs&&... interruptionArgs) { + invariant(!_producerWants); + + _producerWants = cost; + const auto guard = MakeGuard([&] { _producerWants = 0; }); + + _waitFor(lk, + _condvarProducer, + [&] { + _checkProducerClosed(lk); + return _current + cost <= _max; + }, + std::forward<InterruptionArgs>(interruptionArgs)...); + } + + template <typename... InterruptionArgs> + void _waitForNonEmpty(stdx::unique_lock<stdx::mutex>& lk, + InterruptionArgs&&... interruptionArgs) { + + _consumers++; + const auto guard = MakeGuard([&] { _consumers--; }); + + _waitFor(lk, + _condvarConsumer, + [&] { + _checkConsumerClosed(lk); + return _queue.size(); + }, + std::forward<InterruptionArgs>(interruptionArgs)...); + } + + template <typename Callback> + void _waitFor(stdx::unique_lock<stdx::mutex>& lk, + stdx::condition_variable& condvar, + Callback&& pred, + OperationContext* opCtx) { + opCtx->waitForConditionOrInterrupt(condvar, lk, pred); + } + + template <typename Callback> + void _waitFor(stdx::unique_lock<stdx::mutex>& lk, + stdx::condition_variable& condvar, + Callback&& pred) { + condvar.wait(lk, pred); + } + + template <typename Callback> + void _waitFor(stdx::unique_lock<stdx::mutex>& lk, + stdx::condition_variable& condvar, + Callback&& pred, + OperationContext* opCtx, + Date_t deadline) { + uassert(ErrorCodes::ExceededTimeLimit, + "exceeded timeout", + opCtx->waitForConditionOrInterruptUntil(condvar, lk, deadline, pred)); + } + + template <typename Callback> + void _waitFor(stdx::unique_lock<stdx::mutex>& lk, + stdx::condition_variable& condvar, + Callback&& pred, + Date_t deadline) { + uassert(ErrorCodes::ExceededTimeLimit, + "exceeded timeout", + condvar.wait_until(lk, deadline.toSystemTimePoint(), pred)); + } + + template <typename Callback> + void _waitFor(stdx::unique_lock<stdx::mutex>& lk, + stdx::condition_variable& condvar, + Callback&& pred, + Milliseconds duration) { + uassert(ErrorCodes::ExceededTimeLimit, + "exceeded timeout", + condvar.wait_for(lk, duration.toSystemDuration(), pred)); + } + + mutable stdx::mutex _mutex; + stdx::condition_variable _condvarConsumer; + stdx::condition_variable _condvarProducer; + + // Max size of the queue + const size_t _max; + + // User's cost function + CostFunc _costFunc; + + // Current size of the queue + size_t _current = 0; + + std::queue<T> _queue; + + // Counter for consumers in the queue + size_t _consumers = 0; + + // Size of batch the blocking producer wants to insert + size_t _producerWants = 0; + + // Flags that we're shutting down the queue + bool _consumerEndClosed = false; + bool _producerEndClosed = false; +}; + +} // namespace mongo diff --git a/src/mongo/util/producer_consumer_queue_test.cpp b/src/mongo/util/producer_consumer_queue_test.cpp new file mode 100644 index 00000000000..f824b5a63de --- /dev/null +++ b/src/mongo/util/producer_consumer_queue_test.cpp @@ -0,0 +1,531 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/unittest/unittest.h" + +#include "mongo/util/producer_consumer_queue.h" + +#include "mongo/db/service_context_noop.h" +#include "mongo/stdx/condition_variable.h" +#include "mongo/stdx/memory.h" +#include "mongo/stdx/mutex.h" +#include "mongo/stdx/thread.h" +#include "mongo/util/assert_util.h" + +namespace mongo { + +namespace { + +class ProducerConsumerQueueTest : public unittest::Test { +public: + ProducerConsumerQueueTest() : _serviceCtx(stdx::make_unique<ServiceContextNoop>()) {} + + template <typename Callback> + stdx::thread runThread(StringData name, Callback&& cb) { + return stdx::thread([this, name, cb] { cb(); }); + } + +private: + std::unique_ptr<ServiceContext> _serviceCtx; +}; + +class MoveOnly { +public: + struct CostFunc { + CostFunc() = default; + explicit CostFunc(size_t val) : val(val) {} + + size_t operator()(const MoveOnly& mo) const { + return val + *mo._val; + } + + const size_t val = 0; + }; + + explicit MoveOnly(int i) : _val(i) {} + + MoveOnly(const MoveOnly&) = delete; + MoveOnly& operator=(const MoveOnly&) = delete; + + MoveOnly(MoveOnly&& other) : _val(other._val) { + other._val.reset(); + } + + MoveOnly& operator=(MoveOnly&& other) { + if (&other == this) { + return *this; + } + + _val = other._val; + other._val.reset(); + + return *this; + } + + bool movedFrom() const { + return !_val; + } + + friend bool operator==(const MoveOnly& lhs, const MoveOnly& rhs) { + return *lhs._val == *rhs._val; + } + + friend bool operator!=(const MoveOnly& lhs, const MoveOnly& rhs) { + return !(lhs == rhs); + } + + friend std::ostream& operator<<(std::ostream& os, const MoveOnly& mo) { + return (os << "MoveOnly(" << *mo._val << ")"); + } + +private: + boost::optional<int> _val; +}; + +TEST_F(ProducerConsumerQueueTest, basicPushPop) { + ProducerConsumerQueue<MoveOnly> pcq{}; + + runThread("Producer", [&]() { pcq.push(MoveOnly(1)); }).join(); + + ASSERT_EQUALS(pcq.sizeForTest(), 1ul); + + runThread("Consumer", [&]() { ASSERT_EQUALS(pcq.pop(), MoveOnly(1)); }).join(); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +TEST_F(ProducerConsumerQueueTest, closeConsumerEnd) { + ProducerConsumerQueue<MoveOnly> pcq{1}; + + pcq.push(MoveOnly(1)); + + auto producer = runThread("Producer", [&]() { + ASSERT_THROWS_CODE( + pcq.push(MoveOnly(2)), DBException, ErrorCodes::ProducerConsumerQueueEndClosed); + }); + + ASSERT_EQUALS(pcq.sizeForTest(), 1ul); + + pcq.closeConsumerEnd(); + + ASSERT_THROWS_CODE(pcq.pop(), DBException, ErrorCodes::ProducerConsumerQueueEndClosed); + + producer.join(); +} + +TEST_F(ProducerConsumerQueueTest, closeProducerEndImmediate) { + ProducerConsumerQueue<MoveOnly> pcq{}; + + pcq.push(MoveOnly(1)); + pcq.closeProducerEnd(); + + runThread("Consumer", [&]() { + ASSERT_EQUALS(pcq.pop(), MoveOnly(1)); + + ASSERT_THROWS_CODE(pcq.pop(), DBException, ErrorCodes::ProducerConsumerQueueEndClosed); + }).join(); +} + +TEST_F(ProducerConsumerQueueTest, closeProducerEndBlocking) { + ProducerConsumerQueue<MoveOnly> pcq{}; + + auto consumer = runThread("Consumer", [&]() { + ASSERT_THROWS_CODE(pcq.pop(), DBException, ErrorCodes::ProducerConsumerQueueEndClosed); + }); + + pcq.closeProducerEnd(); + + consumer.join(); +} + +TEST_F(ProducerConsumerQueueTest, popsWithTimeout) { + ProducerConsumerQueue<MoveOnly> pcq{}; + + runThread("Consumer", [&]() { + ASSERT_THROWS_CODE(pcq.pop(Milliseconds(100)), DBException, ErrorCodes::ExceededTimeLimit); + + std::vector<MoveOnly> vec; + ASSERT_THROWS_CODE(pcq.popMany(std::back_inserter(vec), Milliseconds(100)), + DBException, + ErrorCodes::ExceededTimeLimit); + + ASSERT_THROWS_CODE(pcq.popManyUpTo(1000, std::back_inserter(vec), Milliseconds(100)), + DBException, + ErrorCodes::ExceededTimeLimit); + }).join(); + + ASSERT_EQUALS(pcq.sizeForTest(), 0ul); +} + +TEST_F(ProducerConsumerQueueTest, pushesWithTimeout) { + ProducerConsumerQueue<MoveOnly> pcq{1}; + + { + MoveOnly mo(1); + pcq.push(std::move(mo)); + ASSERT(mo.movedFrom()); + } + + runThread("Consumer", [&]() { + { + MoveOnly mo(2); + ASSERT_THROWS_CODE(pcq.push(std::move(mo), Milliseconds(100)), + DBException, + ErrorCodes::ExceededTimeLimit); + ASSERT_EQUALS(pcq.sizeForTest(), 1ul); + ASSERT(!mo.movedFrom()); + ASSERT_EQUALS(mo, MoveOnly(2)); + } + + { + std::vector<MoveOnly> vec; + vec.emplace_back(MoveOnly(2)); + + auto iter = begin(vec); + ASSERT_THROWS_CODE(pcq.pushMany(iter, end(vec), Milliseconds(100)), + DBException, + ErrorCodes::ExceededTimeLimit); + ASSERT_EQUALS(pcq.sizeForTest(), 1ul); + ASSERT(!vec[0].movedFrom()); + ASSERT_EQUALS(vec[0], MoveOnly(2)); + } + }).join(); + + ASSERT_EQUALS(pcq.sizeForTest(), 1ul); +} + +TEST_F(ProducerConsumerQueueTest, basicPushPopWithBlocking) { + ProducerConsumerQueue<MoveOnly> pcq{}; + + auto consumer = runThread("Consumer", [&]() { ASSERT_EQUALS(pcq.pop(), MoveOnly(1)); }); + + auto producer = runThread("Producer", [&]() { pcq.push(MoveOnly(1)); }); + + consumer.join(); + producer.join(); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +TEST_F(ProducerConsumerQueueTest, multipleStepPushPopWithBlocking) { + ProducerConsumerQueue<MoveOnly> pcq{1}; + + auto consumer = runThread("Consumer", [&]() { + for (int i = 0; i < 10; ++i) { + ASSERT_EQUALS(pcq.pop(), MoveOnly(i)); + } + }); + + auto producer = runThread("Producer", [&]() { + for (int i = 0; i < 10; ++i) { + pcq.push(MoveOnly(i)); + } + }); + + consumer.join(); + producer.join(); + + ASSERT_TRUE(pcq.emptyForTest()); +} + + +TEST_F(ProducerConsumerQueueTest, pushTooLarge) { + { + ProducerConsumerQueue<MoveOnly, MoveOnly::CostFunc> pcq{1}; + + runThread("Producer", [&]() { + ASSERT_THROWS_CODE( + pcq.push(MoveOnly(2)), DBException, ErrorCodes::ProducerConsumerQueueBatchTooLarge); + }).join(); + } + + { + ProducerConsumerQueue<MoveOnly, MoveOnly::CostFunc> pcq{4}; + + std::vector<MoveOnly> vec; + vec.push_back(MoveOnly(3)); + vec.push_back(MoveOnly(3)); + + runThread("Producer", [&]() { + ASSERT_THROWS_CODE(pcq.pushMany(begin(vec), end(vec)), + DBException, + ErrorCodes::ProducerConsumerQueueBatchTooLarge); + }).join(); + } +} + +TEST_F(ProducerConsumerQueueTest, pushManyPopWithoutBlocking) { + ProducerConsumerQueue<MoveOnly> pcq{}; + + runThread("Producer", [&]() { + std::vector<MoveOnly> vec; + for (int i = 0; i < 10; ++i) { + vec.emplace_back(MoveOnly(i)); + } + + pcq.pushMany(begin(vec), end(vec)); + }).join(); + + runThread("Consumer", [&]() { + for (int i = 0; i < 10; ++i) { + ASSERT_EQUALS(pcq.pop(), MoveOnly(i)); + } + }).join(); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +TEST_F(ProducerConsumerQueueTest, popManyPopWithBlocking) { + ProducerConsumerQueue<MoveOnly> pcq{2}; + + auto consumer = runThread("Consumer", [&]() { + for (int i = 0; i < 10; i = i + 2) { + std::vector<MoveOnly> out; + + pcq.popMany(std::back_inserter(out)); + + ASSERT_EQUALS(out.size(), 2ul); + ASSERT_EQUALS(out[0], MoveOnly(i)); + ASSERT_EQUALS(out[1], MoveOnly(i + 1)); + } + }); + + auto producer = runThread("Producer", [&]() { + std::vector<MoveOnly> vec; + for (int i = 0; i < 10; ++i) { + vec.emplace_back(MoveOnly(i)); + } + + for (auto iter = begin(vec); iter != end(vec); iter += 2) { + pcq.pushMany(iter, iter + 2); + } + }); + + consumer.join(); + producer.join(); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +TEST_F(ProducerConsumerQueueTest, popManyUpToPopWithBlocking) { + ProducerConsumerQueue<MoveOnly> pcq{4}; + + auto consumer = runThread("Consumer", [&]() { + for (int i = 0; i < 10; i = i + 2) { + std::vector<MoveOnly> out; + + size_t spent; + std::tie(spent, std::ignore) = pcq.popManyUpTo(2, std::back_inserter(out)); + + ASSERT_EQUALS(spent, 2ul); + ASSERT_EQUALS(out.size(), 2ul); + ASSERT_EQUALS(out[0], MoveOnly(i)); + ASSERT_EQUALS(out[1], MoveOnly(i + 1)); + } + }); + + auto producer = runThread("Producer", [&]() { + std::vector<MoveOnly> vec; + for (int i = 0; i < 10; ++i) { + vec.emplace_back(MoveOnly(i)); + } + + for (auto iter = begin(vec); iter != end(vec); iter += 2) { + pcq.pushMany(iter, iter + 2); + } + }); + + consumer.join(); + producer.join(); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +TEST_F(ProducerConsumerQueueTest, popManyUpToPopWithBlockingWithSpecialCost) { + ProducerConsumerQueue<MoveOnly, MoveOnly::CostFunc> pcq{}; + + auto consumer = runThread("Consumer", [&]() { + { + std::vector<MoveOnly> out; + size_t spent; + std::tie(spent, std::ignore) = pcq.popManyUpTo(5, std::back_inserter(out)); + + ASSERT_EQUALS(spent, 6ul); + ASSERT_EQUALS(out.size(), 3ul); + ASSERT_EQUALS(out[0], MoveOnly(1)); + ASSERT_EQUALS(out[1], MoveOnly(2)); + ASSERT_EQUALS(out[2], MoveOnly(3)); + } + + { + std::vector<MoveOnly> out; + size_t spent; + std::tie(spent, std::ignore) = pcq.popManyUpTo(15, std::back_inserter(out)); + + ASSERT_EQUALS(spent, 9ul); + ASSERT_EQUALS(out.size(), 2ul); + ASSERT_EQUALS(out[0], MoveOnly(4)); + ASSERT_EQUALS(out[1], MoveOnly(5)); + } + }); + + auto producer = runThread("Producer", [&]() { + std::vector<MoveOnly> vec; + for (int i = 1; i < 6; ++i) { + vec.emplace_back(MoveOnly(i)); + } + + pcq.pushMany(begin(vec), end(vec)); + }); + + consumer.join(); + producer.join(); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +TEST_F(ProducerConsumerQueueTest, singleProducerMultiConsumer) { + ProducerConsumerQueue<MoveOnly> pcq{}; + + stdx::mutex mutex; + size_t success = 0; + size_t failure = 0; + + std::array<stdx::thread, 3> threads; + for (auto& thread : threads) { + thread = runThread("Consumer", [&]() { + { + try { + pcq.pop(); + stdx::lock_guard<stdx::mutex> lk(mutex); + success++; + } catch (const DBException& exception) { + ASSERT_EQUALS(exception.getCode(), ErrorCodes::ProducerConsumerQueueEndClosed); + stdx::lock_guard<stdx::mutex> lk(mutex); + failure++; + } + } + }); + } + + pcq.push(MoveOnly(1)); + pcq.push(MoveOnly(2)); + + pcq.closeProducerEnd(); + + for (auto& thread : threads) { + thread.join(); + } + + ASSERT_EQUALS(success, 2ul); + ASSERT_EQUALS(failure, 1ul); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +TEST_F(ProducerConsumerQueueTest, basicTryPop) { + ProducerConsumerQueue<MoveOnly> pcq{}; + + ASSERT_FALSE(pcq.tryPop()); + ASSERT_TRUE(pcq.tryPush(MoveOnly(1))); + ASSERT_EQUALS(pcq.sizeForTest(), 1ul); + + auto val = pcq.tryPop(); + + ASSERT_FALSE(pcq.tryPop()); + ASSERT_TRUE(val); + ASSERT_EQUALS(*val, MoveOnly(1)); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +TEST_F(ProducerConsumerQueueTest, basicTryPush) { + ProducerConsumerQueue<MoveOnly> pcq{1}; + + ASSERT_TRUE(pcq.tryPush(MoveOnly(1))); + ASSERT_FALSE(pcq.tryPush(MoveOnly(2))); + + ASSERT_EQUALS(pcq.sizeForTest(), 1ul); + + auto val = pcq.tryPop(); + ASSERT_FALSE(pcq.tryPop()); + ASSERT_TRUE(val); + ASSERT_EQUALS(*val, MoveOnly(1)); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +TEST_F(ProducerConsumerQueueTest, tryPushWithSpecialCost) { + ProducerConsumerQueue<MoveOnly, MoveOnly::CostFunc> pcq{5}; + + ASSERT_TRUE(pcq.tryPush(MoveOnly(1))); + ASSERT_TRUE(pcq.tryPush(MoveOnly(2))); + ASSERT_FALSE(pcq.tryPush(MoveOnly(3))); + + ASSERT_EQUALS(pcq.sizeForTest(), 3ul); + + auto val1 = pcq.tryPop(); + ASSERT_EQUALS(pcq.sizeForTest(), 2ul); + auto val2 = pcq.tryPop(); + ASSERT_EQUALS(pcq.sizeForTest(), 0ul); + ASSERT_FALSE(pcq.tryPop()); + ASSERT_TRUE(val1); + ASSERT_TRUE(val2); + ASSERT_EQUALS(*val1, MoveOnly(1)); + ASSERT_EQUALS(*val2, MoveOnly(2)); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +TEST_F(ProducerConsumerQueueTest, tryPushWithSpecialStatefulCost) { + ProducerConsumerQueue<MoveOnly, MoveOnly::CostFunc> pcq{5, MoveOnly::CostFunc(1)}; + + ASSERT_TRUE(pcq.tryPush(MoveOnly(1))); + ASSERT_TRUE(pcq.tryPush(MoveOnly(2))); + ASSERT_FALSE(pcq.tryPush(MoveOnly(3))); + + ASSERT_EQUALS(pcq.sizeForTest(), 5ul); + + auto val1 = pcq.tryPop(); + ASSERT_EQUALS(pcq.sizeForTest(), 3ul); + auto val2 = pcq.tryPop(); + ASSERT_EQUALS(pcq.sizeForTest(), 0ul); + ASSERT_FALSE(pcq.tryPop()); + ASSERT_TRUE(val1); + ASSERT_TRUE(val2); + ASSERT_EQUALS(*val1, MoveOnly(1)); + ASSERT_EQUALS(*val2, MoveOnly(2)); + + ASSERT_TRUE(pcq.emptyForTest()); +} + +} // namespace + +} // namespace mongo diff --git a/src/mongo/util/tcmalloc_server_status_section.cpp b/src/mongo/util/tcmalloc_server_status_section.cpp index 303076ba784..784f74e8cf8 100644 --- a/src/mongo/util/tcmalloc_server_status_section.cpp +++ b/src/mongo/util/tcmalloc_server_status_section.cpp @@ -38,6 +38,7 @@ #include "mongo/base/init.h" #include "mongo/db/commands/server_status.h" +#include "mongo/db/server_parameters.h" #include "mongo/db/service_context.h" #include "mongo/transport/transport_layer.h" #include "mongo/util/log.h" @@ -55,11 +56,17 @@ const int kManyClients = 40; stdx::mutex tcmallocCleanupLock; +MONGO_EXPORT_SERVER_PARAMETER(tcmallocEnableMarkThreadIdle, bool, true); + /** * Callback to allow TCMalloc to release freed memory to the central list at * favorable times. Ideally would do some milder cleanup or scavenge... */ void threadStateChange() { + if (!tcmallocEnableMarkThreadIdle.load()) { + return; + } + if (getGlobalServiceContext()->getTransportLayer()->sessionStats().numOpenSessions <= kManyClients) return; @@ -156,6 +163,25 @@ public: appendNumericPropertyIfAvailable( sub, "aggressive_memory_decommit", "tcmalloc.aggressive_memory_decommit"); + appendNumericPropertyIfAvailable( + sub, "pageheap_committed_bytes", "tcmalloc.pageheap_committed_bytes"); + appendNumericPropertyIfAvailable( + sub, "pageheap_scavenge_count", "tcmalloc.pageheap_scavenge_count"); + appendNumericPropertyIfAvailable( + sub, "pageheap_commit_count", "tcmalloc.pageheap_commit_count"); + appendNumericPropertyIfAvailable( + sub, "pageheap_total_commit_bytes", "tcmalloc.pageheap_total_commit_bytes"); + appendNumericPropertyIfAvailable( + sub, "pageheap_decommit_count", "tcmalloc.pageheap_decommit_count"); + appendNumericPropertyIfAvailable( + sub, "pageheap_total_decommit_bytes", "tcmalloc.pageheap_total_decommit_bytes"); + appendNumericPropertyIfAvailable( + sub, "pageheap_reserve_count", "tcmalloc.pageheap_reserve_count"); + appendNumericPropertyIfAvailable( + sub, "pageheap_total_reserve_bytes", "tcmalloc.pageheap_total_reserve_bytes"); + appendNumericPropertyIfAvailable( + sub, "spinlock_total_delay_ns", "tcmalloc.spinlock_total_delay_ns"); + #if MONGO_HAVE_GPERFTOOLS_SIZE_CLASS_STATS if (verbosity >= 2) { // Size class information diff --git a/src/third_party/SConscript b/src/third_party/SConscript index 3a820399677..add20816b83 100644 --- a/src/third_party/SConscript +++ b/src/third_party/SConscript @@ -6,7 +6,7 @@ Import("wiredtiger") boostSuffix = "-1.60.0" snappySuffix = '-1.1.3' zlibSuffix = '-1.2.8' -pcreSuffix = "-8.39" +pcreSuffix = "-8.41" mozjsSuffix = '-45' yamlSuffix = '-0.5.3' icuSuffix = '-57.1' diff --git a/src/third_party/gperftools-2.5/src/base/spinlock.cc b/src/third_party/gperftools-2.5/src/base/spinlock.cc index 85ff21ed404..acb01c8537a 100644 --- a/src/third_party/gperftools-2.5/src/base/spinlock.cc +++ b/src/third_party/gperftools-2.5/src/base/spinlock.cc @@ -49,6 +49,47 @@ const base::LinkerInitialized SpinLock::LINKER_INITIALIZED = base::LINKER_INITIALIZED; namespace { + +const int64_t kNanoPerSec = uint64_t(1000) * 1000 * 1000; + +#ifdef _WIN32 +int64_t frequency; + +void InitTimer() { + LARGE_INTEGER large_freq; + QueryPerformanceFrequency(&large_freq); + frequency = large_freq.QuadPart; +} + +int64_t NowMonotonic() { + LARGE_INTEGER time_value; + QueryPerformanceCounter(&time_value); + return time_value.QuadPart; +} + +int64_t TicksToNanos(int64_t timer_value) { + return timer_value * kNanoPerSec / frequency; +} + +#else // _WIN32 +void InitTimer() {} + +int64_t NowMonotonic() { + struct timespec t; + + if (clock_gettime(CLOCK_MONOTONIC, &t)) { + return 0; + } + + return (static_cast<double>(t.tv_sec) * kNanoPerSec) + t.tv_nsec; +} + +int64_t TicksToNanos(int64_t timer_value) { + return timer_value; +} + +#endif // _WIN32 + struct SpinLock_InitHelper { SpinLock_InitHelper() { // On multi-cpu machines, spin for longer before yielding @@ -56,6 +97,8 @@ struct SpinLock_InitHelper { if (GetSystemCPUsCount() > 1) { adaptive_spin_count = 1000; } + + InitTimer(); } }; @@ -69,6 +112,31 @@ inline void SpinlockPause(void) { __asm__ __volatile__("rep; nop" : : ); #endif } + +// The current version of atomic_ops.h lacks base::subtle::Barrier_AtomicIncrement +// so a CAS loop is used instead. +void NoBarrier_AtomicAdd(volatile base::subtle::Atomic64* ptr, + base::subtle::Atomic64 increment) { + base::subtle::Atomic64 base_value = base::subtle::NoBarrier_Load(ptr); + while (true) { + base::subtle::Atomic64 new_value; + base::subtle::NoBarrier_Store(&new_value, base::subtle::NoBarrier_Load(&base_value) + + base::subtle::NoBarrier_Load(&increment)); + + // Swap in the new incremented value. + base::subtle::Atomic64 cas_result = base::subtle::Acquire_CompareAndSwap( + ptr, base_value, new_value); + + // Check if the increment succeeded. + if (cas_result == base_value) { + return; + } + + // If the increment failed, just use the previous value as the value to + // add our increment to. + base_value = cas_result; + }; +} } // unnamed namespace @@ -84,10 +152,13 @@ Atomic32 SpinLock::SpinLoop() { kSpinLockSleeper); } +base::subtle::Atomic64 SpinLock::totalDelayNanos_ = 0; + void SpinLock::SlowLock() { Atomic32 lock_value = SpinLoop(); int lock_wait_call_count = 0; + int64_t start = 0; while (lock_value != kSpinLockFree) { // If the lock is currently held, but not marked as having a sleeper, mark // it as having a sleeper. @@ -115,12 +186,18 @@ void SpinLock::SlowLock() { } // Wait for an OS specific delay. + start = NowMonotonic(); base::internal::SpinLockDelay(&lockword_, lock_value, ++lock_wait_call_count); // Spin again after returning from the wait routine to give this thread // some chance of obtaining the lock. lock_value = SpinLoop(); } + + if (start) { + NoBarrier_AtomicAdd(&totalDelayNanos_, static_cast<base::subtle::Atomic64>( + TicksToNanos(NowMonotonic() - start))); + } } void SpinLock::SlowUnlock() { diff --git a/src/third_party/gperftools-2.5/src/base/spinlock.h b/src/third_party/gperftools-2.5/src/base/spinlock.h index 7243aeaaefd..c5ad8f048f6 100644 --- a/src/third_party/gperftools-2.5/src/base/spinlock.h +++ b/src/third_party/gperftools-2.5/src/base/spinlock.h @@ -107,6 +107,10 @@ class LOCKABLE SpinLock { return base::subtle::NoBarrier_Load(&lockword_) != kSpinLockFree; } + static base::subtle::Atomic64 GetTotalDelayNanos() { + return base::subtle::NoBarrier_Load(&totalDelayNanos_); + } + static const base::LinkerInitialized LINKER_INITIALIZED; // backwards compat private: enum { kSpinLockFree = 0 }; @@ -115,6 +119,8 @@ class LOCKABLE SpinLock { volatile Atomic32 lockword_; + static base::subtle::Atomic64 totalDelayNanos_; + void SlowLock(); void SlowUnlock(); Atomic32 SpinLoop(); diff --git a/src/third_party/gperftools-2.5/src/page_heap.cc b/src/third_party/gperftools-2.5/src/page_heap.cc index f52ae2af029..eaa33255f2c 100644 --- a/src/third_party/gperftools-2.5/src/page_heap.cc +++ b/src/third_party/gperftools-2.5/src/page_heap.cc @@ -245,16 +245,22 @@ Span* PageHeap::Split(Span* span, Length n) { } void PageHeap::CommitSpan(Span* span) { + ++stats_.commit_count; + TCMalloc_SystemCommit(reinterpret_cast<void*>(span->start << kPageShift), static_cast<size_t>(span->length << kPageShift)); stats_.committed_bytes += span->length << kPageShift; + stats_.total_commit_bytes += (span->length << kPageShift); } bool PageHeap::DecommitSpan(Span* span) { + ++stats_.decommit_count; + bool rv = TCMalloc_SystemRelease(reinterpret_cast<void*>(span->start << kPageShift), static_cast<size_t>(span->length << kPageShift)); if (rv) { stats_.committed_bytes -= span->length << kPageShift; + stats_.total_decommit_bytes += (span->length << kPageShift); } return rv; @@ -441,6 +447,8 @@ void PageHeap::IncrementalScavenge(Length n) { return; } + ++stats_.scavenge_count; + Length released_pages = ReleaseAtLeastNPages(1); if (released_pages == 0) { @@ -616,9 +624,16 @@ bool PageHeap::GrowHeap(Length n) { ask = actual_size >> kPageShift; RecordGrowth(ask << kPageShift); + ++stats_.reserve_count; + ++stats_.commit_count; + uint64_t old_system_bytes = stats_.system_bytes; stats_.system_bytes += (ask << kPageShift); stats_.committed_bytes += (ask << kPageShift); + + stats_.total_commit_bytes += (ask << kPageShift); + stats_.total_reserve_bytes += (ask << kPageShift); + const PageID p = reinterpret_cast<uintptr_t>(ptr) >> kPageShift; ASSERT(p > 0); diff --git a/src/third_party/gperftools-2.5/src/page_heap.h b/src/third_party/gperftools-2.5/src/page_heap.h index 18abed1974a..5c6607bbf16 100644 --- a/src/third_party/gperftools-2.5/src/page_heap.h +++ b/src/third_party/gperftools-2.5/src/page_heap.h @@ -143,12 +143,24 @@ class PERFTOOLS_DLL_DECL PageHeap { // Page heap statistics struct Stats { - Stats() : system_bytes(0), free_bytes(0), unmapped_bytes(0), committed_bytes(0) {} + Stats() : system_bytes(0), free_bytes(0), unmapped_bytes(0), committed_bytes(0), + scavenge_count(0), commit_count(0), decommit_count(0), + total_commit_bytes(0), total_decommit_bytes(0), + reserve_count(0), total_reserve_bytes(0) {} uint64_t system_bytes; // Total bytes allocated from system uint64_t free_bytes; // Total bytes on normal freelists uint64_t unmapped_bytes; // Total bytes on returned freelists uint64_t committed_bytes; // Bytes committed, always <= system_bytes_. + uint64_t scavenge_count; // Number of times scavagened flush pages + + uint64_t commit_count; // Number of virtual memory commits + uint64_t total_commit_bytes; // Bytes committed in lifetime of process + uint64_t decommit_count; // Number of virtual memory decommits + uint64_t total_decommit_bytes; // Bytes decommitted in lifetime of process + + uint64_t reserve_count; // Number of virtual memory reserves + uint64_t total_reserve_bytes; // Bytes reserved in lifetime of process }; inline Stats stats() const { return stats_; } diff --git a/src/third_party/gperftools-2.5/src/tcmalloc.cc b/src/third_party/gperftools-2.5/src/tcmalloc.cc index ba6c06e9d89..b573b4a9821 100644 --- a/src/third_party/gperftools-2.5/src/tcmalloc.cc +++ b/src/third_party/gperftools-2.5/src/tcmalloc.cc @@ -703,6 +703,54 @@ class TCMallocImplementation : public MallocExtension { return true; } + if (strcmp(name, "tcmalloc.pageheap_committed_bytes") == 0) { + SpinLockHolder l(Static::pageheap_lock()); + *value = Static::pageheap()->stats().committed_bytes; + return true; + } + + if (strcmp(name, "tcmalloc.pageheap_scavenge_count") == 0) { + SpinLockHolder l(Static::pageheap_lock()); + *value = Static::pageheap()->stats().scavenge_count; + return true; + } + + if (strcmp(name, "tcmalloc.pageheap_commit_count") == 0) { + SpinLockHolder l(Static::pageheap_lock()); + *value = Static::pageheap()->stats().commit_count; + return true; + } + + if (strcmp(name, "tcmalloc.pageheap_total_commit_bytes") == 0) { + SpinLockHolder l(Static::pageheap_lock()); + *value = Static::pageheap()->stats().total_commit_bytes; + return true; + } + + if (strcmp(name, "tcmalloc.pageheap_decommit_count") == 0) { + SpinLockHolder l(Static::pageheap_lock()); + *value = Static::pageheap()->stats().decommit_count; + return true; + } + + if (strcmp(name, "tcmalloc.pageheap_total_decommit_bytes") == 0) { + SpinLockHolder l(Static::pageheap_lock()); + *value = Static::pageheap()->stats().total_decommit_bytes; + return true; + } + + if (strcmp(name, "tcmalloc.pageheap_reserve_count") == 0) { + SpinLockHolder l(Static::pageheap_lock()); + *value = Static::pageheap()->stats().reserve_count; + return true; + } + + if (strcmp(name, "tcmalloc.pageheap_total_reserve_bytes") == 0) { + SpinLockHolder l(Static::pageheap_lock()); + *value = Static::pageheap()->stats().total_reserve_bytes; + return true; + } + if (strcmp(name, "tcmalloc.max_total_thread_cache_bytes") == 0) { SpinLockHolder l(Static::pageheap_lock()); *value = ThreadCache::overall_thread_cache_size(); @@ -721,6 +769,11 @@ class TCMallocImplementation : public MallocExtension { return true; } + if (strcmp(name, "tcmalloc.spinlock_total_delay_ns") == 0) { + *value = SpinLock::GetTotalDelayNanos(); + return true; + } + return false; } diff --git a/src/third_party/pcre-8.39/132html b/src/third_party/pcre-8.41/132html index e00024970a5..e00024970a5 100755 --- a/src/third_party/pcre-8.39/132html +++ b/src/third_party/pcre-8.41/132html diff --git a/src/third_party/pcre-8.39/AUTHORS b/src/third_party/pcre-8.41/AUTHORS index 342417a8a19..291657caef1 100644 --- a/src/third_party/pcre-8.39/AUTHORS +++ b/src/third_party/pcre-8.41/AUTHORS @@ -8,7 +8,7 @@ Email domain: cam.ac.uk University of Cambridge Computing Service, Cambridge, England. -Copyright (c) 1997-2016 University of Cambridge +Copyright (c) 1997-2017 University of Cambridge All rights reserved @@ -19,7 +19,7 @@ Written by: Zoltan Herczeg Email local part: hzmester Emain domain: freemail.hu -Copyright(c) 2010-2016 Zoltan Herczeg +Copyright(c) 2010-2017 Zoltan Herczeg All rights reserved. @@ -30,7 +30,7 @@ Written by: Zoltan Herczeg Email local part: hzmester Emain domain: freemail.hu -Copyright(c) 2009-2016 Zoltan Herczeg +Copyright(c) 2009-2017 Zoltan Herczeg All rights reserved. diff --git a/src/third_party/pcre-8.39/COPYING b/src/third_party/pcre-8.41/COPYING index 58eed01b61d..58eed01b61d 100644 --- a/src/third_party/pcre-8.39/COPYING +++ b/src/third_party/pcre-8.41/COPYING diff --git a/src/third_party/pcre-8.39/ChangeLog b/src/third_party/pcre-8.41/ChangeLog index a34f845f8a1..590a7542885 100644 --- a/src/third_party/pcre-8.39/ChangeLog +++ b/src/third_party/pcre-8.41/ChangeLog @@ -4,6 +4,100 @@ ChangeLog for PCRE Note that the PCRE 8.xx series (PCRE1) is now in a bugfix-only state. All development is happening in the PCRE2 10.xx series. +Version 8.41 05-July-2017 +------------------------- + +1. Fixed typo in CMakeLists.txt (wrong number of arguments for +PCRE_STATIC_RUNTIME (affects MSVC only). + +2. Issue 1 for 8.40 below was not correctly fixed. If pcregrep in multiline +mode with --only-matching matched several lines, it restarted scanning at the +next line instead of moving on to the end of the matched string, which can be +several lines after the start. + +3. Fix a missing else in the JIT compiler reported by 'idaifish'. + +4. A (?# style comment is now ignored between a basic quantifier and a +following '+' or '?' (example: /X+(?#comment)?Y/. + +5. Avoid use of a potentially overflowing buffer in pcregrep (patch by Petr +Pisar). + +6. Fuzzers have reported issues in pcretest. These are NOT serious (it is, +after all, just a test program). However, to stop the reports, some easy ones +are fixed: + + (a) Check for values < 256 when calling isprint() in pcretest. + (b) Give an error for too big a number after \O. + +7. In the 32-bit library in non-UTF mode, an attempt to find a Unicode +property for a character with a code point greater than 0x10ffff (the Unicode +maximum) caused a crash. + +8. The alternative matching function, pcre_dfa_exec() misbehaved if it +encountered a character class with a possessive repeat, for example [a-f]{3}+. + +9. When pcretest called pcre_copy_substring() in 32-bit mode, it set the buffer +length incorrectly, which could result in buffer overflow. + +10. Remove redundant line of code (accidentally left in ages ago). + +11. Applied C++ patch from Irfan Adilovic to guard 'using std::' directives +with namespace pcrecpp (Bugzilla #2084). + +12. Remove a duplication typo in pcre_tables.c. + +13. Fix returned offsets from regexec() when REG_STARTEND is used with a +starting offset greater than zero. + + +Version 8.40 11-January-2017 +---------------------------- + +1. Using -o with -M in pcregrep could cause unnecessary repeated output when + the match extended over a line boundary. + +2. Applied Chris Wilson's second patch (Bugzilla #1681) to CMakeLists.txt for + MSVC static compilation, putting the first patch under a new option. + +3. Fix register overwite in JIT when SSE2 acceleration is enabled. + +4. Ignore "show all captures" (/=) for DFA matching. + +5. Fix JIT unaligned accesses on x86. Patch by Marc Mutz. + +6. In any wide-character mode (8-bit UTF or any 16-bit or 32-bit mode), + without PCRE_UCP set, a negative character type such as \D in a positive + class should cause all characters greater than 255 to match, whatever else + is in the class. There was a bug that caused this not to happen if a + Unicode property item was added to such a class, for example [\D\P{Nd}] or + [\W\pL]. + +7. When pcretest was outputing information from a callout, the caret indicator + for the current position in the subject line was incorrect if it was after + an escape sequence for a character whose code point was greater than + \x{ff}. + +8. A pattern such as (?<RA>abc)(?(R)xyz) was incorrectly compiled such that + the conditional was interpreted as a reference to capturing group 1 instead + of a test for recursion. Any group whose name began with R was + misinterpreted in this way. (The reference interpretation should only + happen if the group's name is precisely "R".) + +9. A number of bugs have been mended relating to match start-up optimizations + when the first thing in a pattern is a positive lookahead. These all + applied only when PCRE_NO_START_OPTIMIZE was *not* set: + + (a) A pattern such as (?=.*X)X$ was incorrectly optimized as if it needed + both an initial 'X' and a following 'X'. + (b) Some patterns starting with an assertion that started with .* were + incorrectly optimized as having to match at the start of the subject or + after a newline. There are cases where this is not true, for example, + (?=.*[A-Z])(?=.{8,16})(?!.*[\s]) matches after the start in lines that + start with spaces. Starting .* in an assertion is no longer taken as an + indication of matching at the start (or after a newline). + + Version 8.39 14-June-2016 ------------------------- diff --git a/src/third_party/pcre-8.39/CheckMan b/src/third_party/pcre-8.41/CheckMan index 480d735481e..480d735481e 100755 --- a/src/third_party/pcre-8.39/CheckMan +++ b/src/third_party/pcre-8.41/CheckMan diff --git a/src/third_party/pcre-8.39/CleanTxt b/src/third_party/pcre-8.41/CleanTxt index 1f42519c8d7..1f42519c8d7 100755 --- a/src/third_party/pcre-8.39/CleanTxt +++ b/src/third_party/pcre-8.41/CleanTxt diff --git a/src/third_party/pcre-8.39/Detrail b/src/third_party/pcre-8.41/Detrail index 1c5c7e9cae1..1c5c7e9cae1 100755 --- a/src/third_party/pcre-8.39/Detrail +++ b/src/third_party/pcre-8.41/Detrail diff --git a/src/third_party/pcre-8.39/HACKING b/src/third_party/pcre-8.41/HACKING index 691b7a14e50..691b7a14e50 100644 --- a/src/third_party/pcre-8.39/HACKING +++ b/src/third_party/pcre-8.41/HACKING diff --git a/src/third_party/pcre-8.39/INSTALL b/src/third_party/pcre-8.41/INSTALL index 2099840756e..2099840756e 100644 --- a/src/third_party/pcre-8.39/INSTALL +++ b/src/third_party/pcre-8.41/INSTALL diff --git a/src/third_party/pcre-8.39/LICENCE b/src/third_party/pcre-8.41/LICENCE index dd977af971b..dd9071a8dd8 100644 --- a/src/third_party/pcre-8.39/LICENCE +++ b/src/third_party/pcre-8.41/LICENCE @@ -25,7 +25,7 @@ Email domain: cam.ac.uk University of Cambridge Computing Service, Cambridge, England. -Copyright (c) 1997-2016 University of Cambridge +Copyright (c) 1997-2017 University of Cambridge All rights reserved. @@ -36,7 +36,7 @@ Written by: Zoltan Herczeg Email local part: hzmester Emain domain: freemail.hu -Copyright(c) 2010-2016 Zoltan Herczeg +Copyright(c) 2010-2017 Zoltan Herczeg All rights reserved. @@ -47,7 +47,7 @@ Written by: Zoltan Herczeg Email local part: hzmester Emain domain: freemail.hu -Copyright(c) 2009-2016 Zoltan Herczeg +Copyright(c) 2009-2017 Zoltan Herczeg All rights reserved. diff --git a/src/third_party/pcre-8.39/NEWS b/src/third_party/pcre-8.41/NEWS index 0ca1bab2a4c..36be07cb880 100644 --- a/src/third_party/pcre-8.39/NEWS +++ b/src/third_party/pcre-8.41/NEWS @@ -1,6 +1,18 @@ News about PCRE releases ------------------------ +Release 8.41 13-June-2017 +------------------------- + +This is a bug-fix release. + + +Release 8.40 11-January-2017 +---------------------------- + +This is a bug-fix release. + + Release 8.39 14-June-2016 ------------------------- diff --git a/src/third_party/pcre-8.39/NON-AUTOTOOLS-BUILD b/src/third_party/pcre-8.41/NON-AUTOTOOLS-BUILD index 3910059106b..3910059106b 100644 --- a/src/third_party/pcre-8.39/NON-AUTOTOOLS-BUILD +++ b/src/third_party/pcre-8.41/NON-AUTOTOOLS-BUILD diff --git a/src/third_party/pcre-8.39/NON-UNIX-USE b/src/third_party/pcre-8.41/NON-UNIX-USE index a25546b6ff5..a25546b6ff5 100644 --- a/src/third_party/pcre-8.39/NON-UNIX-USE +++ b/src/third_party/pcre-8.41/NON-UNIX-USE diff --git a/src/third_party/pcre-8.39/PrepareRelease b/src/third_party/pcre-8.41/PrepareRelease index 9891e08d67f..9891e08d67f 100755 --- a/src/third_party/pcre-8.39/PrepareRelease +++ b/src/third_party/pcre-8.41/PrepareRelease diff --git a/src/third_party/pcre-8.39/README b/src/third_party/pcre-8.41/README index 4887ebf350e..4887ebf350e 100644 --- a/src/third_party/pcre-8.39/README +++ b/src/third_party/pcre-8.41/README diff --git a/src/third_party/pcre-8.39/RunGrepTest b/src/third_party/pcre-8.41/RunGrepTest index 721ec5184ab..721ec5184ab 100755 --- a/src/third_party/pcre-8.39/RunGrepTest +++ b/src/third_party/pcre-8.41/RunGrepTest diff --git a/src/third_party/pcre-8.39/RunTest b/src/third_party/pcre-8.41/RunTest index 357a739b75f..357a739b75f 100755 --- a/src/third_party/pcre-8.39/RunTest +++ b/src/third_party/pcre-8.41/RunTest diff --git a/src/third_party/pcre-8.39/RunTest.bat b/src/third_party/pcre-8.41/RunTest.bat index 35d7f71f9e8..35d7f71f9e8 100644 --- a/src/third_party/pcre-8.39/RunTest.bat +++ b/src/third_party/pcre-8.41/RunTest.bat diff --git a/src/third_party/pcre-8.39/SConscript b/src/third_party/pcre-8.41/SConscript index e7b9a59a550..e7b9a59a550 100644 --- a/src/third_party/pcre-8.39/SConscript +++ b/src/third_party/pcre-8.41/SConscript diff --git a/src/third_party/pcre-8.39/ar-lib b/src/third_party/pcre-8.41/ar-lib index 463b9ec0206..463b9ec0206 100755 --- a/src/third_party/pcre-8.39/ar-lib +++ b/src/third_party/pcre-8.41/ar-lib diff --git a/src/third_party/pcre-8.39/build_posix/config.h b/src/third_party/pcre-8.41/build_posix/config.h index 9cfa5a4d70f..8af8c7add1e 100644 --- a/src/third_party/pcre-8.39/build_posix/config.h +++ b/src/third_party/pcre-8.41/build_posix/config.h @@ -221,7 +221,7 @@ sure both macros are undefined; an emulation function will then be used. */ #define PACKAGE_NAME "PCRE" /* Define to the full name and version of this package. */ -#define PACKAGE_STRING "PCRE 8.39" +#define PACKAGE_STRING "PCRE 8.41" /* Define to the one symbol short name of this package. */ #define PACKAGE_TARNAME "pcre" @@ -230,7 +230,7 @@ sure both macros are undefined; an emulation function will then be used. */ #define PACKAGE_URL "" /* Define to the version of this package. */ -#define PACKAGE_VERSION "8.39" +#define PACKAGE_VERSION "8.41" /* The value of PARENS_NEST_LIMIT specifies the maximum depth of nested parentheses (of any kind) in a pattern. This limits the amount of system @@ -335,7 +335,7 @@ sure both macros are undefined; an emulation function will then be used. */ /* #undef SUPPORT_VALGRIND */ /* Version number of package */ -#define VERSION "8.39" +#define VERSION "8.41" /* Define to empty if `const' does not conform to ANSI C. */ /* #undef const */ diff --git a/src/third_party/pcre-8.39/build_solaris/config.h b/src/third_party/pcre-8.41/build_solaris/config.h index a391fd1cee5..164bd8a2e39 100644 --- a/src/third_party/pcre-8.39/build_solaris/config.h +++ b/src/third_party/pcre-8.41/build_solaris/config.h @@ -221,7 +221,7 @@ sure both macros are undefined; an emulation function will then be used. */ #define PACKAGE_NAME "PCRE" /* Define to the full name and version of this package. */ -#define PACKAGE_STRING "PCRE 8.39" +#define PACKAGE_STRING "PCRE 8.41" /* Define to the one symbol short name of this package. */ #define PACKAGE_TARNAME "pcre" @@ -230,7 +230,7 @@ sure both macros are undefined; an emulation function will then be used. */ #define PACKAGE_URL "" /* Define to the version of this package. */ -#define PACKAGE_VERSION "8.39" +#define PACKAGE_VERSION "8.41" /* The value of PARENS_NEST_LIMIT specifies the maximum depth of nested parentheses (of any kind) in a pattern. This limits the amount of system @@ -335,7 +335,7 @@ sure both macros are undefined; an emulation function will then be used. */ /* #undef SUPPORT_VALGRIND */ /* Version number of package */ -#define VERSION "8.39" +#define VERSION "8.41" /* Define to empty if `const' does not conform to ANSI C. */ /* #undef const */ diff --git a/src/third_party/pcre-8.39/build_windows/config.h b/src/third_party/pcre-8.41/build_windows/config.h index 2e8d4ec7b6a..2e8d4ec7b6a 100755 --- a/src/third_party/pcre-8.39/build_windows/config.h +++ b/src/third_party/pcre-8.41/build_windows/config.h diff --git a/src/third_party/pcre-8.39/dftables.c b/src/third_party/pcre-8.41/dftables.c index 1fdc8e0f231..1fdc8e0f231 100644 --- a/src/third_party/pcre-8.39/dftables.c +++ b/src/third_party/pcre-8.41/dftables.c diff --git a/src/third_party/pcre-8.39/libpcre.pc.in b/src/third_party/pcre-8.41/libpcre.pc.in index 0a35da87f85..0a35da87f85 100644 --- a/src/third_party/pcre-8.39/libpcre.pc.in +++ b/src/third_party/pcre-8.41/libpcre.pc.in diff --git a/src/third_party/pcre-8.39/libpcre16.pc.in b/src/third_party/pcre-8.41/libpcre16.pc.in index 080c9dcfe8a..080c9dcfe8a 100644 --- a/src/third_party/pcre-8.39/libpcre16.pc.in +++ b/src/third_party/pcre-8.41/libpcre16.pc.in diff --git a/src/third_party/pcre-8.39/libpcre32.pc.in b/src/third_party/pcre-8.41/libpcre32.pc.in index a3ae0e11fcf..a3ae0e11fcf 100644 --- a/src/third_party/pcre-8.39/libpcre32.pc.in +++ b/src/third_party/pcre-8.41/libpcre32.pc.in diff --git a/src/third_party/pcre-8.39/libpcrecpp.pc.in b/src/third_party/pcre-8.41/libpcrecpp.pc.in index ef006fe47aa..ef006fe47aa 100644 --- a/src/third_party/pcre-8.39/libpcrecpp.pc.in +++ b/src/third_party/pcre-8.41/libpcrecpp.pc.in diff --git a/src/third_party/pcre-8.39/libpcreposix.pc.in b/src/third_party/pcre-8.41/libpcreposix.pc.in index c6c0b0c6c66..c6c0b0c6c66 100644 --- a/src/third_party/pcre-8.39/libpcreposix.pc.in +++ b/src/third_party/pcre-8.41/libpcreposix.pc.in diff --git a/src/third_party/pcre-8.39/makevp.bat b/src/third_party/pcre-8.41/makevp.bat index 5f795487eb9..5f795487eb9 100644 --- a/src/third_party/pcre-8.39/makevp.bat +++ b/src/third_party/pcre-8.41/makevp.bat diff --git a/src/third_party/pcre-8.39/makevp_c.txt b/src/third_party/pcre-8.41/makevp_c.txt index 5648115405d..5648115405d 100644 --- a/src/third_party/pcre-8.39/makevp_c.txt +++ b/src/third_party/pcre-8.41/makevp_c.txt diff --git a/src/third_party/pcre-8.39/makevp_l.txt b/src/third_party/pcre-8.41/makevp_l.txt index 9b071e0f90a..9b071e0f90a 100644 --- a/src/third_party/pcre-8.39/makevp_l.txt +++ b/src/third_party/pcre-8.41/makevp_l.txt diff --git a/src/third_party/pcre-8.39/pcre-config.in b/src/third_party/pcre-8.41/pcre-config.in index ac06a3325bc..ac06a3325bc 100644 --- a/src/third_party/pcre-8.39/pcre-config.in +++ b/src/third_party/pcre-8.41/pcre-config.in diff --git a/src/third_party/pcre-8.39/pcre.h b/src/third_party/pcre-8.41/pcre.h index 70559700654..442c6bdb5fe 100644 --- a/src/third_party/pcre-8.39/pcre.h +++ b/src/third_party/pcre-8.41/pcre.h @@ -42,9 +42,9 @@ POSSIBILITY OF SUCH DAMAGE. /* The current PCRE version information. */ #define PCRE_MAJOR 8 -#define PCRE_MINOR 39 +#define PCRE_MINOR 41 #define PCRE_PRERELEASE -#define PCRE_DATE 2016-06-14 +#define PCRE_DATE 2017-07-05 /* When an application links to a PCRE DLL in Windows, the symbols that are imported have to be identified as such. When building PCRE, the appropriate diff --git a/src/third_party/pcre-8.39/pcre.h.generic b/src/third_party/pcre-8.41/pcre.h.generic index 70559700654..442c6bdb5fe 100644 --- a/src/third_party/pcre-8.39/pcre.h.generic +++ b/src/third_party/pcre-8.41/pcre.h.generic @@ -42,9 +42,9 @@ POSSIBILITY OF SUCH DAMAGE. /* The current PCRE version information. */ #define PCRE_MAJOR 8 -#define PCRE_MINOR 39 +#define PCRE_MINOR 41 #define PCRE_PRERELEASE -#define PCRE_DATE 2016-06-14 +#define PCRE_DATE 2017-07-05 /* When an application links to a PCRE DLL in Windows, the symbols that are imported have to be identified as such. When building PCRE, the appropriate diff --git a/src/third_party/pcre-8.39/pcre.h.in b/src/third_party/pcre-8.41/pcre.h.in index 667a45ed575..667a45ed575 100644 --- a/src/third_party/pcre-8.39/pcre.h.in +++ b/src/third_party/pcre-8.41/pcre.h.in diff --git a/src/third_party/pcre-8.39/pcre16_byte_order.c b/src/third_party/pcre-8.41/pcre16_byte_order.c index 11d2973a3db..11d2973a3db 100644 --- a/src/third_party/pcre-8.39/pcre16_byte_order.c +++ b/src/third_party/pcre-8.41/pcre16_byte_order.c diff --git a/src/third_party/pcre-8.39/pcre16_chartables.c b/src/third_party/pcre-8.41/pcre16_chartables.c index 7c0ff35f5e0..7c0ff35f5e0 100644 --- a/src/third_party/pcre-8.39/pcre16_chartables.c +++ b/src/third_party/pcre-8.41/pcre16_chartables.c diff --git a/src/third_party/pcre-8.39/pcre16_compile.c b/src/third_party/pcre-8.41/pcre16_compile.c index e499b670877..e499b670877 100644 --- a/src/third_party/pcre-8.39/pcre16_compile.c +++ b/src/third_party/pcre-8.41/pcre16_compile.c diff --git a/src/third_party/pcre-8.39/pcre16_config.c b/src/third_party/pcre-8.41/pcre16_config.c index b52138764f6..b52138764f6 100644 --- a/src/third_party/pcre-8.39/pcre16_config.c +++ b/src/third_party/pcre-8.41/pcre16_config.c diff --git a/src/third_party/pcre-8.39/pcre16_dfa_exec.c b/src/third_party/pcre-8.41/pcre16_dfa_exec.c index 2ba740e972b..2ba740e972b 100644 --- a/src/third_party/pcre-8.39/pcre16_dfa_exec.c +++ b/src/third_party/pcre-8.41/pcre16_dfa_exec.c diff --git a/src/third_party/pcre-8.39/pcre16_exec.c b/src/third_party/pcre-8.41/pcre16_exec.c index 7417b1770c6..7417b1770c6 100644 --- a/src/third_party/pcre-8.39/pcre16_exec.c +++ b/src/third_party/pcre-8.41/pcre16_exec.c diff --git a/src/third_party/pcre-8.39/pcre16_fullinfo.c b/src/third_party/pcre-8.41/pcre16_fullinfo.c index 544dca6ed5c..544dca6ed5c 100644 --- a/src/third_party/pcre-8.39/pcre16_fullinfo.c +++ b/src/third_party/pcre-8.41/pcre16_fullinfo.c diff --git a/src/third_party/pcre-8.39/pcre16_get.c b/src/third_party/pcre-8.41/pcre16_get.c index 3ded08c622c..3ded08c622c 100644 --- a/src/third_party/pcre-8.39/pcre16_get.c +++ b/src/third_party/pcre-8.41/pcre16_get.c diff --git a/src/third_party/pcre-8.39/pcre16_globals.c b/src/third_party/pcre-8.41/pcre16_globals.c index a136b3d8c22..a136b3d8c22 100644 --- a/src/third_party/pcre-8.39/pcre16_globals.c +++ b/src/third_party/pcre-8.41/pcre16_globals.c diff --git a/src/third_party/pcre-8.39/pcre16_jit_compile.c b/src/third_party/pcre-8.41/pcre16_jit_compile.c index ab0cacd7646..ab0cacd7646 100644 --- a/src/third_party/pcre-8.39/pcre16_jit_compile.c +++ b/src/third_party/pcre-8.41/pcre16_jit_compile.c diff --git a/src/third_party/pcre-8.39/pcre16_maketables.c b/src/third_party/pcre-8.41/pcre16_maketables.c index b1cd1c579d6..b1cd1c579d6 100644 --- a/src/third_party/pcre-8.39/pcre16_maketables.c +++ b/src/third_party/pcre-8.41/pcre16_maketables.c diff --git a/src/third_party/pcre-8.39/pcre16_newline.c b/src/third_party/pcre-8.41/pcre16_newline.c index 7fe201400f5..7fe201400f5 100644 --- a/src/third_party/pcre-8.39/pcre16_newline.c +++ b/src/third_party/pcre-8.41/pcre16_newline.c diff --git a/src/third_party/pcre-8.39/pcre16_ord2utf16.c b/src/third_party/pcre-8.41/pcre16_ord2utf16.c index 8e2ce5ea6c5..8e2ce5ea6c5 100644 --- a/src/third_party/pcre-8.39/pcre16_ord2utf16.c +++ b/src/third_party/pcre-8.41/pcre16_ord2utf16.c diff --git a/src/third_party/pcre-8.39/pcre16_printint.c b/src/third_party/pcre-8.41/pcre16_printint.c index 33d8c340200..33d8c340200 100644 --- a/src/third_party/pcre-8.39/pcre16_printint.c +++ b/src/third_party/pcre-8.41/pcre16_printint.c diff --git a/src/third_party/pcre-8.39/pcre16_refcount.c b/src/third_party/pcre-8.41/pcre16_refcount.c index d3d15439737..d3d15439737 100644 --- a/src/third_party/pcre-8.39/pcre16_refcount.c +++ b/src/third_party/pcre-8.41/pcre16_refcount.c diff --git a/src/third_party/pcre-8.39/pcre16_string_utils.c b/src/third_party/pcre-8.41/pcre16_string_utils.c index 382c40799fb..382c40799fb 100644 --- a/src/third_party/pcre-8.39/pcre16_string_utils.c +++ b/src/third_party/pcre-8.41/pcre16_string_utils.c diff --git a/src/third_party/pcre-8.39/pcre16_study.c b/src/third_party/pcre-8.41/pcre16_study.c index f87de081fc4..f87de081fc4 100644 --- a/src/third_party/pcre-8.39/pcre16_study.c +++ b/src/third_party/pcre-8.41/pcre16_study.c diff --git a/src/third_party/pcre-8.39/pcre16_tables.c b/src/third_party/pcre-8.41/pcre16_tables.c index d84297093a4..d84297093a4 100644 --- a/src/third_party/pcre-8.39/pcre16_tables.c +++ b/src/third_party/pcre-8.41/pcre16_tables.c diff --git a/src/third_party/pcre-8.39/pcre16_ucd.c b/src/third_party/pcre-8.41/pcre16_ucd.c index ee23439a013..ee23439a013 100644 --- a/src/third_party/pcre-8.39/pcre16_ucd.c +++ b/src/third_party/pcre-8.41/pcre16_ucd.c diff --git a/src/third_party/pcre-8.39/pcre16_utf16_utils.c b/src/third_party/pcre-8.41/pcre16_utf16_utils.c index 49ced0c0b1c..49ced0c0b1c 100644 --- a/src/third_party/pcre-8.39/pcre16_utf16_utils.c +++ b/src/third_party/pcre-8.41/pcre16_utf16_utils.c diff --git a/src/third_party/pcre-8.39/pcre16_valid_utf16.c b/src/third_party/pcre-8.41/pcre16_valid_utf16.c index 09076539d09..09076539d09 100644 --- a/src/third_party/pcre-8.39/pcre16_valid_utf16.c +++ b/src/third_party/pcre-8.41/pcre16_valid_utf16.c diff --git a/src/third_party/pcre-8.39/pcre16_version.c b/src/third_party/pcre-8.41/pcre16_version.c index e991b1a8cfd..e991b1a8cfd 100644 --- a/src/third_party/pcre-8.39/pcre16_version.c +++ b/src/third_party/pcre-8.41/pcre16_version.c diff --git a/src/third_party/pcre-8.39/pcre16_xclass.c b/src/third_party/pcre-8.41/pcre16_xclass.c index 5aac2a36c68..5aac2a36c68 100644 --- a/src/third_party/pcre-8.39/pcre16_xclass.c +++ b/src/third_party/pcre-8.41/pcre16_xclass.c diff --git a/src/third_party/pcre-8.39/pcre32_byte_order.c b/src/third_party/pcre-8.41/pcre32_byte_order.c index 9cf5362730a..9cf5362730a 100644 --- a/src/third_party/pcre-8.39/pcre32_byte_order.c +++ b/src/third_party/pcre-8.41/pcre32_byte_order.c diff --git a/src/third_party/pcre-8.39/pcre32_chartables.c b/src/third_party/pcre-8.41/pcre32_chartables.c index b5d8c23dbf1..b5d8c23dbf1 100644 --- a/src/third_party/pcre-8.39/pcre32_chartables.c +++ b/src/third_party/pcre-8.41/pcre32_chartables.c diff --git a/src/third_party/pcre-8.39/pcre32_compile.c b/src/third_party/pcre-8.41/pcre32_compile.c index d781eb377e0..d781eb377e0 100644 --- a/src/third_party/pcre-8.39/pcre32_compile.c +++ b/src/third_party/pcre-8.41/pcre32_compile.c diff --git a/src/third_party/pcre-8.39/pcre32_config.c b/src/third_party/pcre-8.41/pcre32_config.c index d63f3e9ea23..d63f3e9ea23 100644 --- a/src/third_party/pcre-8.39/pcre32_config.c +++ b/src/third_party/pcre-8.41/pcre32_config.c diff --git a/src/third_party/pcre-8.39/pcre32_dfa_exec.c b/src/third_party/pcre-8.41/pcre32_dfa_exec.c index b0bfd34f04d..b0bfd34f04d 100644 --- a/src/third_party/pcre-8.39/pcre32_dfa_exec.c +++ b/src/third_party/pcre-8.41/pcre32_dfa_exec.c diff --git a/src/third_party/pcre-8.39/pcre32_exec.c b/src/third_party/pcre-8.41/pcre32_exec.c index 8170ed77d35..8170ed77d35 100644 --- a/src/third_party/pcre-8.39/pcre32_exec.c +++ b/src/third_party/pcre-8.41/pcre32_exec.c diff --git a/src/third_party/pcre-8.39/pcre32_fullinfo.c b/src/third_party/pcre-8.41/pcre32_fullinfo.c index 6ecc5209a08..6ecc5209a08 100644 --- a/src/third_party/pcre-8.39/pcre32_fullinfo.c +++ b/src/third_party/pcre-8.41/pcre32_fullinfo.c diff --git a/src/third_party/pcre-8.39/pcre32_get.c b/src/third_party/pcre-8.41/pcre32_get.c index d35deee0cd4..d35deee0cd4 100644 --- a/src/third_party/pcre-8.39/pcre32_get.c +++ b/src/third_party/pcre-8.41/pcre32_get.c diff --git a/src/third_party/pcre-8.39/pcre32_globals.c b/src/third_party/pcre-8.41/pcre32_globals.c index 32e0914ca6d..32e0914ca6d 100644 --- a/src/third_party/pcre-8.39/pcre32_globals.c +++ b/src/third_party/pcre-8.41/pcre32_globals.c diff --git a/src/third_party/pcre-8.39/pcre32_jit_compile.c b/src/third_party/pcre-8.41/pcre32_jit_compile.c index 2e7c6f97c96..2e7c6f97c96 100644 --- a/src/third_party/pcre-8.39/pcre32_jit_compile.c +++ b/src/third_party/pcre-8.41/pcre32_jit_compile.c diff --git a/src/third_party/pcre-8.39/pcre32_maketables.c b/src/third_party/pcre-8.41/pcre32_maketables.c index 5d1b1c64c96..5d1b1c64c96 100644 --- a/src/third_party/pcre-8.39/pcre32_maketables.c +++ b/src/third_party/pcre-8.41/pcre32_maketables.c diff --git a/src/third_party/pcre-8.39/pcre32_newline.c b/src/third_party/pcre-8.41/pcre32_newline.c index 7f8d5360cdc..7f8d5360cdc 100644 --- a/src/third_party/pcre-8.39/pcre32_newline.c +++ b/src/third_party/pcre-8.41/pcre32_newline.c diff --git a/src/third_party/pcre-8.39/pcre32_ord2utf32.c b/src/third_party/pcre-8.41/pcre32_ord2utf32.c index 606bcb3d7ba..606bcb3d7ba 100644 --- a/src/third_party/pcre-8.39/pcre32_ord2utf32.c +++ b/src/third_party/pcre-8.41/pcre32_ord2utf32.c diff --git a/src/third_party/pcre-8.39/pcre32_printint.c b/src/third_party/pcre-8.41/pcre32_printint.c index f3fd7b25e2c..f3fd7b25e2c 100644 --- a/src/third_party/pcre-8.39/pcre32_printint.c +++ b/src/third_party/pcre-8.41/pcre32_printint.c diff --git a/src/third_party/pcre-8.39/pcre32_refcount.c b/src/third_party/pcre-8.41/pcre32_refcount.c index dbdf432d82a..dbdf432d82a 100644 --- a/src/third_party/pcre-8.39/pcre32_refcount.c +++ b/src/third_party/pcre-8.41/pcre32_refcount.c diff --git a/src/third_party/pcre-8.39/pcre32_string_utils.c b/src/third_party/pcre-8.41/pcre32_string_utils.c index e37b3d4805f..e37b3d4805f 100644 --- a/src/third_party/pcre-8.39/pcre32_string_utils.c +++ b/src/third_party/pcre-8.41/pcre32_string_utils.c diff --git a/src/third_party/pcre-8.39/pcre32_study.c b/src/third_party/pcre-8.41/pcre32_study.c index d3a3afed791..d3a3afed791 100644 --- a/src/third_party/pcre-8.39/pcre32_study.c +++ b/src/third_party/pcre-8.41/pcre32_study.c diff --git a/src/third_party/pcre-8.39/pcre32_tables.c b/src/third_party/pcre-8.41/pcre32_tables.c index 3d94cca33a1..3d94cca33a1 100644 --- a/src/third_party/pcre-8.39/pcre32_tables.c +++ b/src/third_party/pcre-8.41/pcre32_tables.c diff --git a/src/third_party/pcre-8.39/pcre32_ucd.c b/src/third_party/pcre-8.41/pcre32_ucd.c index befe22d3435..befe22d3435 100644 --- a/src/third_party/pcre-8.39/pcre32_ucd.c +++ b/src/third_party/pcre-8.41/pcre32_ucd.c diff --git a/src/third_party/pcre-8.39/pcre32_utf32_utils.c b/src/third_party/pcre-8.41/pcre32_utf32_utils.c index f844e237165..f844e237165 100644 --- a/src/third_party/pcre-8.39/pcre32_utf32_utils.c +++ b/src/third_party/pcre-8.41/pcre32_utf32_utils.c diff --git a/src/third_party/pcre-8.39/pcre32_valid_utf32.c b/src/third_party/pcre-8.41/pcre32_valid_utf32.c index 94cda1a2c4c..94cda1a2c4c 100644 --- a/src/third_party/pcre-8.39/pcre32_valid_utf32.c +++ b/src/third_party/pcre-8.41/pcre32_valid_utf32.c diff --git a/src/third_party/pcre-8.39/pcre32_version.c b/src/third_party/pcre-8.41/pcre32_version.c index fdaad9b0859..fdaad9b0859 100644 --- a/src/third_party/pcre-8.39/pcre32_version.c +++ b/src/third_party/pcre-8.41/pcre32_version.c diff --git a/src/third_party/pcre-8.39/pcre32_xclass.c b/src/third_party/pcre-8.41/pcre32_xclass.c index 5662408ad5f..5662408ad5f 100644 --- a/src/third_party/pcre-8.39/pcre32_xclass.c +++ b/src/third_party/pcre-8.41/pcre32_xclass.c diff --git a/src/third_party/pcre-8.39/pcre_byte_order.c b/src/third_party/pcre-8.41/pcre_byte_order.c index cf5f12b04ea..cf5f12b04ea 100644 --- a/src/third_party/pcre-8.39/pcre_byte_order.c +++ b/src/third_party/pcre-8.41/pcre_byte_order.c diff --git a/src/third_party/pcre-8.39/pcre_chartables.c b/src/third_party/pcre-8.41/pcre_chartables.c index 1e20ec29d05..1e20ec29d05 100644 --- a/src/third_party/pcre-8.39/pcre_chartables.c +++ b/src/third_party/pcre-8.41/pcre_chartables.c diff --git a/src/third_party/pcre-8.39/pcre_chartables.c.dist b/src/third_party/pcre-8.41/pcre_chartables.c.dist index 1e20ec29d05..1e20ec29d05 100644 --- a/src/third_party/pcre-8.39/pcre_chartables.c.dist +++ b/src/third_party/pcre-8.41/pcre_chartables.c.dist diff --git a/src/third_party/pcre-8.39/pcre_compile.c b/src/third_party/pcre-8.41/pcre_compile.c index 7cd39501230..42f204cdfff 100644 --- a/src/third_party/pcre-8.39/pcre_compile.c +++ b/src/third_party/pcre-8.41/pcre_compile.c @@ -5579,6 +5579,34 @@ for (;; ptr++) #endif #if defined SUPPORT_UTF || !defined COMPILE_PCRE8 { + /* For non-UCP wide characters, in a non-negative class containing \S or + similar (should_flip_negation is set), all characters greater than 255 + must be in the class. */ + + if ( +#if defined COMPILE_PCRE8 + utf && +#endif + should_flip_negation && !negate_class && (options & PCRE_UCP) == 0) + { + *class_uchardata++ = XCL_RANGE; + if (utf) /* Will always be utf in the 8-bit library */ + { + class_uchardata += PRIV(ord2utf)(0x100, class_uchardata); + class_uchardata += PRIV(ord2utf)(0x10ffff, class_uchardata); + } + else /* Can only happen for the 16-bit & 32-bit libraries */ + { +#if defined COMPILE_PCRE16 + *class_uchardata++ = 0x100; + *class_uchardata++ = 0xffffu; +#elif defined COMPILE_PCRE32 + *class_uchardata++ = 0x100; + *class_uchardata++ = 0xffffffffu; +#endif + } + } + *class_uchardata++ = XCL_END; /* Marks the end of extra data */ *code++ = OP_XCLASS; code += LINK_SIZE; @@ -5711,6 +5739,21 @@ for (;; ptr++) ptr = p - 1; /* Character before the next significant one. */ } + /* We also need to skip over (?# comments, which are not dependent on + extended mode. */ + + if (ptr[1] == CHAR_LEFT_PARENTHESIS && ptr[2] == CHAR_QUESTION_MARK && + ptr[3] == CHAR_NUMBER_SIGN) + { + ptr += 4; + while (*ptr != CHAR_NULL && *ptr != CHAR_RIGHT_PARENTHESIS) ptr++; + if (*ptr == CHAR_NULL) + { + *errorcodeptr = ERR18; + goto FAILED; + } + } + /* If the next character is '+', we have a possessive quantifier. This implies greediness, whatever the setting of the PCRE_UNGREEDY option. If the next character is '?' this is a minimizing repeat, by default, @@ -6923,7 +6966,8 @@ for (;; ptr++) slot = cd->name_table; for (i = 0; i < cd->names_found; i++) { - if (STRNCMP_UC_UC(name, slot+IMM2_SIZE, namelen) == 0) break; + if (STRNCMP_UC_UC(name, slot+IMM2_SIZE, namelen) == 0 && + slot[IMM2_SIZE+namelen] == 0) break; slot += cd->name_entry_size; } @@ -7889,15 +7933,17 @@ for (;; ptr++) } } - /* For a forward assertion, we take the reqchar, if set. This can be - helpful if the pattern that follows the assertion doesn't set a different - char. For example, it's useful for /(?=abcde).+/. We can't set firstchar - for an assertion, however because it leads to incorrect effect for patterns - such as /(?=a)a.+/ when the "real" "a" would then become a reqchar instead - of a firstchar. This is overcome by a scan at the end if there's no - firstchar, looking for an asserted first char. */ - - else if (bravalue == OP_ASSERT && subreqcharflags >= 0) + /* For a forward assertion, we take the reqchar, if set, provided that the + group has also set a first char. This can be helpful if the pattern that + follows the assertion doesn't set a different char. For example, it's + useful for /(?=abcde).+/. We can't set firstchar for an assertion, however + because it leads to incorrect effect for patterns such as /(?=a)a.+/ when + the "real" "a" would then become a reqchar instead of a firstchar. This is + overcome by a scan at the end if there's no firstchar, looking for an + asserted first char. */ + + else if (bravalue == OP_ASSERT && subreqcharflags >= 0 && + subfirstcharflags >= 0) { reqchar = subreqchar; reqcharflags = subreqcharflags; @@ -8179,7 +8225,6 @@ for (;; ptr++) if (mclength == 1 || req_caseopt == 0) { - firstchar = mcbuffer[0] | req_caseopt; firstchar = mcbuffer[0]; firstcharflags = req_caseopt; @@ -8686,8 +8731,8 @@ matching and for non-DOTALL patterns that start with .* (which must start at the beginning or after \n). As in the case of is_anchored() (see above), we have to take account of back references to capturing brackets that contain .* because in that case we can't make the assumption. Also, the appearance of .* -inside atomic brackets or in a pattern that contains *PRUNE or *SKIP does not -count, because once again the assumption no longer holds. +inside atomic brackets or in an assertion, or in a pattern that contains *PRUNE +or *SKIP does not count, because once again the assumption no longer holds. Arguments: code points to start of expression (the bracket) @@ -8696,13 +8741,14 @@ Arguments: the less precise approach cd points to the compile data atomcount atomic group level + inassert TRUE if in an assertion Returns: TRUE or FALSE */ static BOOL is_startline(const pcre_uchar *code, unsigned int bracket_map, - compile_data *cd, int atomcount) + compile_data *cd, int atomcount, BOOL inassert) { do { const pcre_uchar *scode = first_significant_code( @@ -8729,7 +8775,7 @@ do { return FALSE; default: /* Assertion */ - if (!is_startline(scode, bracket_map, cd, atomcount)) return FALSE; + if (!is_startline(scode, bracket_map, cd, atomcount, TRUE)) return FALSE; do scode += GET(scode, 1); while (*scode == OP_ALT); scode += 1 + LINK_SIZE; break; @@ -8743,7 +8789,7 @@ do { if (op == OP_BRA || op == OP_BRAPOS || op == OP_SBRA || op == OP_SBRAPOS) { - if (!is_startline(scode, bracket_map, cd, atomcount)) return FALSE; + if (!is_startline(scode, bracket_map, cd, atomcount, inassert)) return FALSE; } /* Capturing brackets */ @@ -8753,33 +8799,33 @@ do { { int n = GET2(scode, 1+LINK_SIZE); int new_map = bracket_map | ((n < 32)? (1 << n) : 1); - if (!is_startline(scode, new_map, cd, atomcount)) return FALSE; + if (!is_startline(scode, new_map, cd, atomcount, inassert)) return FALSE; } /* Positive forward assertions */ else if (op == OP_ASSERT) { - if (!is_startline(scode, bracket_map, cd, atomcount)) return FALSE; + if (!is_startline(scode, bracket_map, cd, atomcount, TRUE)) return FALSE; } /* Atomic brackets */ else if (op == OP_ONCE || op == OP_ONCE_NC) { - if (!is_startline(scode, bracket_map, cd, atomcount + 1)) return FALSE; + if (!is_startline(scode, bracket_map, cd, atomcount + 1, inassert)) return FALSE; } /* .* means "start at start or after \n" if it isn't in atomic brackets or - brackets that may be referenced, as long as the pattern does not contain - *PRUNE or *SKIP, because these break the feature. Consider, for example, - /.*?a(*PRUNE)b/ with the subject "aab", which matches "ab", i.e. not at the - start of a line. */ + brackets that may be referenced or an assertion, as long as the pattern does + not contain *PRUNE or *SKIP, because these break the feature. Consider, for + example, /.*?a(*PRUNE)b/ with the subject "aab", which matches "ab", i.e. + not at the start of a line. */ else if (op == OP_TYPESTAR || op == OP_TYPEMINSTAR || op == OP_TYPEPOSSTAR) { if (scode[1] != OP_ANY || (bracket_map & cd->backref_map) != 0 || - atomcount > 0 || cd->had_pruneorskip) + atomcount > 0 || cd->had_pruneorskip || inassert) return FALSE; } @@ -9634,7 +9680,7 @@ if ((re->options & PCRE_ANCHORED) == 0) re->flags |= PCRE_FIRSTSET; } - else if (is_startline(codestart, 0, cd, 0)) re->flags |= PCRE_STARTLINE; + else if (is_startline(codestart, 0, cd, 0, FALSE)) re->flags |= PCRE_STARTLINE; } } diff --git a/src/third_party/pcre-8.39/pcre_config.c b/src/third_party/pcre-8.41/pcre_config.c index 1cbdd9c960c..1cbdd9c960c 100644 --- a/src/third_party/pcre-8.39/pcre_config.c +++ b/src/third_party/pcre-8.41/pcre_config.c diff --git a/src/third_party/pcre-8.39/pcre_dfa_exec.c b/src/third_party/pcre-8.41/pcre_dfa_exec.c index 170ce6a0016..bc09ced3a7c 100644 --- a/src/third_party/pcre-8.39/pcre_dfa_exec.c +++ b/src/third_party/pcre-8.41/pcre_dfa_exec.c @@ -7,7 +7,7 @@ and semantics are as close as possible to those of the Perl 5 language (but see below for why this module is different). Written by Philip Hazel - Copyright (c) 1997-2014 University of Cambridge + Copyright (c) 1997-2017 University of Cambridge ----------------------------------------------------------------------------- Redistribution and use in source and binary forms, with or without @@ -2625,7 +2625,7 @@ for (;;) if (isinclass) { int max = (int)GET2(ecode, 1 + IMM2_SIZE); - if (*ecode == OP_CRPOSRANGE) + if (*ecode == OP_CRPOSRANGE && count >= (int)GET2(ecode, 1)) { active_count--; /* Remove non-match possibility */ next_active_state--; diff --git a/src/third_party/pcre-8.39/pcre_exec.c b/src/third_party/pcre-8.41/pcre_exec.c index 24b23ca2864..1a9bdd546ee 100644 --- a/src/third_party/pcre-8.39/pcre_exec.c +++ b/src/third_party/pcre-8.41/pcre_exec.c @@ -669,7 +669,7 @@ if (ecode == NULL) return match((PCRE_PUCHAR)&rdepth, NULL, NULL, 0, NULL, NULL, 1); else { - int len = (char *)&rdepth - (char *)eptr; + int len = (int)((char *)&rdepth - (char *)eptr); return (len > 0)? -len : len; } } diff --git a/src/third_party/pcre-8.39/pcre_fullinfo.c b/src/third_party/pcre-8.41/pcre_fullinfo.c index a6c2ece6ca5..a6c2ece6ca5 100644 --- a/src/third_party/pcre-8.39/pcre_fullinfo.c +++ b/src/third_party/pcre-8.41/pcre_fullinfo.c diff --git a/src/third_party/pcre-8.39/pcre_get.c b/src/third_party/pcre-8.41/pcre_get.c index 9475d5e88cd..9475d5e88cd 100644 --- a/src/third_party/pcre-8.39/pcre_get.c +++ b/src/third_party/pcre-8.41/pcre_get.c diff --git a/src/third_party/pcre-8.39/pcre_globals.c b/src/third_party/pcre-8.41/pcre_globals.c index 0f106aa9013..0f106aa9013 100644 --- a/src/third_party/pcre-8.39/pcre_globals.c +++ b/src/third_party/pcre-8.41/pcre_globals.c diff --git a/src/third_party/pcre-8.39/pcre_internal.h b/src/third_party/pcre-8.41/pcre_internal.h index 2923b29f82d..97ff55d03b3 100644 --- a/src/third_party/pcre-8.39/pcre_internal.h +++ b/src/third_party/pcre-8.41/pcre_internal.h @@ -2772,6 +2772,9 @@ extern const pcre_uint8 PRIV(ucd_stage1)[]; extern const pcre_uint16 PRIV(ucd_stage2)[]; extern const pcre_uint32 PRIV(ucp_gentype)[]; extern const pcre_uint32 PRIV(ucp_gbtable)[]; +#ifdef COMPILE_PCRE32 +extern const ucd_record PRIV(dummy_ucd_record)[]; +#endif #ifdef SUPPORT_JIT extern const int PRIV(ucp_typerange)[]; #endif @@ -2780,10 +2783,16 @@ extern const int PRIV(ucp_typerange)[]; /* UCD access macros */ #define UCD_BLOCK_SIZE 128 -#define GET_UCD(ch) (PRIV(ucd_records) + \ +#define REAL_GET_UCD(ch) (PRIV(ucd_records) + \ PRIV(ucd_stage2)[PRIV(ucd_stage1)[(int)(ch) / UCD_BLOCK_SIZE] * \ UCD_BLOCK_SIZE + (int)(ch) % UCD_BLOCK_SIZE]) +#ifdef COMPILE_PCRE32 +#define GET_UCD(ch) ((ch > 0x10ffff)? PRIV(dummy_ucd_record) : REAL_GET_UCD(ch)) +#else +#define GET_UCD(ch) REAL_GET_UCD(ch) +#endif + #define UCD_CHARTYPE(ch) GET_UCD(ch)->chartype #define UCD_SCRIPT(ch) GET_UCD(ch)->script #define UCD_CATEGORY(ch) PRIV(ucp_gentype)[UCD_CHARTYPE(ch)] diff --git a/src/third_party/pcre-8.39/pcre_jit_compile.c b/src/third_party/pcre-8.41/pcre_jit_compile.c index 4f15a27ac28..249edbe8e7f 100644 --- a/src/third_party/pcre-8.39/pcre_jit_compile.c +++ b/src/third_party/pcre-8.41/pcre_jit_compile.c @@ -487,7 +487,7 @@ typedef struct compare_context { #undef CMP /* Used for accessing the elements of the stack. */ -#define STACK(i) ((-(i) - 1) * (int)sizeof(sljit_sw)) +#define STACK(i) ((i) * (int)sizeof(sljit_sw)) #define TMP1 SLJIT_R0 #define TMP2 SLJIT_R2 @@ -552,13 +552,15 @@ the start pointers when the end of the capturing group has not yet reached. */ sljit_emit_cmp(compiler, (type), (src1), (src1w), (src2), (src2w)) #define CMPTO(type, src1, src1w, src2, src2w, label) \ sljit_set_label(sljit_emit_cmp(compiler, (type), (src1), (src1w), (src2), (src2w)), (label)) -#define OP_FLAGS(op, dst, dstw, src, srcw, type) \ - sljit_emit_op_flags(compiler, (op), (dst), (dstw), (src), (srcw), (type)) +#define OP_FLAGS(op, dst, dstw, type) \ + sljit_emit_op_flags(compiler, (op), (dst), (dstw), (type)) #define GET_LOCAL_BASE(dst, dstw, offset) \ sljit_get_local_base(compiler, (dst), (dstw), (offset)) #define READ_CHAR_MAX 0x7fffffff +#define INVALID_UTF_CHAR 888 + static pcre_uchar *bracketend(pcre_uchar *cc) { SLJIT_ASSERT((*cc >= OP_ASSERT && *cc <= OP_ASSERTBACK_NOT) || (*cc >= OP_ONCE && *cc <= OP_SCOND)); @@ -784,7 +786,7 @@ switch(*cc) default: /* All opcodes are supported now! */ - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); return NULL; } } @@ -1660,9 +1662,9 @@ while (cc < ccend) { OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), OVECTOR(0)); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, SLJIT_IMM, -OVECTOR(0)); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, TMP1, 0); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); setsom_found = TRUE; } cc += 1; @@ -1676,9 +1678,9 @@ while (cc < ccend) { OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), common->mark_ptr); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, SLJIT_IMM, -common->mark_ptr); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, TMP1, 0); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); setmark_found = TRUE; } cc += 1 + 2 + cc[1]; @@ -1689,27 +1691,27 @@ while (cc < ccend) { OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), OVECTOR(0)); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, SLJIT_IMM, -OVECTOR(0)); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, TMP1, 0); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); setsom_found = TRUE; } if (common->mark_ptr != 0 && !setmark_found) { OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), common->mark_ptr); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, SLJIT_IMM, -common->mark_ptr); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, TMP1, 0); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); setmark_found = TRUE; } if (common->capture_last_ptr != 0 && !capture_last_found) { OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), common->capture_last_ptr); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, SLJIT_IMM, -common->capture_last_ptr); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, TMP1, 0); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); capture_last_found = TRUE; } cc += 1 + LINK_SIZE; @@ -1723,20 +1725,20 @@ while (cc < ccend) { OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), common->capture_last_ptr); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, SLJIT_IMM, -common->capture_last_ptr); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, TMP1, 0); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); capture_last_found = TRUE; } offset = (GET2(cc, 1 + LINK_SIZE)) << 1; OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, SLJIT_IMM, OVECTOR(offset)); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), OVECTOR(offset)); OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(SLJIT_SP), OVECTOR(offset + 1)); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, TMP1, 0); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), stackpos, TMP2, 0); - stackpos += (int)sizeof(sljit_sw); + stackpos -= (int)sizeof(sljit_sw); cc += 1 + LINK_SIZE + IMM2_SIZE; break; @@ -1887,18 +1889,17 @@ BOOL tmp1empty = TRUE; BOOL tmp2empty = TRUE; pcre_uchar *alternative; enum { - start, loop, end } status; -status = save ? start : loop; -stackptr = STACK(stackptr - 2); +status = loop; +stackptr = STACK(stackptr); stacktop = STACK(stacktop - 1); if (!save) { - stackptr += (needs_control_head ? 2 : 1) * sizeof(sljit_sw); + stacktop -= (needs_control_head ? 2 : 1) * sizeof(sljit_sw); if (stackptr < stacktop) { OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STACK_TOP), stackptr); @@ -1914,196 +1915,186 @@ if (!save) /* The tmp1next must be TRUE in either way. */ } +SLJIT_ASSERT(common->recursive_head_ptr != 0); + do { count = 0; - switch(status) + if (cc >= ccend) { - case start: - SLJIT_ASSERT(save && common->recursive_head_ptr != 0); + if (!save) + break; + count = 1; srcw[0] = common->recursive_head_ptr; if (needs_control_head) { SLJIT_ASSERT(common->control_head_ptr != 0); count = 2; - srcw[1] = common->control_head_ptr; + srcw[0] = common->control_head_ptr; + srcw[1] = common->recursive_head_ptr; + } + status = end; + } + else switch(*cc) + { + case OP_KET: + if (PRIVATE_DATA(cc) != 0) + { + count = 1; + srcw[0] = PRIVATE_DATA(cc); + SLJIT_ASSERT(PRIVATE_DATA(cc + 1) != 0); + cc += PRIVATE_DATA(cc + 1); } - status = loop; + cc += 1 + LINK_SIZE; + break; + + case OP_ASSERT: + case OP_ASSERT_NOT: + case OP_ASSERTBACK: + case OP_ASSERTBACK_NOT: + case OP_ONCE: + case OP_ONCE_NC: + case OP_BRAPOS: + case OP_SBRA: + case OP_SBRAPOS: + case OP_SCOND: + count = 1; + srcw[0] = PRIVATE_DATA(cc); + SLJIT_ASSERT(srcw[0] != 0); + cc += 1 + LINK_SIZE; break; - case loop: - if (cc >= ccend) + case OP_CBRA: + case OP_SCBRA: + if (common->optimized_cbracket[GET2(cc, 1 + LINK_SIZE)] == 0) { - status = end; - break; + count = 1; + srcw[0] = OVECTOR_PRIV(GET2(cc, 1 + LINK_SIZE)); } + cc += 1 + LINK_SIZE + IMM2_SIZE; + break; - switch(*cc) - { - case OP_KET: - if (PRIVATE_DATA(cc) != 0) - { - count = 1; - srcw[0] = PRIVATE_DATA(cc); - SLJIT_ASSERT(PRIVATE_DATA(cc + 1) != 0); - cc += PRIVATE_DATA(cc + 1); - } - cc += 1 + LINK_SIZE; - break; + case OP_CBRAPOS: + case OP_SCBRAPOS: + count = 2; + srcw[0] = PRIVATE_DATA(cc); + srcw[1] = OVECTOR_PRIV(GET2(cc, 1 + LINK_SIZE)); + SLJIT_ASSERT(srcw[0] != 0 && srcw[1] != 0); + cc += 1 + LINK_SIZE + IMM2_SIZE; + break; - case OP_ASSERT: - case OP_ASSERT_NOT: - case OP_ASSERTBACK: - case OP_ASSERTBACK_NOT: - case OP_ONCE: - case OP_ONCE_NC: - case OP_BRAPOS: - case OP_SBRA: - case OP_SBRAPOS: - case OP_SCOND: + case OP_COND: + /* Might be a hidden SCOND. */ + alternative = cc + GET(cc, 1); + if (*alternative == OP_KETRMAX || *alternative == OP_KETRMIN) + { count = 1; srcw[0] = PRIVATE_DATA(cc); SLJIT_ASSERT(srcw[0] != 0); - cc += 1 + LINK_SIZE; - break; - - case OP_CBRA: - case OP_SCBRA: - if (common->optimized_cbracket[GET2(cc, 1 + LINK_SIZE)] == 0) - { - count = 1; - srcw[0] = OVECTOR_PRIV(GET2(cc, 1 + LINK_SIZE)); - } - cc += 1 + LINK_SIZE + IMM2_SIZE; - break; + } + cc += 1 + LINK_SIZE; + break; - case OP_CBRAPOS: - case OP_SCBRAPOS: - count = 2; + CASE_ITERATOR_PRIVATE_DATA_1 + if (PRIVATE_DATA(cc)) + { + count = 1; srcw[0] = PRIVATE_DATA(cc); - srcw[1] = OVECTOR_PRIV(GET2(cc, 1 + LINK_SIZE)); - SLJIT_ASSERT(srcw[0] != 0 && srcw[1] != 0); - cc += 1 + LINK_SIZE + IMM2_SIZE; - break; - - case OP_COND: - /* Might be a hidden SCOND. */ - alternative = cc + GET(cc, 1); - if (*alternative == OP_KETRMAX || *alternative == OP_KETRMIN) - { - count = 1; - srcw[0] = PRIVATE_DATA(cc); - SLJIT_ASSERT(srcw[0] != 0); - } - cc += 1 + LINK_SIZE; - break; - - CASE_ITERATOR_PRIVATE_DATA_1 - if (PRIVATE_DATA(cc)) - { - count = 1; - srcw[0] = PRIVATE_DATA(cc); - } - cc += 2; + } + cc += 2; #ifdef SUPPORT_UTF - if (common->utf && HAS_EXTRALEN(cc[-1])) cc += GET_EXTRALEN(cc[-1]); + if (common->utf && HAS_EXTRALEN(cc[-1])) cc += GET_EXTRALEN(cc[-1]); #endif - break; + break; - CASE_ITERATOR_PRIVATE_DATA_2A - if (PRIVATE_DATA(cc)) - { - count = 2; - srcw[0] = PRIVATE_DATA(cc); - srcw[1] = PRIVATE_DATA(cc) + sizeof(sljit_sw); - } - cc += 2; + CASE_ITERATOR_PRIVATE_DATA_2A + if (PRIVATE_DATA(cc)) + { + count = 2; + srcw[0] = PRIVATE_DATA(cc); + srcw[1] = PRIVATE_DATA(cc) + sizeof(sljit_sw); + } + cc += 2; #ifdef SUPPORT_UTF - if (common->utf && HAS_EXTRALEN(cc[-1])) cc += GET_EXTRALEN(cc[-1]); + if (common->utf && HAS_EXTRALEN(cc[-1])) cc += GET_EXTRALEN(cc[-1]); #endif - break; + break; - CASE_ITERATOR_PRIVATE_DATA_2B - if (PRIVATE_DATA(cc)) - { - count = 2; - srcw[0] = PRIVATE_DATA(cc); - srcw[1] = PRIVATE_DATA(cc) + sizeof(sljit_sw); - } - cc += 2 + IMM2_SIZE; + CASE_ITERATOR_PRIVATE_DATA_2B + if (PRIVATE_DATA(cc)) + { + count = 2; + srcw[0] = PRIVATE_DATA(cc); + srcw[1] = PRIVATE_DATA(cc) + sizeof(sljit_sw); + } + cc += 2 + IMM2_SIZE; #ifdef SUPPORT_UTF - if (common->utf && HAS_EXTRALEN(cc[-1])) cc += GET_EXTRALEN(cc[-1]); + if (common->utf && HAS_EXTRALEN(cc[-1])) cc += GET_EXTRALEN(cc[-1]); #endif - break; + break; - CASE_ITERATOR_TYPE_PRIVATE_DATA_1 - if (PRIVATE_DATA(cc)) + CASE_ITERATOR_TYPE_PRIVATE_DATA_1 + if (PRIVATE_DATA(cc)) + { + count = 1; + srcw[0] = PRIVATE_DATA(cc); + } + cc += 1; + break; + + CASE_ITERATOR_TYPE_PRIVATE_DATA_2A + if (PRIVATE_DATA(cc)) + { + count = 2; + srcw[0] = PRIVATE_DATA(cc); + srcw[1] = srcw[0] + sizeof(sljit_sw); + } + cc += 1; + break; + + CASE_ITERATOR_TYPE_PRIVATE_DATA_2B + if (PRIVATE_DATA(cc)) + { + count = 2; + srcw[0] = PRIVATE_DATA(cc); + srcw[1] = srcw[0] + sizeof(sljit_sw); + } + cc += 1 + IMM2_SIZE; + break; + + case OP_CLASS: + case OP_NCLASS: +#if defined SUPPORT_UTF || !defined COMPILE_PCRE8 + case OP_XCLASS: + size = (*cc == OP_XCLASS) ? GET(cc, 1) : 1 + 32 / (int)sizeof(pcre_uchar); +#else + size = 1 + 32 / (int)sizeof(pcre_uchar); +#endif + if (PRIVATE_DATA(cc)) + switch(get_class_iterator_size(cc + size)) { + case 1: count = 1; srcw[0] = PRIVATE_DATA(cc); - } - cc += 1; - break; + break; - CASE_ITERATOR_TYPE_PRIVATE_DATA_2A - if (PRIVATE_DATA(cc)) - { + case 2: count = 2; srcw[0] = PRIVATE_DATA(cc); srcw[1] = srcw[0] + sizeof(sljit_sw); - } - cc += 1; - break; + break; - CASE_ITERATOR_TYPE_PRIVATE_DATA_2B - if (PRIVATE_DATA(cc)) - { - count = 2; - srcw[0] = PRIVATE_DATA(cc); - srcw[1] = srcw[0] + sizeof(sljit_sw); + default: + SLJIT_UNREACHABLE(); + break; } - cc += 1 + IMM2_SIZE; - break; - - case OP_CLASS: - case OP_NCLASS: -#if defined SUPPORT_UTF || !defined COMPILE_PCRE8 - case OP_XCLASS: - size = (*cc == OP_XCLASS) ? GET(cc, 1) : 1 + 32 / (int)sizeof(pcre_uchar); -#else - size = 1 + 32 / (int)sizeof(pcre_uchar); -#endif - if (PRIVATE_DATA(cc)) - switch(get_class_iterator_size(cc + size)) - { - case 1: - count = 1; - srcw[0] = PRIVATE_DATA(cc); - break; - - case 2: - count = 2; - srcw[0] = PRIVATE_DATA(cc); - srcw[1] = srcw[0] + sizeof(sljit_sw); - break; - - default: - SLJIT_ASSERT_STOP(); - break; - } - cc += size; - break; - - default: - cc = next_opcode(common, cc); - SLJIT_ASSERT(cc != NULL); - break; - } + cc += size; break; - case end: - SLJIT_ASSERT_STOP(); + default: + cc = next_opcode(common, cc); + SLJIT_ASSERT(cc != NULL); break; } @@ -2312,7 +2303,7 @@ static SLJIT_INLINE void count_match(compiler_common *common) { DEFINE_COMPILER; -OP2(SLJIT_SUB | SLJIT_SET_E, COUNT_MATCH, 0, COUNT_MATCH, 0, SLJIT_IMM, 1); +OP2(SLJIT_SUB | SLJIT_SET_Z, COUNT_MATCH, 0, COUNT_MATCH, 0, SLJIT_IMM, 1); add_jump(compiler, &common->calllimit, JUMP(SLJIT_ZERO)); } @@ -2322,7 +2313,7 @@ static SLJIT_INLINE void allocate_stack(compiler_common *common, int size) DEFINE_COMPILER; SLJIT_ASSERT(size > 0); -OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, size * sizeof(sljit_sw)); +OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, size * sizeof(sljit_sw)); #ifdef DESTROY_REGISTERS OP1(SLJIT_MOV, TMP1, 0, SLJIT_IMM, 12345); OP1(SLJIT_MOV, TMP3, 0, TMP1, 0); @@ -2330,7 +2321,7 @@ OP1(SLJIT_MOV, RETURN_ADDR, 0, TMP1, 0); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), LOCALS0, TMP1, 0); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), LOCALS1, TMP1, 0); #endif -add_stub(common, CMP(SLJIT_GREATER, STACK_TOP, 0, STACK_LIMIT, 0)); +add_stub(common, CMP(SLJIT_LESS, STACK_TOP, 0, STACK_LIMIT, 0)); } static SLJIT_INLINE void free_stack(compiler_common *common, int size) @@ -2338,7 +2329,7 @@ static SLJIT_INLINE void free_stack(compiler_common *common, int size) DEFINE_COMPILER; SLJIT_ASSERT(size > 0); -OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, size * sizeof(sljit_sw)); +OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, size * sizeof(sljit_sw)); } static sljit_uw * allocate_read_only_data(compiler_common *common, sljit_uw size) @@ -2396,7 +2387,7 @@ else OP1(SLJIT_MOV, SLJIT_R2, 0, SLJIT_IMM, length - 1); loop = LABEL(); OP1(SLJIT_MOVU, SLJIT_MEM1(SLJIT_R1), sizeof(sljit_sw), SLJIT_R0, 0); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_R2, 0, SLJIT_R2, 0, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_R2, 0, SLJIT_R2, 0, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, loop); } } @@ -2434,7 +2425,7 @@ else OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_IMM, length - 2); loop = LABEL(); OP1(SLJIT_MOVU, SLJIT_MEM1(TMP2), sizeof(sljit_sw), TMP1, 0); - OP2(SLJIT_SUB | SLJIT_SET_E, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, loop); } @@ -2452,22 +2443,22 @@ static sljit_sw SLJIT_CALL do_search_mark(sljit_sw *current, const pcre_uchar *s { while (current != NULL) { - switch (current[-2]) + switch (current[1]) { case type_then_trap: break; case type_mark: - if (STRCMP_UC_UC(skip_arg, (pcre_uchar *)current[-3]) == 0) - return current[-4]; + if (STRCMP_UC_UC(skip_arg, (pcre_uchar *)current[2]) == 0) + return current[3]; break; default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); break; } - SLJIT_ASSERT(current > (sljit_sw*)current[-1]); - current = (sljit_sw*)current[-1]; + SLJIT_ASSERT(current[0] == 0 || current < (sljit_sw*)current[0]); + current = (sljit_sw*)current[0]; } return -1; } @@ -2501,7 +2492,7 @@ OP2(SLJIT_ADD, SLJIT_S0, 0, SLJIT_S0, 0, SLJIT_IMM, sizeof(sljit_sw)); OP2(SLJIT_ASHR, SLJIT_S1, 0, SLJIT_S1, 0, SLJIT_IMM, UCHAR_SHIFT); #endif OP1(SLJIT_MOVU_S32, SLJIT_MEM1(SLJIT_R2), sizeof(int), SLJIT_S1, 0); -OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_R1, 0, SLJIT_R1, 0, SLJIT_IMM, 1); +OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_R1, 0, SLJIT_R1, 0, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, loop); JUMPHERE(early_quit); @@ -3106,8 +3097,8 @@ if (common->utf) OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(1)); /* Skip low surrogate if necessary. */ OP2(SLJIT_AND, TMP1, 0, TMP1, 0, SLJIT_IMM, 0xfc00); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xdc00); - OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xdc00); + OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_EQUAL); OP2(SLJIT_SHL, TMP1, 0, TMP1, 0, SLJIT_IMM, 1); OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, TMP1, 0); return; @@ -3126,6 +3117,7 @@ struct sljit_jump *jump; if (nltype == NLTYPE_ANY) { add_jump(compiler, &common->anynewline, JUMP(SLJIT_FAST_CALL)); + sljit_set_current_flags(compiler, SLJIT_SET_Z); add_jump(compiler, backtracks, JUMP(jumpifmatch ? SLJIT_NOT_ZERO : SLJIT_ZERO)); } else if (nltype == NLTYPE_ANYCRLF) @@ -3167,7 +3159,7 @@ OP2(SLJIT_AND, TMP2, 0, TMP2, 0, SLJIT_IMM, 0x3f); OP2(SLJIT_OR, TMP1, 0, TMP1, 0, TMP2, 0); /* Searching for the first zero. */ -OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x800); +OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x800); jump = JUMP(SLJIT_NOT_ZERO); /* Two byte sequence. */ OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(1)); @@ -3181,7 +3173,7 @@ OP2(SLJIT_SHL, TMP1, 0, TMP1, 0, SLJIT_IMM, 6); OP2(SLJIT_AND, TMP2, 0, TMP2, 0, SLJIT_IMM, 0x3f); OP2(SLJIT_OR, TMP1, 0, TMP1, 0, TMP2, 0); -OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x10000); +OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x10000); jump = JUMP(SLJIT_NOT_ZERO); /* Three byte sequence. */ OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(2)); @@ -3215,15 +3207,15 @@ OP2(SLJIT_AND, TMP2, 0, TMP2, 0, SLJIT_IMM, 0x3f); OP2(SLJIT_OR, TMP1, 0, TMP1, 0, TMP2, 0); /* Searching for the first zero. */ -OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x800); +OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x800); jump = JUMP(SLJIT_NOT_ZERO); /* Two byte sequence. */ OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(1)); sljit_emit_fast_return(compiler, RETURN_ADDR, 0); JUMPHERE(jump); -OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x400); -OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_NOT_ZERO); +OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x400); +OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_NOT_ZERO); /* This code runs only in 8 bit mode. No need to shift the value. */ OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, TMP2, 0); OP1(MOV_UCHAR, TMP2, 0, SLJIT_MEM1(STR_PTR), IN_UCHARS(1)); @@ -3246,7 +3238,7 @@ struct sljit_jump *compare; sljit_emit_fast_enter(compiler, RETURN_ADDR, 0); -OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP2, 0, SLJIT_IMM, 0x20); +OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP2, 0, SLJIT_IMM, 0x20); jump = JUMP(SLJIT_NOT_ZERO); /* Two byte sequence. */ OP1(MOV_UCHAR, TMP1, 0, SLJIT_MEM1(STR_PTR), IN_UCHARS(0)); @@ -3287,10 +3279,30 @@ static void do_getucd(compiler_common *common) /* Search the UCD record for the character comes in TMP1. Returns chartype in TMP1 and UCD offset in TMP2. */ DEFINE_COMPILER; +#ifdef COMPILE_PCRE32 +struct sljit_jump *jump; +#endif + +#if defined SLJIT_DEBUG && SLJIT_DEBUG +/* dummy_ucd_record */ +const ucd_record *record = GET_UCD(INVALID_UTF_CHAR); +SLJIT_ASSERT(record->script == ucp_Common && record->chartype == ucp_Cn && record->gbprop == ucp_gbOther); +SLJIT_ASSERT(record->caseset == 0 && record->other_case == 0); +#endif SLJIT_ASSERT(UCD_BLOCK_SIZE == 128 && sizeof(ucd_record) == 8); sljit_emit_fast_enter(compiler, RETURN_ADDR, 0); + +#ifdef COMPILE_PCRE32 +if (!common->utf) + { + jump = CMP(SLJIT_LESS, TMP1, 0, SLJIT_IMM, 0x10ffff + 1); + OP1(SLJIT_MOV, TMP1, 0, SLJIT_IMM, INVALID_UTF_CHAR); + JUMPHERE(jump); + } +#endif + OP2(SLJIT_LSHR, TMP2, 0, TMP1, 0, SLJIT_IMM, UCD_BLOCK_SHIFT); OP1(SLJIT_MOV_U8, TMP2, 0, SLJIT_MEM1(TMP2), (sljit_sw)PRIV(ucd_stage1)); OP2(SLJIT_AND, TMP1, 0, TMP1, 0, SLJIT_IMM, UCD_BLOCK_MASK); @@ -3365,8 +3377,8 @@ if (newlinecheck) OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(1)); end = CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0); OP1(MOV_UCHAR, TMP1, 0, SLJIT_MEM1(STR_PTR), 0); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, common->newline & 0xff); - OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, common->newline & 0xff); + OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_EQUAL); #if defined COMPILE_PCRE16 || defined COMPILE_PCRE32 OP2(SLJIT_SHL, TMP1, 0, TMP1, 0, SLJIT_IMM, UCHAR_SHIFT); #endif @@ -3403,8 +3415,8 @@ if (common->utf) { singlechar = CMP(SLJIT_LESS, TMP1, 0, SLJIT_IMM, 0xd800); OP2(SLJIT_AND, TMP1, 0, TMP1, 0, SLJIT_IMM, 0xfc00); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xd800); - OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xd800); + OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_EQUAL); OP2(SLJIT_SHL, TMP1, 0, TMP1, 0, SLJIT_IMM, 1); OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, TMP1, 0); JUMPHERE(singlechar); @@ -3853,7 +3865,7 @@ while (TRUE) } } -#if (defined SLJIT_CONFIG_X86 && SLJIT_CONFIG_X86) +#if (defined SLJIT_CONFIG_X86 && SLJIT_CONFIG_X86) && !(defined SUPPORT_VALGRIND) static sljit_s32 character_to_int32(pcre_uchar chr) { @@ -4004,12 +4016,12 @@ sljit_emit_op_custom(compiler, instruction, 4); if (load_twice) { - OP1(SLJIT_MOV, TMP3, 0, TMP2, 0); + OP1(SLJIT_MOV, RETURN_ADDR, 0, TMP2, 0); instruction[3] = 0xc0 | (tmp2_ind << 3) | 1; sljit_emit_op_custom(compiler, instruction, 4); OP2(SLJIT_OR, TMP1, 0, TMP1, 0, TMP2, 0); - OP1(SLJIT_MOV, TMP2, 0, TMP3, 0); + OP1(SLJIT_MOV, TMP2, 0, RETURN_ADDR, 0); } OP2(SLJIT_ASHR, TMP1, 0, TMP1, 0, TMP2, 0); @@ -4019,6 +4031,7 @@ instruction[0] = 0x0f; instruction[1] = 0xbc; instruction[2] = 0xc0 | (tmp1_ind << 3) | tmp1_ind; sljit_emit_op_custom(compiler, instruction, 3); +sljit_set_current_flags(compiler, SLJIT_SET_Z); nomatch = JUMP(SLJIT_ZERO); @@ -4119,6 +4132,7 @@ instruction[0] = 0x0f; instruction[1] = 0xbc; instruction[2] = 0xc0 | (tmp1_ind << 3) | tmp1_ind; sljit_emit_op_custom(compiler, instruction, 3); +sljit_set_current_flags(compiler, SLJIT_SET_Z); JUMPTO(SLJIT_ZERO, start); @@ -4155,18 +4169,8 @@ if (has_match_end) OP1(SLJIT_MOV, TMP3, 0, STR_END, 0); OP2(SLJIT_ADD, STR_END, 0, SLJIT_MEM1(SLJIT_SP), common->match_end_ptr, SLJIT_IMM, IN_UCHARS(offset + 1)); -#if (defined SLJIT_CONFIG_X86 && SLJIT_CONFIG_X86) - if (sljit_x86_is_cmov_available()) - { - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, STR_END, 0, TMP3, 0); - sljit_x86_emit_cmov(compiler, SLJIT_GREATER, STR_END, TMP3, 0); - } -#endif - { - quit = CMP(SLJIT_LESS_EQUAL, STR_END, 0, TMP3, 0); - OP1(SLJIT_MOV, STR_END, 0, TMP3, 0); - JUMPHERE(quit); - } + OP2(SLJIT_SUB | SLJIT_SET_GREATER, SLJIT_UNUSED, 0, STR_END, 0, TMP3, 0); + sljit_emit_cmov(compiler, SLJIT_GREATER, STR_END, TMP3, 0); } #if defined SUPPORT_UTF && !defined COMPILE_PCRE32 @@ -4174,11 +4178,11 @@ if (common->utf && offset > 0) utf_start = LABEL(); #endif -#if (defined SLJIT_CONFIG_X86 && SLJIT_CONFIG_X86) +#if (defined SLJIT_CONFIG_X86 && SLJIT_CONFIG_X86) && !(defined SUPPORT_VALGRIND) /* SSE2 accelerated first character search. */ -if (sljit_x86_is_sse2_available()) +if (sljit_has_cpu_feature(SLJIT_HAS_SSE2)) { fast_forward_first_char2_sse2(common, char1, char2); @@ -4213,16 +4217,16 @@ if (sljit_x86_is_sse2_available()) if (offset > 0) OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(offset)); } - else if (sljit_x86_is_cmov_available()) - { - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, STR_PTR, 0, STR_END, 0); - sljit_x86_emit_cmov(compiler, SLJIT_GREATER_EQUAL, STR_PTR, has_match_end ? SLJIT_MEM1(SLJIT_SP) : STR_END, has_match_end ? common->match_end_ptr : 0); - } else { - quit = CMP(SLJIT_LESS, STR_PTR, 0, STR_END, 0); - OP1(SLJIT_MOV, STR_PTR, 0, has_match_end ? SLJIT_MEM1(SLJIT_SP) : STR_END, has_match_end ? common->match_end_ptr : 0); - JUMPHERE(quit); + OP2(SLJIT_SUB | SLJIT_SET_GREATER_EQUAL, SLJIT_UNUSED, 0, STR_PTR, 0, STR_END, 0); + if (has_match_end) + { + OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), common->match_end_ptr); + sljit_emit_cmov(compiler, SLJIT_GREATER_EQUAL, STR_PTR, TMP1, 0); + } + else + sljit_emit_cmov(compiler, SLJIT_GREATER_EQUAL, STR_PTR, STR_END, 0); } if (has_match_end) @@ -4249,10 +4253,10 @@ else } else { - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, char1); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, char2); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, char1); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, char2); + OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_EQUAL); found = JUMP(SLJIT_NOT_ZERO); } } @@ -4571,8 +4575,8 @@ if (common->nltype == NLTYPE_FIXED && common->newline > 255) firstchar = CMP(SLJIT_LESS_EQUAL, STR_PTR, 0, TMP2, 0); OP2(SLJIT_ADD, TMP1, 0, TMP1, 0, SLJIT_IMM, IN_UCHARS(2)); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, STR_PTR, 0, TMP1, 0); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_GREATER_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_GREATER_EQUAL, SLJIT_UNUSED, 0, STR_PTR, 0, TMP1, 0); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_GREATER_EQUAL); #if defined COMPILE_PCRE16 || defined COMPILE_PCRE32 OP2(SLJIT_SHL, TMP2, 0, TMP2, 0, SLJIT_IMM, UCHAR_SHIFT); #endif @@ -4616,8 +4620,8 @@ if (common->nltype == NLTYPE_ANY || common->nltype == NLTYPE_ANYCRLF) JUMPHERE(foundcr); notfoundnl = CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0); OP1(MOV_UCHAR, TMP1, 0, SLJIT_MEM1(STR_PTR), 0); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, CHAR_NL); - OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, CHAR_NL); + OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_EQUAL); #if defined COMPILE_PCRE16 || defined COMPILE_PCRE32 OP2(SLJIT_SHL, TMP1, 0, TMP1, 0, SLJIT_IMM, UCHAR_SHIFT); #endif @@ -4670,7 +4674,7 @@ if (!check_class_ranges(common, start_bits, (start_bits[31] & 0x80) != 0, TRUE, OP2(SLJIT_LSHR, TMP1, 0, TMP1, 0, SLJIT_IMM, 3); OP1(SLJIT_MOV_U8, TMP1, 0, SLJIT_MEM1(TMP1), (sljit_sw)start_bits); OP2(SLJIT_SHL, TMP2, 0, SLJIT_IMM, 1, TMP2, 0); - OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, TMP2, 0); + OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, TMP2, 0); found = JUMP(SLJIT_NOT_ZERO); } @@ -4692,8 +4696,8 @@ if (common->utf) { CMPTO(SLJIT_LESS, TMP1, 0, SLJIT_IMM, 0xd800, start); OP2(SLJIT_AND, TMP1, 0, TMP1, 0, SLJIT_IMM, 0xfc00); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xd800); - OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xd800); + OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_EQUAL); OP2(SLJIT_SHL, TMP1, 0, TMP1, 0, SLJIT_IMM, 1); OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, TMP1, 0); } @@ -4780,31 +4784,31 @@ struct sljit_jump *jump; struct sljit_label *mainloop; sljit_emit_fast_enter(compiler, RETURN_ADDR, 0); -OP1(SLJIT_MOV, TMP1, 0, STACK_TOP, 0); -GET_LOCAL_BASE(TMP3, 0, 0); +OP1(SLJIT_MOV, TMP3, 0, STACK_TOP, 0); +GET_LOCAL_BASE(TMP1, 0, 0); /* Drop frames until we reach STACK_TOP. */ mainloop = LABEL(); -OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(TMP1), 0); -OP2(SLJIT_SUB | SLJIT_SET_S, SLJIT_UNUSED, 0, TMP2, 0, SLJIT_IMM, 0); -jump = JUMP(SLJIT_SIG_LESS_EQUAL); - -OP2(SLJIT_ADD, TMP2, 0, TMP2, 0, TMP3, 0); -OP1(SLJIT_MOV, SLJIT_MEM1(TMP2), 0, SLJIT_MEM1(TMP1), sizeof(sljit_sw)); -OP1(SLJIT_MOV, SLJIT_MEM1(TMP2), sizeof(sljit_sw), SLJIT_MEM1(TMP1), 2 * sizeof(sljit_sw)); -OP2(SLJIT_ADD, TMP1, 0, TMP1, 0, SLJIT_IMM, 3 * sizeof(sljit_sw)); +OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(STACK_TOP), -sizeof(sljit_sw)); +jump = CMP(SLJIT_SIG_LESS_EQUAL, TMP2, 0, SLJIT_IMM, 0); + +OP2(SLJIT_ADD, TMP2, 0, TMP2, 0, TMP1, 0); +OP1(SLJIT_MOV, SLJIT_MEM1(TMP2), 0, SLJIT_MEM1(STACK_TOP), -2 * sizeof(sljit_sw)); +OP1(SLJIT_MOV, SLJIT_MEM1(TMP2), sizeof(sljit_sw), SLJIT_MEM1(STACK_TOP), -3 * sizeof(sljit_sw)); +OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, 3 * sizeof(sljit_sw)); JUMPTO(SLJIT_JUMP, mainloop); JUMPHERE(jump); -jump = JUMP(SLJIT_SIG_LESS); -/* End of dropping frames. */ +jump = CMP(SLJIT_NOT_ZERO /* SIG_LESS */, TMP2, 0, SLJIT_IMM, 0); +/* End of reverting values. */ +OP1(SLJIT_MOV, STACK_TOP, 0, TMP3, 0); sljit_emit_fast_return(compiler, RETURN_ADDR, 0); JUMPHERE(jump); OP1(SLJIT_NEG, TMP2, 0, TMP2, 0); -OP2(SLJIT_ADD, TMP2, 0, TMP2, 0, TMP3, 0); -OP1(SLJIT_MOV, SLJIT_MEM1(TMP2), 0, SLJIT_MEM1(TMP1), sizeof(sljit_sw)); -OP2(SLJIT_ADD, TMP1, 0, TMP1, 0, SLJIT_IMM, 2 * sizeof(sljit_sw)); +OP2(SLJIT_ADD, TMP2, 0, TMP2, 0, TMP1, 0); +OP1(SLJIT_MOV, SLJIT_MEM1(TMP2), 0, SLJIT_MEM1(STACK_TOP), -2 * sizeof(sljit_sw)); +OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, 2 * sizeof(sljit_sw)); JUMPTO(SLJIT_JUMP, mainloop); } @@ -4837,11 +4841,11 @@ if (common->use_ucp) jump = CMP(SLJIT_EQUAL, TMP1, 0, SLJIT_IMM, CHAR_UNDERSCORE); add_jump(compiler, &common->getucd, JUMP(SLJIT_FAST_CALL)); OP2(SLJIT_SUB, TMP1, 0, TMP1, 0, SLJIT_IMM, ucp_Ll); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ucp_Lu - ucp_Ll); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ucp_Lu - ucp_Ll); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_LESS_EQUAL); OP2(SLJIT_SUB, TMP1, 0, TMP1, 0, SLJIT_IMM, ucp_Nd - ucp_Ll); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ucp_No - ucp_Nd); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ucp_No - ucp_Nd); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_LESS_EQUAL); JUMPHERE(jump); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), LOCALS1, TMP2, 0); } @@ -4881,11 +4885,11 @@ if (common->use_ucp) jump = CMP(SLJIT_EQUAL, TMP1, 0, SLJIT_IMM, CHAR_UNDERSCORE); add_jump(compiler, &common->getucd, JUMP(SLJIT_FAST_CALL)); OP2(SLJIT_SUB, TMP1, 0, TMP1, 0, SLJIT_IMM, ucp_Ll); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ucp_Lu - ucp_Ll); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ucp_Lu - ucp_Ll); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_LESS_EQUAL); OP2(SLJIT_SUB, TMP1, 0, TMP1, 0, SLJIT_IMM, ucp_Nd - ucp_Ll); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ucp_No - ucp_Nd); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ucp_No - ucp_Nd); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_LESS_EQUAL); JUMPHERE(jump); } else @@ -4913,7 +4917,7 @@ else } set_jumps(skipread_list, LABEL()); -OP2(SLJIT_XOR | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP2, 0, SLJIT_MEM1(SLJIT_SP), LOCALS1); +OP2(SLJIT_XOR | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP2, 0, SLJIT_MEM1(SLJIT_SP), LOCALS1); sljit_emit_fast_return(compiler, SLJIT_MEM1(SLJIT_SP), LOCALS0); } @@ -5064,7 +5068,7 @@ switch(length) return TRUE; default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); return FALSE; } } @@ -5077,22 +5081,22 @@ DEFINE_COMPILER; sljit_emit_fast_enter(compiler, RETURN_ADDR, 0); OP2(SLJIT_SUB, TMP1, 0, TMP1, 0, SLJIT_IMM, 0x0a); -OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x0d - 0x0a); -OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_LESS_EQUAL); -OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x85 - 0x0a); +OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x0d - 0x0a); +OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_LESS_EQUAL); +OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x85 - 0x0a); #if defined SUPPORT_UTF || defined COMPILE_PCRE16 || defined COMPILE_PCRE32 #ifdef COMPILE_PCRE8 if (common->utf) { #endif - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); OP2(SLJIT_OR, TMP1, 0, TMP1, 0, SLJIT_IMM, 0x1); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x2029 - 0x0a); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x2029 - 0x0a); #ifdef COMPILE_PCRE8 } #endif #endif /* SUPPORT_UTF || COMPILE_PCRE16 || COMPILE_PCRE32 */ -OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_EQUAL); +OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_EQUAL); sljit_emit_fast_return(compiler, RETURN_ADDR, 0); } @@ -5103,34 +5107,34 @@ DEFINE_COMPILER; sljit_emit_fast_enter(compiler, RETURN_ADDR, 0); -OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x09); -OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); -OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x20); -OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); -OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xa0); +OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x09); +OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_EQUAL); +OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x20); +OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); +OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xa0); #if defined SUPPORT_UTF || defined COMPILE_PCRE16 || defined COMPILE_PCRE32 #ifdef COMPILE_PCRE8 if (common->utf) { #endif - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x1680); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x180e); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x1680); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x180e); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); OP2(SLJIT_SUB, TMP1, 0, TMP1, 0, SLJIT_IMM, 0x2000); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x200A - 0x2000); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_LESS_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x202f - 0x2000); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x205f - 0x2000); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x3000 - 0x2000); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x200A - 0x2000); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x202f - 0x2000); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x205f - 0x2000); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x3000 - 0x2000); #ifdef COMPILE_PCRE8 } #endif #endif /* SUPPORT_UTF || COMPILE_PCRE16 || COMPILE_PCRE32 */ -OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_EQUAL); +OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_EQUAL); sljit_emit_fast_return(compiler, RETURN_ADDR, 0); } @@ -5143,22 +5147,22 @@ DEFINE_COMPILER; sljit_emit_fast_enter(compiler, RETURN_ADDR, 0); OP2(SLJIT_SUB, TMP1, 0, TMP1, 0, SLJIT_IMM, 0x0a); -OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x0d - 0x0a); -OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_LESS_EQUAL); -OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x85 - 0x0a); +OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x0d - 0x0a); +OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_LESS_EQUAL); +OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x85 - 0x0a); #if defined SUPPORT_UTF || defined COMPILE_PCRE16 || defined COMPILE_PCRE32 #ifdef COMPILE_PCRE8 if (common->utf) { #endif - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); OP2(SLJIT_OR, TMP1, 0, TMP1, 0, SLJIT_IMM, 0x1); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x2029 - 0x0a); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x2029 - 0x0a); #ifdef COMPILE_PCRE8 } #endif #endif /* SUPPORT_UTF || COMPILE_PCRE16 || COMPILE_PCRE32 */ -OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_EQUAL); +OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_EQUAL); sljit_emit_fast_return(compiler, RETURN_ADDR, 0); } @@ -5183,7 +5187,7 @@ label = LABEL(); OP1(MOVU_UCHAR, CHAR1, 0, SLJIT_MEM1(TMP1), IN_UCHARS(1)); OP1(MOVU_UCHAR, CHAR2, 0, SLJIT_MEM1(STR_PTR), IN_UCHARS(1)); jump = CMP(SLJIT_NOT_EQUAL, CHAR1, 0, CHAR2, 0); -OP2(SLJIT_SUB | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_IMM, IN_UCHARS(1)); +OP2(SLJIT_SUB | SLJIT_SET_Z, TMP2, 0, TMP2, 0, SLJIT_IMM, IN_UCHARS(1)); JUMPTO(SLJIT_NOT_ZERO, label); JUMPHERE(jump); @@ -5227,7 +5231,7 @@ OP1(SLJIT_MOV_U8, CHAR2, 0, SLJIT_MEM2(LCC_TABLE, CHAR2), 0); JUMPHERE(jump); #endif jump = CMP(SLJIT_NOT_EQUAL, CHAR1, 0, CHAR2, 0); -OP2(SLJIT_SUB | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_IMM, IN_UCHARS(1)); +OP2(SLJIT_SUB | SLJIT_SET_Z, TMP2, 0, TMP2, 0, SLJIT_IMM, IN_UCHARS(1)); JUMPTO(SLJIT_NOT_ZERO, label); JUMPHERE(jump); @@ -5394,7 +5398,7 @@ do #endif default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); break; } context->ucharptr = 0; @@ -5568,7 +5572,7 @@ while (*cc != XCL_END) break; default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); break; } cc += 2; @@ -5592,7 +5596,7 @@ if ((cc[-1] & XCL_HASPROP) == 0) OP2(SLJIT_LSHR, TMP1, 0, TMP1, 0, SLJIT_IMM, 3); OP1(SLJIT_MOV_U8, TMP1, 0, SLJIT_MEM1(TMP1), (sljit_sw)cc); OP2(SLJIT_SHL, TMP2, 0, SLJIT_IMM, 1, TMP2, 0); - OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, TMP2, 0); + OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, TMP2, 0); add_jump(compiler, &found, JUMP(SLJIT_NOT_ZERO)); } @@ -5625,7 +5629,7 @@ else if ((cc[-1] & XCL_MAP) != 0) OP2(SLJIT_LSHR, TMP1, 0, TMP1, 0, SLJIT_IMM, 3); OP1(SLJIT_MOV_U8, TMP1, 0, SLJIT_MEM1(TMP1), (sljit_sw)cc); OP2(SLJIT_SHL, TMP2, 0, SLJIT_IMM, 1, TMP2, 0); - OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, TMP2, 0); + OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, TMP2, 0); add_jump(compiler, list, JUMP(SLJIT_NOT_ZERO)); #ifdef COMPILE_PCRE8 @@ -5644,6 +5648,15 @@ if (needstype || needsscript) if (needschar && !charsaved) OP1(SLJIT_MOV, RETURN_ADDR, 0, TMP1, 0); +#ifdef COMPILE_PCRE32 + if (!common->utf) + { + jump = CMP(SLJIT_LESS, TMP1, 0, SLJIT_IMM, 0x10ffff + 1); + OP1(SLJIT_MOV, TMP1, 0, SLJIT_IMM, INVALID_UTF_CHAR); + JUMPHERE(jump); + } +#endif + OP2(SLJIT_LSHR, TMP2, 0, TMP1, 0, SLJIT_IMM, UCD_BLOCK_SHIFT); OP1(SLJIT_MOV_U8, TMP2, 0, SLJIT_MEM1(TMP2), (sljit_sw)PRIV(ucd_stage1)); OP2(SLJIT_AND, TMP1, 0, TMP1, 0, SLJIT_IMM, UCD_BLOCK_MASK); @@ -5735,14 +5748,14 @@ while (*cc != XCL_END) if (numberofcmps < 3 && (*cc == XCL_SINGLE || *cc == XCL_RANGE)) { - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(c - charoffset)); - OP_FLAGS(numberofcmps == 0 ? SLJIT_MOV : SLJIT_OR, TMP2, 0, numberofcmps == 0 ? SLJIT_UNUSED : TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(c - charoffset)); + OP_FLAGS(numberofcmps == 0 ? SLJIT_MOV : SLJIT_OR, TMP2, 0, SLJIT_EQUAL); numberofcmps++; } else if (numberofcmps > 0) { - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(c - charoffset)); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(c - charoffset)); + OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_EQUAL); jump = JUMP(SLJIT_NOT_ZERO ^ invertcmp); numberofcmps = 0; } @@ -5761,14 +5774,14 @@ while (*cc != XCL_END) if (numberofcmps < 3 && (*cc == XCL_SINGLE || *cc == XCL_RANGE)) { - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(c - charoffset)); - OP_FLAGS(numberofcmps == 0 ? SLJIT_MOV : SLJIT_OR, TMP2, 0, numberofcmps == 0 ? SLJIT_UNUSED : TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(c - charoffset)); + OP_FLAGS(numberofcmps == 0 ? SLJIT_MOV : SLJIT_OR, TMP2, 0, SLJIT_LESS_EQUAL); numberofcmps++; } else if (numberofcmps > 0) { - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(c - charoffset)); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(c - charoffset)); + OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_LESS_EQUAL); jump = JUMP(SLJIT_NOT_ZERO ^ invertcmp); numberofcmps = 0; } @@ -5793,12 +5806,12 @@ while (*cc != XCL_END) break; case PT_LAMP: - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Lu - typeoffset); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Ll - typeoffset); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Lt - typeoffset); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Lu - typeoffset); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Ll - typeoffset); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Lt - typeoffset); + OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_EQUAL); jump = JUMP(SLJIT_NOT_ZERO ^ invertcmp); break; @@ -5820,33 +5833,33 @@ while (*cc != XCL_END) case PT_SPACE: case PT_PXSPACE: SET_CHAR_OFFSET(9); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xd - 0x9); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xd - 0x9); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_LESS_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x85 - 0x9); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x85 - 0x9); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x180e - 0x9); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x180e - 0x9); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); SET_TYPE_OFFSET(ucp_Zl); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Zs - ucp_Zl); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Zs - ucp_Zl); + OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_LESS_EQUAL); jump = JUMP(SLJIT_NOT_ZERO ^ invertcmp); break; case PT_WORD: - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(CHAR_UNDERSCORE - charoffset)); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(CHAR_UNDERSCORE - charoffset)); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_EQUAL); /* Fall through. */ case PT_ALNUM: SET_TYPE_OFFSET(ucp_Ll); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Lu - ucp_Ll); - OP_FLAGS((*cc == PT_ALNUM) ? SLJIT_MOV : SLJIT_OR, TMP2, 0, (*cc == PT_ALNUM) ? SLJIT_UNUSED : TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Lu - ucp_Ll); + OP_FLAGS((*cc == PT_ALNUM) ? SLJIT_MOV : SLJIT_OR, TMP2, 0, SLJIT_LESS_EQUAL); SET_TYPE_OFFSET(ucp_Nd); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_No - ucp_Nd); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_No - ucp_Nd); + OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_LESS_EQUAL); jump = JUMP(SLJIT_NOT_ZERO ^ invertcmp); break; @@ -5868,8 +5881,8 @@ while (*cc != XCL_END) OP2(SLJIT_ADD, TMP2, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)charoffset); OP2(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_IMM, other_cases[1] ^ other_cases[0]); } - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP2, 0, SLJIT_IMM, other_cases[1]); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP2, 0, SLJIT_IMM, other_cases[1]); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_EQUAL); other_cases += 2; } else if (is_powerof2(other_cases[2] ^ other_cases[1])) @@ -5881,63 +5894,63 @@ while (*cc != XCL_END) OP2(SLJIT_ADD, TMP2, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)charoffset); OP2(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_IMM, other_cases[1] ^ other_cases[0]); } - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP2, 0, SLJIT_IMM, other_cases[2]); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP2, 0, SLJIT_IMM, other_cases[2]); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(other_cases[0] - charoffset)); - OP_FLAGS(SLJIT_OR | ((other_cases[3] == NOTACHAR) ? SLJIT_SET_E : 0), TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(other_cases[0] - charoffset)); + OP_FLAGS(SLJIT_OR | ((other_cases[3] == NOTACHAR) ? SLJIT_SET_Z : 0), TMP2, 0, SLJIT_EQUAL); other_cases += 3; } else { - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(*other_cases++ - charoffset)); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(*other_cases++ - charoffset)); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_EQUAL); } while (*other_cases != NOTACHAR) { - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(*other_cases++ - charoffset)); - OP_FLAGS(SLJIT_OR | ((*other_cases == NOTACHAR) ? SLJIT_SET_E : 0), TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(*other_cases++ - charoffset)); + OP_FLAGS(SLJIT_OR | ((*other_cases == NOTACHAR) ? SLJIT_SET_Z : 0), TMP2, 0, SLJIT_EQUAL); } jump = JUMP(SLJIT_NOT_ZERO ^ invertcmp); break; case PT_UCNC: - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(CHAR_DOLLAR_SIGN - charoffset)); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(CHAR_COMMERCIAL_AT - charoffset)); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(CHAR_GRAVE_ACCENT - charoffset)); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(CHAR_DOLLAR_SIGN - charoffset)); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(CHAR_COMMERCIAL_AT - charoffset)); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(CHAR_GRAVE_ACCENT - charoffset)); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); SET_CHAR_OFFSET(0xa0); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(0xd7ff - charoffset)); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (sljit_sw)(0xd7ff - charoffset)); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_LESS_EQUAL); SET_CHAR_OFFSET(0); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xe000 - 0); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_GREATER_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_GREATER_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xe000 - 0); + OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_GREATER_EQUAL); jump = JUMP(SLJIT_NOT_ZERO ^ invertcmp); break; case PT_PXGRAPH: /* C and Z groups are the farthest two groups. */ SET_TYPE_OFFSET(ucp_Ll); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_So - ucp_Ll); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_GREATER); + OP2(SLJIT_SUB | SLJIT_SET_GREATER, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_So - ucp_Ll); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_GREATER); jump = CMP(SLJIT_NOT_EQUAL, typereg, 0, SLJIT_IMM, ucp_Cf - ucp_Ll); /* In case of ucp_Cf, we overwrite the result. */ SET_CHAR_OFFSET(0x2066); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x2069 - 0x2066); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x2069 - 0x2066); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_LESS_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x061c - 0x2066); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x061c - 0x2066); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x180e - 0x2066); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x180e - 0x2066); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); JUMPHERE(jump); jump = CMP(SLJIT_ZERO ^ invertcmp, TMP2, 0, SLJIT_IMM, 0); @@ -5946,21 +5959,21 @@ while (*cc != XCL_END) case PT_PXPRINT: /* C and Z groups are the farthest two groups. */ SET_TYPE_OFFSET(ucp_Ll); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_So - ucp_Ll); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_GREATER); + OP2(SLJIT_SUB | SLJIT_SET_GREATER, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_So - ucp_Ll); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_GREATER); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Zs - ucp_Ll); - OP_FLAGS(SLJIT_AND, TMP2, 0, TMP2, 0, SLJIT_NOT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Zs - ucp_Ll); + OP_FLAGS(SLJIT_AND, TMP2, 0, SLJIT_NOT_EQUAL); jump = CMP(SLJIT_NOT_EQUAL, typereg, 0, SLJIT_IMM, ucp_Cf - ucp_Ll); /* In case of ucp_Cf, we overwrite the result. */ SET_CHAR_OFFSET(0x2066); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x2069 - 0x2066); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x2069 - 0x2066); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_LESS_EQUAL); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x061c - 0x2066); - OP_FLAGS(SLJIT_OR, TMP2, 0, TMP2, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x061c - 0x2066); + OP_FLAGS(SLJIT_OR, TMP2, 0, SLJIT_EQUAL); JUMPHERE(jump); jump = CMP(SLJIT_ZERO ^ invertcmp, TMP2, 0, SLJIT_IMM, 0); @@ -5968,21 +5981,21 @@ while (*cc != XCL_END) case PT_PXPUNCT: SET_TYPE_OFFSET(ucp_Sc); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_So - ucp_Sc); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_So - ucp_Sc); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_LESS_EQUAL); SET_CHAR_OFFSET(0); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x7f); - OP_FLAGS(SLJIT_AND, TMP2, 0, TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0x7f); + OP_FLAGS(SLJIT_AND, TMP2, 0, SLJIT_LESS_EQUAL); SET_TYPE_OFFSET(ucp_Pc); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Ps - ucp_Pc); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_LESS_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS_EQUAL, SLJIT_UNUSED, 0, typereg, 0, SLJIT_IMM, ucp_Ps - ucp_Pc); + OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_LESS_EQUAL); jump = JUMP(SLJIT_NOT_ZERO ^ invertcmp); break; default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); break; } cc += 2; @@ -6028,6 +6041,7 @@ switch(type) case OP_NOT_WORD_BOUNDARY: case OP_WORD_BOUNDARY: add_jump(compiler, &common->wordboundary, JUMP(SLJIT_FAST_CALL)); + sljit_set_current_flags(compiler, SLJIT_SET_Z); add_jump(compiler, backtracks, JUMP(type == OP_NOT_WORD_BOUNDARY ? SLJIT_NOT_ZERO : SLJIT_ZERO)); return cc; @@ -6043,10 +6057,10 @@ switch(type) else { jump[1] = CMP(SLJIT_EQUAL, TMP2, 0, STR_END, 0); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP2, 0, STR_END, 0); - OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_UNUSED, 0, SLJIT_LESS); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (common->newline >> 8) & 0xff); - OP_FLAGS(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_NOT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_LESS, SLJIT_UNUSED, 0, TMP2, 0, STR_END, 0); + OP_FLAGS(SLJIT_MOV, TMP2, 0, SLJIT_LESS); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, (common->newline >> 8) & 0xff); + OP_FLAGS(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, SLJIT_NOT_EQUAL); add_jump(compiler, backtracks, JUMP(SLJIT_NOT_EQUAL)); check_partial(common, TRUE); add_jump(compiler, backtracks, JUMP(SLJIT_JUMP)); @@ -6068,9 +6082,9 @@ switch(type) OP1(MOV_UCHAR, TMP1, 0, SLJIT_MEM1(STR_PTR), IN_UCHARS(0)); jump[1] = CMP(SLJIT_NOT_EQUAL, TMP1, 0, SLJIT_IMM, CHAR_CR); OP2(SLJIT_ADD, TMP2, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(2)); - OP2(SLJIT_SUB | SLJIT_SET_U, SLJIT_UNUSED, 0, TMP2, 0, STR_END, 0); + OP2(SLJIT_SUB | SLJIT_SET_Z | SLJIT_SET_GREATER, SLJIT_UNUSED, 0, TMP2, 0, STR_END, 0); jump[2] = JUMP(SLJIT_GREATER); - add_jump(compiler, backtracks, JUMP(SLJIT_LESS)); + add_jump(compiler, backtracks, JUMP(SLJIT_NOT_EQUAL) /* LESS */); /* Equal. */ OP1(MOV_UCHAR, TMP1, 0, SLJIT_MEM1(STR_PTR), IN_UCHARS(1)); jump[3] = CMP(SLJIT_EQUAL, TMP1, 0, SLJIT_IMM, CHAR_NL); @@ -6089,6 +6103,7 @@ switch(type) read_char_range(common, common->nlmin, common->nlmax, TRUE); add_jump(compiler, backtracks, CMP(SLJIT_NOT_EQUAL, STR_PTR, 0, STR_END, 0)); add_jump(compiler, &common->anynewline, JUMP(SLJIT_FAST_CALL)); + sljit_set_current_flags(compiler, SLJIT_SET_Z); add_jump(compiler, backtracks, JUMP(SLJIT_ZERO)); OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(SLJIT_SP), LOCALS1); } @@ -6204,7 +6219,7 @@ switch(type) label = LABEL(); add_jump(compiler, backtracks, CMP(SLJIT_LESS_EQUAL, STR_PTR, 0, TMP3, 0)); skip_char_back(common); - OP2(SLJIT_SUB | SLJIT_SET_E, TMP2, 0, TMP2, 0, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, TMP2, 0, TMP2, 0, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, label); } else @@ -6217,7 +6232,7 @@ switch(type) check_start_used_ptr(common); return cc + LINK_SIZE; } -SLJIT_ASSERT_STOP(); +SLJIT_UNREACHABLE(); return cc; } @@ -6250,7 +6265,7 @@ switch(type) #endif read_char8_type(common, type == OP_NOT_DIGIT); /* Flip the starting bit in the negative case. */ - OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ctype_digit); + OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ctype_digit); add_jump(compiler, backtracks, JUMP(type == OP_DIGIT ? SLJIT_ZERO : SLJIT_NOT_ZERO)); return cc; @@ -6264,7 +6279,7 @@ switch(type) else #endif read_char8_type(common, type == OP_NOT_WHITESPACE); - OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ctype_space); + OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ctype_space); add_jump(compiler, backtracks, JUMP(type == OP_WHITESPACE ? SLJIT_ZERO : SLJIT_NOT_ZERO)); return cc; @@ -6278,7 +6293,7 @@ switch(type) else #endif read_char8_type(common, type == OP_NOT_WORDCHAR); - OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ctype_word); + OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, ctype_word); add_jump(compiler, backtracks, JUMP(type == OP_WORDCHAR ? SLJIT_ZERO : SLJIT_NOT_ZERO)); return cc; @@ -6320,8 +6335,8 @@ switch(type) #elif defined COMPILE_PCRE16 jump[0] = CMP(SLJIT_LESS, TMP1, 0, SLJIT_IMM, 0xd800); OP2(SLJIT_AND, TMP1, 0, TMP1, 0, SLJIT_IMM, 0xfc00); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xd800); - OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_UNUSED, 0, SLJIT_EQUAL); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, SLJIT_IMM, 0xd800); + OP_FLAGS(SLJIT_MOV, TMP1, 0, SLJIT_EQUAL); OP2(SLJIT_SHL, TMP1, 0, TMP1, 0, SLJIT_IMM, 1); OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, TMP1, 0); #endif @@ -6383,6 +6398,7 @@ switch(type) detect_partial_match(common, backtracks); read_char_range(common, 0x9, 0x3000, type == OP_NOT_HSPACE); add_jump(compiler, &common->hspace, JUMP(SLJIT_FAST_CALL)); + sljit_set_current_flags(compiler, SLJIT_SET_Z); add_jump(compiler, backtracks, JUMP(type == OP_NOT_HSPACE ? SLJIT_NOT_ZERO : SLJIT_ZERO)); return cc; @@ -6392,6 +6408,7 @@ switch(type) detect_partial_match(common, backtracks); read_char_range(common, 0xa, 0x2029, type == OP_NOT_VSPACE); add_jump(compiler, &common->vspace, JUMP(SLJIT_FAST_CALL)); + sljit_set_current_flags(compiler, SLJIT_SET_Z); add_jump(compiler, backtracks, JUMP(type == OP_NOT_VSPACE ? SLJIT_NOT_ZERO : SLJIT_ZERO)); return cc; @@ -6418,7 +6435,7 @@ switch(type) OP1(SLJIT_MOV_U32, TMP1, 0, SLJIT_MEM1(STACK_TOP), (sljit_sw)PRIV(ucp_gbtable)); OP1(SLJIT_MOV, STACK_TOP, 0, TMP2, 0); OP2(SLJIT_SHL, TMP2, 0, SLJIT_IMM, 1, TMP2, 0); - OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, TMP2, 0); + OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, TMP2, 0); JUMPTO(SLJIT_NOT_ZERO, label); OP1(SLJIT_MOV, STR_PTR, 0, TMP3, 0); @@ -6587,7 +6604,7 @@ switch(type) OP2(SLJIT_LSHR, TMP1, 0, TMP1, 0, SLJIT_IMM, 3); OP1(SLJIT_MOV_U8, TMP1, 0, SLJIT_MEM1(TMP1), (sljit_sw)cc); OP2(SLJIT_SHL, TMP2, 0, SLJIT_IMM, 1, TMP2, 0); - OP2(SLJIT_AND | SLJIT_SET_E, SLJIT_UNUSED, 0, TMP1, 0, TMP2, 0); + OP2(SLJIT_AND | SLJIT_SET_Z, SLJIT_UNUSED, 0, TMP1, 0, TMP2, 0); add_jump(compiler, backtracks, JUMP(SLJIT_ZERO)); #if defined SUPPORT_UTF || !defined COMPILE_PCRE8 @@ -6604,7 +6621,7 @@ switch(type) return cc + GET(cc, 0) - 1; #endif } -SLJIT_ASSERT_STOP(); +SLJIT_UNREACHABLE(); return cc; } @@ -6790,9 +6807,9 @@ else #endif /* SUPPORT_UTF && SUPPORT_UCP */ { if (ref) - OP2(SLJIT_SUB | SLJIT_SET_E, TMP2, 0, SLJIT_MEM1(SLJIT_SP), OVECTOR(offset + 1), TMP1, 0); + OP2(SLJIT_SUB | SLJIT_SET_Z, TMP2, 0, SLJIT_MEM1(SLJIT_SP), OVECTOR(offset + 1), TMP1, 0); else - OP2(SLJIT_SUB | SLJIT_SET_E, TMP2, 0, SLJIT_MEM1(TMP2), sizeof(sljit_sw), TMP1, 0); + OP2(SLJIT_SUB | SLJIT_SET_Z, TMP2, 0, SLJIT_MEM1(TMP2), sizeof(sljit_sw), TMP1, 0); if (withchecks) jump = JUMP(SLJIT_ZERO); @@ -6883,7 +6900,7 @@ switch(type) cc += 1 + IMM2_SIZE + 1 + 2 * IMM2_SIZE; break; default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); break; } @@ -6897,7 +6914,7 @@ if (!minimize) OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(0), STR_PTR, 0); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(1), SLJIT_IMM, 0); /* Temporary release of STR_PTR. */ - OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); + OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); /* Handles both invalid and empty cases. Since the minimum repeat, is zero the invalid case is basically the same as an empty case. */ if (ref) @@ -6910,7 +6927,7 @@ if (!minimize) zerolength = CMP(SLJIT_EQUAL, TMP1, 0, SLJIT_MEM1(TMP2), sizeof(sljit_sw)); } /* Restore if not zero length. */ - OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); } else { @@ -7157,7 +7174,7 @@ return (*PUBL(callout))(callout_block); (((int)sizeof(PUBL(callout_block)) + 7) & ~7) #define CALLOUT_ARG_OFFSET(arg) \ - (-CALLOUT_ARG_SIZE + SLJIT_OFFSETOF(PUBL(callout_block), arg)) + SLJIT_OFFSETOF(PUBL(callout_block), arg) static SLJIT_INLINE pcre_uchar *compile_callout_matchingpath(compiler_common *common, pcre_uchar *cc, backtrack_common *parent) { @@ -7187,7 +7204,8 @@ OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), CALLOUT_ARG_OFFSET(mark), (common->mark_pt /* Needed to save important temporary registers. */ OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), LOCALS0, STACK_TOP, 0); -OP2(SLJIT_SUB, SLJIT_R1, 0, STACK_TOP, 0, SLJIT_IMM, CALLOUT_ARG_SIZE); +/* SLJIT_R0 = arguments */ +OP1(SLJIT_MOV, SLJIT_R1, 0, STACK_TOP, 0); GET_LOCAL_BASE(SLJIT_R2, 0, OVECTOR_START); sljit_emit_ijump(compiler, SLJIT_CALL3, SLJIT_IMM, SLJIT_FUNC_OFFSET(do_callout)); OP1(SLJIT_MOV_S32, SLJIT_RETURN_REG, 0, SLJIT_RETURN_REG, 0); @@ -7195,12 +7213,12 @@ OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), LOCALS0); free_stack(common, CALLOUT_ARG_SIZE / sizeof(sljit_sw)); /* Check return value. */ -OP2(SLJIT_SUB | SLJIT_SET_S, SLJIT_UNUSED, 0, SLJIT_RETURN_REG, 0, SLJIT_IMM, 0); +OP2(SLJIT_SUB | SLJIT_SET_Z | SLJIT_SET_SIG_GREATER, SLJIT_UNUSED, 0, SLJIT_RETURN_REG, 0, SLJIT_IMM, 0); add_jump(compiler, &backtrack->topbacktracks, JUMP(SLJIT_SIG_GREATER)); if (common->forced_quit_label == NULL) - add_jump(compiler, &common->forced_quit, JUMP(SLJIT_SIG_LESS)); + add_jump(compiler, &common->forced_quit, JUMP(SLJIT_NOT_EQUAL) /* SIG_LESS */); else - JUMPTO(SLJIT_SIG_LESS, common->forced_quit_label); + JUMPTO(SLJIT_NOT_EQUAL /* SIG_LESS */, common->forced_quit_label); return cc + 2 + 2 * LINK_SIZE; } @@ -7321,7 +7339,7 @@ else allocate_stack(common, framesize + extrasize); OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); - OP2(SLJIT_SUB, TMP2, 0, STACK_TOP, 0, SLJIT_IMM, (framesize + extrasize) * sizeof(sljit_sw)); + OP2(SLJIT_ADD, TMP2, 0, STACK_TOP, 0, SLJIT_IMM, (framesize + extrasize) * sizeof(sljit_sw)); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), private_data_ptr, TMP2, 0); if (needs_control_head) OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr); @@ -7392,22 +7410,22 @@ while (1) free_stack(common, extrasize); if (needs_control_head) - OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, SLJIT_MEM1(STACK_TOP), 0); + OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, SLJIT_MEM1(STACK_TOP), STACK(-1)); } else { if ((opcode != OP_ASSERT_NOT && opcode != OP_ASSERTBACK_NOT) || conditional) { /* We don't need to keep the STR_PTR, only the previous private_data_ptr. */ - OP2(SLJIT_ADD, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, (framesize + 1) * sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, (framesize + 1) * sizeof(sljit_sw)); if (needs_control_head) - OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, SLJIT_MEM1(STACK_TOP), 0); + OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, SLJIT_MEM1(STACK_TOP), STACK(-1)); } else { OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); if (needs_control_head) - OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, SLJIT_MEM1(STACK_TOP), (framesize + 1) * sizeof(sljit_sw)); + OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, SLJIT_MEM1(STACK_TOP), STACK(-framesize - 2)); add_jump(compiler, &common->revertframes, JUMP(SLJIT_FAST_CALL)); } } @@ -7418,25 +7436,25 @@ while (1) if (conditional) { if (extrasize > 0) - OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), needs_control_head ? sizeof(sljit_sw) : 0); + OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), needs_control_head ? STACK(-2) : STACK(-1)); } else if (bra == OP_BRAZERO) { if (framesize < 0) - OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), (extrasize - 1) * sizeof(sljit_sw)); + OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), STACK(-extrasize)); else { - OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STACK_TOP), framesize * sizeof(sljit_sw)); - OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), (framesize + extrasize - 1) * sizeof(sljit_sw)); + OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STACK_TOP), STACK(-framesize - 1)); + OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), STACK(-framesize - extrasize)); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), private_data_ptr, TMP1, 0); } - OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(0), SLJIT_IMM, 0); } else if (framesize >= 0) { /* For OP_BRA and OP_BRAMINZERO. */ - OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_MEM1(STACK_TOP), framesize * sizeof(sljit_sw)); + OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_MEM1(STACK_TOP), STACK(-framesize - 1)); } } add_jump(compiler, found, JUMP(SLJIT_JUMP)); @@ -7480,12 +7498,12 @@ if (common->positive_assert_quit != NULL) set_jumps(common->positive_assert_quit, LABEL()); SLJIT_ASSERT(framesize != no_stack); if (framesize < 0) - OP2(SLJIT_ADD, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, extrasize * sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, extrasize * sizeof(sljit_sw)); else { OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); add_jump(compiler, &common->revertframes, JUMP(SLJIT_FAST_CALL)); - OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, (framesize + extrasize) * sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, (framesize + extrasize) * sizeof(sljit_sw)); } JUMPHERE(jump); } @@ -7534,18 +7552,18 @@ if (opcode == OP_ASSERT || opcode == OP_ASSERTBACK) { /* We know that STR_PTR was stored on the top of the stack. */ if (extrasize > 0) - OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), (extrasize - 1) * sizeof(sljit_sw)); + OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), STACK(-extrasize)); /* Keep the STR_PTR on the top of the stack. */ if (bra == OP_BRAZERO) { - OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); if (extrasize == 2) OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(0), STR_PTR, 0); } else if (bra == OP_BRAMINZERO) { - OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(0), SLJIT_IMM, 0); } } @@ -7554,13 +7572,13 @@ if (opcode == OP_ASSERT || opcode == OP_ASSERTBACK) if (bra == OP_BRA) { /* We don't need to keep the STR_PTR, only the previous private_data_ptr. */ - OP2(SLJIT_ADD, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, (framesize + 1) * sizeof(sljit_sw)); - OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), (extrasize - 2) * sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, (framesize + 1) * sizeof(sljit_sw)); + OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), STACK(-extrasize + 1)); } else { /* We don't need to keep the STR_PTR, only the previous private_data_ptr. */ - OP2(SLJIT_ADD, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, (framesize + 2) * sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, (framesize + 2) * sizeof(sljit_sw)); if (extrasize == 2) { OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(STACK_TOP), STACK(0)); @@ -7588,7 +7606,7 @@ if (opcode == OP_ASSERT || opcode == OP_ASSERTBACK) { OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); add_jump(compiler, &common->revertframes, JUMP(SLJIT_FAST_CALL)); - OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_MEM1(STACK_TOP), framesize * sizeof(sljit_sw)); + OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_MEM1(STACK_TOP), STACK(-framesize - 1)); } set_jumps(backtrack->common.topbacktracks, LABEL()); } @@ -7675,23 +7693,23 @@ if (framesize < 0) } if (needs_control_head) - OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STACK_TOP), (ket != OP_KET || has_alternatives) ? sizeof(sljit_sw) : 0); + OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STACK_TOP), (ket != OP_KET || has_alternatives) ? STACK(-2) : STACK(-1)); /* TMP2 which is set here used by OP_KETRMAX below. */ if (ket == OP_KETRMAX) - OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(STACK_TOP), 0); + OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(STACK_TOP), STACK(-1)); else if (ket == OP_KETRMIN) { /* Move the STR_PTR to the private_data_ptr. */ - OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_MEM1(STACK_TOP), 0); + OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_MEM1(STACK_TOP), STACK(-1)); } } else { stacksize = (ket != OP_KET || has_alternatives) ? 2 : 1; - OP2(SLJIT_ADD, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, (framesize + stacksize) * sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, (framesize + stacksize) * sizeof(sljit_sw)); if (needs_control_head) - OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STACK_TOP), 0); + OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STACK_TOP), STACK(-1)); if (ket == OP_KETRMAX) { @@ -7927,7 +7945,7 @@ if (bra == OP_BRAMINZERO) { /* Except when the whole stack frame must be saved. */ OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); - braminzero = CMP(SLJIT_EQUAL, STR_PTR, 0, SLJIT_MEM1(TMP1), (BACKTRACK_AS(bracket_backtrack)->u.framesize + 1) * sizeof(sljit_sw)); + braminzero = CMP(SLJIT_EQUAL, STR_PTR, 0, SLJIT_MEM1(TMP1), STACK(-BACKTRACK_AS(bracket_backtrack)->u.framesize - 2)); } JUMPHERE(skip); } @@ -8000,7 +8018,7 @@ if (opcode == OP_ONCE) OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(stacksize), STR_PTR, 0); if (BACKTRACK_AS(bracket_backtrack)->u.framesize == no_frame) - OP2(SLJIT_SUB, SLJIT_MEM1(SLJIT_SP), private_data_ptr, STACK_TOP, 0, SLJIT_IMM, needs_control_head ? (2 * sizeof(sljit_sw)) : sizeof(sljit_sw)); + OP2(SLJIT_ADD, SLJIT_MEM1(SLJIT_SP), private_data_ptr, STACK_TOP, 0, SLJIT_IMM, needs_control_head ? (2 * sizeof(sljit_sw)) : sizeof(sljit_sw)); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(stacksize + 1), TMP2, 0); } else if (ket == OP_KETRMAX || has_alternatives) @@ -8018,7 +8036,7 @@ if (opcode == OP_ONCE) OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(0), TMP2, 0); OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); - OP2(SLJIT_SUB, TMP2, 0, STACK_TOP, 0, SLJIT_IMM, stacksize * sizeof(sljit_sw)); + OP2(SLJIT_ADD, TMP2, 0, STACK_TOP, 0, SLJIT_IMM, stacksize * sizeof(sljit_sw)); stacksize = needs_control_head ? 1 : 0; if (ket != OP_KET || has_alternatives) @@ -8090,13 +8108,13 @@ if (opcode == OP_COND || opcode == OP_SCOND) slot = common->name_table + GET2(matchingpath, 1) * common->name_entry_size; OP1(SLJIT_MOV, TMP3, 0, STR_PTR, 0); OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), OVECTOR(1)); - OP2(SLJIT_SUB | SLJIT_SET_E, TMP2, 0, SLJIT_MEM1(SLJIT_SP), OVECTOR(GET2(slot, 0) << 1), TMP1, 0); + OP2(SLJIT_SUB | SLJIT_SET_Z, TMP2, 0, SLJIT_MEM1(SLJIT_SP), OVECTOR(GET2(slot, 0) << 1), TMP1, 0); slot += common->name_entry_size; i--; while (i-- > 0) { OP2(SLJIT_SUB, STR_PTR, 0, SLJIT_MEM1(SLJIT_SP), OVECTOR(GET2(slot, 0) << 1), TMP1, 0); - OP2(SLJIT_OR | SLJIT_SET_E, TMP2, 0, TMP2, 0, STR_PTR, 0); + OP2(SLJIT_OR | SLJIT_SET_Z, TMP2, 0, TMP2, 0, STR_PTR, 0); slot += common->name_entry_size; } OP1(SLJIT_MOV, STR_PTR, 0, TMP3, 0); @@ -8111,7 +8129,7 @@ if (opcode == OP_COND || opcode == OP_SCOND) if (*matchingpath == OP_FAIL) stacksize = 0; - if (*matchingpath == OP_RREF) + else if (*matchingpath == OP_RREF) { stacksize = GET2(matchingpath, 1); if (common->currententry == NULL) @@ -8244,7 +8262,7 @@ if (ket == OP_KETRMAX) { if (has_alternatives) BACKTRACK_AS(bracket_backtrack)->alternative_matchingpath = LABEL(); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_MEM1(SLJIT_SP), repeat_ptr, SLJIT_MEM1(SLJIT_SP), repeat_ptr, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_MEM1(SLJIT_SP), repeat_ptr, SLJIT_MEM1(SLJIT_SP), repeat_ptr, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, rmax_label); /* Drop STR_PTR for greedy plus quantifier. */ if (opcode != OP_ONCE) @@ -8274,7 +8292,7 @@ if (ket == OP_KETRMAX) if (repeat_type == OP_EXACT) { count_match(common); - OP2(SLJIT_SUB | SLJIT_SET_E, SLJIT_MEM1(SLJIT_SP), repeat_ptr, SLJIT_MEM1(SLJIT_SP), repeat_ptr, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, SLJIT_MEM1(SLJIT_SP), repeat_ptr, SLJIT_MEM1(SLJIT_SP), repeat_ptr, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, rmax_label); } else if (repeat_type == OP_UPTO) @@ -8374,7 +8392,7 @@ switch(opcode) break; default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); break; } @@ -8452,7 +8470,7 @@ else OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); if (needs_control_head) OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr); - OP2(SLJIT_SUB, SLJIT_MEM1(SLJIT_SP), private_data_ptr, STACK_TOP, 0, SLJIT_IMM, -STACK(stacksize - 1)); + OP2(SLJIT_ADD, SLJIT_MEM1(SLJIT_SP), private_data_ptr, STACK_TOP, 0, SLJIT_IMM, stacksize * sizeof(sljit_sw)); stack = 0; if (!zero) @@ -8524,7 +8542,7 @@ while (*cc != OP_KETRPOS) { if (offset != 0) { - OP2(SLJIT_ADD, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, stacksize * sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_IMM, stacksize * sizeof(sljit_sw)); OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), cbraprivptr); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), OVECTOR(offset + 1), STR_PTR, 0); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), cbraprivptr, STR_PTR, 0); @@ -8535,10 +8553,10 @@ while (*cc != OP_KETRPOS) else { OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); - OP2(SLJIT_ADD, STACK_TOP, 0, TMP2, 0, SLJIT_IMM, stacksize * sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, TMP2, 0, SLJIT_IMM, stacksize * sizeof(sljit_sw)); if (opcode == OP_SBRAPOS) - OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(TMP2), (framesize + 1) * sizeof(sljit_sw)); - OP1(SLJIT_MOV, SLJIT_MEM1(TMP2), (framesize + 1) * sizeof(sljit_sw), STR_PTR, 0); + OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(TMP2), STACK(-framesize - 2)); + OP1(SLJIT_MOV, SLJIT_MEM1(TMP2), STACK(-framesize - 2), STR_PTR, 0); } /* Even if the match is empty, we need to reset the control head. */ @@ -8584,7 +8602,7 @@ while (*cc != OP_KETRPOS) else { OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); - OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(TMP2), (framesize + 1) * sizeof(sljit_sw)); + OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(TMP2), STACK(-framesize - 2)); } } @@ -8601,7 +8619,7 @@ if (!zero) if (framesize < 0) add_jump(compiler, &backtrack->topbacktracks, CMP(SLJIT_NOT_EQUAL, SLJIT_MEM1(STACK_TOP), STACK(stacksize - 1), SLJIT_IMM, 0)); else /* TMP2 is set to [private_data_ptr] above. */ - add_jump(compiler, &backtrack->topbacktracks, CMP(SLJIT_NOT_EQUAL, SLJIT_MEM1(TMP2), (stacksize - 1) * sizeof(sljit_sw), SLJIT_IMM, 0)); + add_jump(compiler, &backtrack->topbacktracks, CMP(SLJIT_NOT_EQUAL, SLJIT_MEM1(TMP2), STACK(-stacksize), SLJIT_IMM, 0)); } /* None of them matched. */ @@ -8824,7 +8842,7 @@ if (exact > 1) OP1(SLJIT_MOV, tmp_base, tmp_offset, SLJIT_IMM, exact); label = LABEL(); compile_char1_matchingpath(common, type, cc, &backtrack->topbacktracks, FALSE); - OP2(SLJIT_SUB | SLJIT_SET_E, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, label); } else @@ -8832,7 +8850,7 @@ if (exact > 1) OP1(SLJIT_MOV, tmp_base, tmp_offset, SLJIT_IMM, exact); label = LABEL(); compile_char1_matchingpath(common, type, cc, &backtrack->topbacktracks, TRUE); - OP2(SLJIT_SUB | SLJIT_SET_E, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, label); } } @@ -8862,7 +8880,7 @@ switch(opcode) if (opcode == OP_UPTO) { OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), POSSESSIVE0); - OP2(SLJIT_SUB | SLJIT_SET_E, TMP1, 0, TMP1, 0, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, TMP1, 0, TMP1, 0, SLJIT_IMM, 1); jump = JUMP(SLJIT_ZERO); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), POSSESSIVE0, TMP1, 0); } @@ -8924,7 +8942,7 @@ switch(opcode) label = LABEL(); if (opcode == OP_UPTO) { - OP2(SLJIT_SUB | SLJIT_SET_E, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); add_jump(compiler, &backtrack->topbacktracks, JUMP(SLJIT_ZERO)); } compile_char1_matchingpath(common, type, cc, &backtrack->topbacktracks, FALSE); @@ -8944,7 +8962,7 @@ switch(opcode) OP1(SLJIT_MOV, base, offset1, STR_PTR, 0); if (opcode == OP_UPTO) { - OP2(SLJIT_SUB | SLJIT_SET_E, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); add_jump(compiler, &no_match, JUMP(SLJIT_ZERO)); } @@ -8971,7 +8989,7 @@ switch(opcode) if (opcode == OP_UPTO) { - OP2(SLJIT_SUB | SLJIT_SET_E, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, label); } else @@ -9000,7 +9018,7 @@ switch(opcode) if (opcode == OP_UPTO) { - OP2(SLJIT_SUB | SLJIT_SET_E, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, label); } else @@ -9026,7 +9044,7 @@ switch(opcode) compile_char1_matchingpath(common, type, cc, &no_char1_match, FALSE); if (opcode == OP_UPTO) { - OP2(SLJIT_SUB | SLJIT_SET_E, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, label); OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(1)); } @@ -9113,7 +9131,7 @@ switch(opcode) label = LABEL(); compile_char1_matchingpath(common, type, cc, &no_match, TRUE); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), POSSESSIVE1, STR_PTR, 0); - OP2(SLJIT_SUB | SLJIT_SET_E, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, label); set_jumps(no_match, LABEL()); OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(SLJIT_SP), POSSESSIVE1); @@ -9124,7 +9142,7 @@ switch(opcode) label = LABEL(); detect_partial_match(common, &no_match); compile_char1_matchingpath(common, type, cc, &no_char1_match, FALSE); - OP2(SLJIT_SUB | SLJIT_SET_E, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, tmp_base, tmp_offset, tmp_base, tmp_offset, SLJIT_IMM, 1); JUMPTO(SLJIT_NOT_ZERO, label); OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(1)); set_jumps(no_char1_match, LABEL()); @@ -9142,7 +9160,7 @@ switch(opcode) break; default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); break; } @@ -9264,7 +9282,7 @@ size = 3 + (size < 0 ? 0 : size); OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr); allocate_stack(common, size); if (size > 3) - OP2(SLJIT_SUB, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, STACK_TOP, 0, SLJIT_IMM, (size - 3) * sizeof(sljit_sw)); + OP2(SLJIT_ADD, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, STACK_TOP, 0, SLJIT_IMM, (size - 3) * sizeof(sljit_sw)); else OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, STACK_TOP, 0); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(size - 1), SLJIT_IMM, BACKTRACK_AS(then_trap_backtrack)->start); @@ -9569,7 +9587,7 @@ while (cc < ccend) break; default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); return; } if (cc == NULL) @@ -9677,7 +9695,7 @@ switch(opcode) case OP_MINUPTO: OP1(SLJIT_MOV, TMP1, 0, base, offset1); OP1(SLJIT_MOV, STR_PTR, 0, base, offset0); - OP2(SLJIT_SUB | SLJIT_SET_E, TMP1, 0, TMP1, 0, SLJIT_IMM, 1); + OP2(SLJIT_SUB | SLJIT_SET_Z, TMP1, 0, TMP1, 0, SLJIT_IMM, 1); add_jump(compiler, &jumplist, JUMP(SLJIT_ZERO)); OP1(SLJIT_MOV, base, offset1, TMP1, 0); @@ -9723,7 +9741,7 @@ switch(opcode) break; default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); break; } @@ -9831,7 +9849,7 @@ if (*cc == OP_ASSERT || *cc == OP_ASSERTBACK) { OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), CURRENT_AS(assert_backtrack)->private_data_ptr); add_jump(compiler, &common->revertframes, JUMP(SLJIT_FAST_CALL)); - OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), CURRENT_AS(assert_backtrack)->private_data_ptr, SLJIT_MEM1(STACK_TOP), CURRENT_AS(assert_backtrack)->framesize * sizeof(sljit_sw)); + OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), CURRENT_AS(assert_backtrack)->private_data_ptr, SLJIT_MEM1(STACK_TOP), STACK(-CURRENT_AS(assert_backtrack)->framesize - 1)); set_jumps(current->topbacktracks, LABEL()); } @@ -9841,7 +9859,7 @@ else if (bra == OP_BRAZERO) { /* We know there is enough place on the stack. */ - OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); + OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, sizeof(sljit_sw)); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(0), SLJIT_IMM, 0); JUMPTO(SLJIT_JUMP, CURRENT_AS(assert_backtrack)->matchingpath); JUMPHERE(brajump); @@ -9954,7 +9972,7 @@ else if (ket == OP_KETRMIN) else { OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), private_data_ptr); - CMPTO(SLJIT_NOT_EQUAL, STR_PTR, 0, SLJIT_MEM1(TMP1), (CURRENT_AS(bracket_backtrack)->u.framesize + 1) * sizeof(sljit_sw), CURRENT_AS(bracket_backtrack)->recursive_matchingpath); + CMPTO(SLJIT_NOT_EQUAL, STR_PTR, 0, SLJIT_MEM1(TMP1), STACK(-CURRENT_AS(bracket_backtrack)->u.framesize - 2), CURRENT_AS(bracket_backtrack)->recursive_matchingpath); } /* Drop STR_PTR for non-greedy plus quantifier. */ if (opcode != OP_ONCE) @@ -10060,7 +10078,7 @@ if (SLJIT_UNLIKELY(opcode == OP_COND) || SLJIT_UNLIKELY(opcode == OP_SCOND)) { OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), assert->private_data_ptr); add_jump(compiler, &common->revertframes, JUMP(SLJIT_FAST_CALL)); - OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), assert->private_data_ptr, SLJIT_MEM1(STACK_TOP), assert->framesize * sizeof(sljit_sw)); + OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), assert->private_data_ptr, SLJIT_MEM1(STACK_TOP), STACK(-assert->framesize - 1)); } cond = JUMP(SLJIT_JUMP); set_jumps(CURRENT_AS(bracket_backtrack)->u.assert->condfailed, LABEL()); @@ -10201,7 +10219,7 @@ if (has_alternatives) { OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), assert->private_data_ptr); add_jump(compiler, &common->revertframes, JUMP(SLJIT_FAST_CALL)); - OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), assert->private_data_ptr, SLJIT_MEM1(STACK_TOP), assert->framesize * sizeof(sljit_sw)); + OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), assert->private_data_ptr, SLJIT_MEM1(STACK_TOP), STACK(-assert->framesize - 1)); } JUMPHERE(cond); } @@ -10256,7 +10274,7 @@ else if (opcode == OP_ONCE) JUMPHERE(once); /* Restore previous private_data_ptr */ if (CURRENT_AS(bracket_backtrack)->u.framesize >= 0) - OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_MEM1(STACK_TOP), CURRENT_AS(bracket_backtrack)->u.framesize * sizeof(sljit_sw)); + OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), private_data_ptr, SLJIT_MEM1(STACK_TOP), STACK(-CURRENT_AS(bracket_backtrack)->u.framesize - 1)); else if (ket == OP_KETRMIN) { OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STACK_TOP), STACK(1)); @@ -10346,7 +10364,7 @@ if (current->topbacktracks) free_stack(common, CURRENT_AS(bracketpos_backtrack)->stacksize); JUMPHERE(jump); } -OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), CURRENT_AS(bracketpos_backtrack)->private_data_ptr, SLJIT_MEM1(STACK_TOP), CURRENT_AS(bracketpos_backtrack)->framesize * sizeof(sljit_sw)); +OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), CURRENT_AS(bracketpos_backtrack)->private_data_ptr, SLJIT_MEM1(STACK_TOP), STACK(-CURRENT_AS(bracketpos_backtrack)->framesize - 1)); } static SLJIT_INLINE void compile_braminzero_backtrackingpath(compiler_common *common, struct backtrack_common *current) @@ -10392,10 +10410,10 @@ if (opcode == OP_THEN || opcode == OP_THEN_ARG) jump = JUMP(SLJIT_JUMP); loop = LABEL(); - OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(STACK_TOP), -(int)sizeof(sljit_sw)); + OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(STACK_TOP), STACK(0)); JUMPHERE(jump); - CMPTO(SLJIT_NOT_EQUAL, SLJIT_MEM1(STACK_TOP), -(int)(2 * sizeof(sljit_sw)), TMP1, 0, loop); - CMPTO(SLJIT_NOT_EQUAL, SLJIT_MEM1(STACK_TOP), -(int)(3 * sizeof(sljit_sw)), TMP2, 0, loop); + CMPTO(SLJIT_NOT_EQUAL, SLJIT_MEM1(STACK_TOP), STACK(1), TMP1, 0, loop); + CMPTO(SLJIT_NOT_EQUAL, SLJIT_MEM1(STACK_TOP), STACK(2), TMP2, 0, loop); add_jump(compiler, &common->then_trap->quit, JUMP(SLJIT_JUMP)); return; } @@ -10645,7 +10663,7 @@ while (current) break; default: - SLJIT_ASSERT_STOP(); + SLJIT_UNREACHABLE(); break; } current = current->prev; @@ -10684,7 +10702,7 @@ sljit_emit_fast_enter(compiler, TMP2, 0); count_match(common); allocate_stack(common, private_data_size + framesize + alternativesize); OP1(SLJIT_MOV, SLJIT_MEM1(STACK_TOP), STACK(private_data_size + framesize + alternativesize - 1), TMP2, 0); -copy_private_data(common, ccbegin, ccend, TRUE, private_data_size + framesize + alternativesize, framesize + alternativesize, needs_control_head); +copy_private_data(common, ccbegin, ccend, TRUE, framesize + alternativesize, private_data_size + framesize + alternativesize, needs_control_head); if (needs_control_head) OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, SLJIT_IMM, 0); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->recursive_head_ptr, STACK_TOP, 0); @@ -10737,9 +10755,9 @@ if (common->quit != NULL) OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), common->recursive_head_ptr); if (needs_frame) { - OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, (framesize + alternativesize) * sizeof(sljit_sw)); - add_jump(compiler, &common->revertframes, JUMP(SLJIT_FAST_CALL)); OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, (framesize + alternativesize) * sizeof(sljit_sw)); + add_jump(compiler, &common->revertframes, JUMP(SLJIT_FAST_CALL)); + OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, (framesize + alternativesize) * sizeof(sljit_sw)); } OP1(SLJIT_MOV, TMP3, 0, SLJIT_IMM, 0); common->quit = NULL; @@ -10750,32 +10768,32 @@ set_jumps(common->accept, LABEL()); OP1(SLJIT_MOV, STACK_TOP, 0, SLJIT_MEM1(SLJIT_SP), common->recursive_head_ptr); if (needs_frame) { - OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, (framesize + alternativesize) * sizeof(sljit_sw)); - add_jump(compiler, &common->revertframes, JUMP(SLJIT_FAST_CALL)); OP2(SLJIT_ADD, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, (framesize + alternativesize) * sizeof(sljit_sw)); + add_jump(compiler, &common->revertframes, JUMP(SLJIT_FAST_CALL)); + OP2(SLJIT_SUB, STACK_TOP, 0, STACK_TOP, 0, SLJIT_IMM, (framesize + alternativesize) * sizeof(sljit_sw)); } OP1(SLJIT_MOV, TMP3, 0, SLJIT_IMM, 1); JUMPHERE(jump); if (common->quit != NULL) set_jumps(common->quit, LABEL()); -copy_private_data(common, ccbegin, ccend, FALSE, private_data_size + framesize + alternativesize, framesize + alternativesize, needs_control_head); +copy_private_data(common, ccbegin, ccend, FALSE, framesize + alternativesize, private_data_size + framesize + alternativesize, needs_control_head); free_stack(common, private_data_size + framesize + alternativesize); if (needs_control_head) { - OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STACK_TOP), 2 * sizeof(sljit_sw)); - OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(STACK_TOP), sizeof(sljit_sw)); + OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STACK_TOP), STACK(-3)); + OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(STACK_TOP), STACK(-2)); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->recursive_head_ptr, TMP1, 0); OP1(SLJIT_MOV, TMP1, 0, TMP3, 0); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->control_head_ptr, TMP2, 0); } else { - OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(STACK_TOP), sizeof(sljit_sw)); + OP1(SLJIT_MOV, TMP2, 0, SLJIT_MEM1(STACK_TOP), STACK(-2)); OP1(SLJIT_MOV, TMP1, 0, TMP3, 0); OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), common->recursive_head_ptr, TMP2, 0); } -sljit_emit_fast_return(compiler, SLJIT_MEM1(STACK_TOP), 0); +sljit_emit_fast_return(compiler, SLJIT_MEM1(STACK_TOP), STACK(-1)); } #undef COMPILE_BACKTRACKINGPATH @@ -11237,7 +11255,7 @@ OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), LOCALS1, TMP2, 0); OP1(SLJIT_MOV, TMP1, 0, ARGUMENTS, 0); OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(TMP1), SLJIT_OFFSETOF(jit_arguments, stack)); OP1(SLJIT_MOV, SLJIT_MEM1(TMP1), SLJIT_OFFSETOF(struct sljit_stack, top), STACK_TOP, 0); -OP2(SLJIT_ADD, TMP2, 0, SLJIT_MEM1(TMP1), SLJIT_OFFSETOF(struct sljit_stack, limit), SLJIT_IMM, STACK_GROWTH_RATE); +OP2(SLJIT_SUB, TMP2, 0, SLJIT_MEM1(TMP1), SLJIT_OFFSETOF(struct sljit_stack, limit), SLJIT_IMM, STACK_GROWTH_RATE); sljit_emit_ijump(compiler, SLJIT_CALL2, SLJIT_IMM, SLJIT_FUNC_OFFSET(sljit_stack_resize)); jump = CMP(SLJIT_NOT_EQUAL, SLJIT_RETURN_REG, 0, SLJIT_IMM, 0); @@ -11391,10 +11409,10 @@ union { sljit_u8 local_space[MACHINE_STACK_SIZE]; struct sljit_stack local_stack; -local_stack.top = (sljit_sw)&local_space; -local_stack.base = local_stack.top; -local_stack.limit = local_stack.base + MACHINE_STACK_SIZE; -local_stack.max_limit = local_stack.limit; +local_stack.max_limit = local_space; +local_stack.limit = local_space; +local_stack.base = local_space + MACHINE_STACK_SIZE; +local_stack.top = local_space + MACHINE_STACK_SIZE; arguments->stack = &local_stack; convert_executable_func.executable_func = executable_func; return convert_executable_func.call_executable_func(arguments); diff --git a/src/third_party/pcre-8.39/pcre_jit_test.c b/src/third_party/pcre-8.41/pcre_jit_test.c index 9b61ec000fa..034cb52697f 100644 --- a/src/third_party/pcre-8.39/pcre_jit_test.c +++ b/src/third_party/pcre-8.41/pcre_jit_test.c @@ -687,6 +687,7 @@ static struct regression_test_case regression_test_cases[] = { { PCRE_FIRSTLINE | PCRE_NEWLINE_LF | PCRE_DOTALL, 0 | F_NOMATCH, "ab.", "ab" }, { MUA | PCRE_FIRSTLINE, 1 | F_NOMATCH, "^[a-d0-9]", "\nxx\nd" }, { PCRE_NEWLINE_ANY | PCRE_FIRSTLINE | PCRE_DOTALL, 0, "....a", "012\n0a" }, + { MUA | PCRE_FIRSTLINE, 0, "[aC]", "a" }, /* Recurse. */ { MUA, 0, "(a)(?1)", "aa" }, diff --git a/src/third_party/pcre-8.39/pcre_maketables.c b/src/third_party/pcre-8.41/pcre_maketables.c index a44a6eaa905..a44a6eaa905 100644 --- a/src/third_party/pcre-8.39/pcre_maketables.c +++ b/src/third_party/pcre-8.41/pcre_maketables.c diff --git a/src/third_party/pcre-8.39/pcre_newline.c b/src/third_party/pcre-8.41/pcre_newline.c index b8f5a4de19c..b8f5a4de19c 100644 --- a/src/third_party/pcre-8.39/pcre_newline.c +++ b/src/third_party/pcre-8.41/pcre_newline.c diff --git a/src/third_party/pcre-8.39/pcre_ord2utf8.c b/src/third_party/pcre-8.41/pcre_ord2utf8.c index 95f1beb963e..95f1beb963e 100644 --- a/src/third_party/pcre-8.39/pcre_ord2utf8.c +++ b/src/third_party/pcre-8.41/pcre_ord2utf8.c diff --git a/src/third_party/pcre-8.39/pcre_printint.c b/src/third_party/pcre-8.41/pcre_printint.c index 60dcb55efbf..60dcb55efbf 100644 --- a/src/third_party/pcre-8.39/pcre_printint.c +++ b/src/third_party/pcre-8.41/pcre_printint.c diff --git a/src/third_party/pcre-8.39/pcre_refcount.c b/src/third_party/pcre-8.41/pcre_refcount.c index 79efa90f216..79efa90f216 100644 --- a/src/third_party/pcre-8.39/pcre_refcount.c +++ b/src/third_party/pcre-8.41/pcre_refcount.c diff --git a/src/third_party/pcre-8.39/pcre_scanner.cc b/src/third_party/pcre-8.41/pcre_scanner.cc index 6be2be6829b..6be2be6829b 100644 --- a/src/third_party/pcre-8.39/pcre_scanner.cc +++ b/src/third_party/pcre-8.41/pcre_scanner.cc diff --git a/src/third_party/pcre-8.39/pcre_scanner.h b/src/third_party/pcre-8.41/pcre_scanner.h index 5617e4515cb..5617e4515cb 100644 --- a/src/third_party/pcre-8.39/pcre_scanner.h +++ b/src/third_party/pcre-8.41/pcre_scanner.h diff --git a/src/third_party/pcre-8.39/pcre_scanner_unittest.cc b/src/third_party/pcre-8.41/pcre_scanner_unittest.cc index c00312c4f63..623e2afda80 100644 --- a/src/third_party/pcre-8.39/pcre_scanner_unittest.cc +++ b/src/third_party/pcre-8.41/pcre_scanner_unittest.cc @@ -57,6 +57,7 @@ } while (0) using std::vector; +using std::string; using pcrecpp::StringPiece; using pcrecpp::Scanner; diff --git a/src/third_party/pcre-8.39/pcre_string_utils.c b/src/third_party/pcre-8.41/pcre_string_utils.c index 25eacc85073..25eacc85073 100644 --- a/src/third_party/pcre-8.39/pcre_string_utils.c +++ b/src/third_party/pcre-8.41/pcre_string_utils.c diff --git a/src/third_party/pcre-8.39/pcre_stringpiece.cc b/src/third_party/pcre-8.41/pcre_stringpiece.cc index 67c0f1fc0e5..67c0f1fc0e5 100644 --- a/src/third_party/pcre-8.39/pcre_stringpiece.cc +++ b/src/third_party/pcre-8.41/pcre_stringpiece.cc diff --git a/src/third_party/pcre-8.39/pcre_stringpiece.h b/src/third_party/pcre-8.41/pcre_stringpiece.h index cc3dc42963a..cb94f52a010 100644 --- a/src/third_party/pcre-8.39/pcre_stringpiece.h +++ b/src/third_party/pcre-8.41/pcre_stringpiece.h @@ -52,12 +52,12 @@ #include <pcre.h> +namespace pcrecpp { + using std::memcmp; using std::strlen; using std::string; -namespace pcrecpp { - class PCRECPP_EXP_DEFN StringPiece { private: const char* ptr_; diff --git a/src/third_party/pcre-8.39/pcre_stringpiece.h.in b/src/third_party/pcre-8.41/pcre_stringpiece.h.in index eb25826b453..f54f3f3b31b 100644 --- a/src/third_party/pcre-8.39/pcre_stringpiece.h.in +++ b/src/third_party/pcre-8.41/pcre_stringpiece.h.in @@ -52,12 +52,12 @@ #include <pcre.h> +namespace pcrecpp { + using std::memcmp; using std::strlen; using std::string; -namespace pcrecpp { - class PCRECPP_EXP_DEFN StringPiece { private: const char* ptr_; diff --git a/src/third_party/pcre-8.39/pcre_stringpiece_unittest.cc b/src/third_party/pcre-8.41/pcre_stringpiece_unittest.cc index 1c4759da3b0..88e73a1f976 100644 --- a/src/third_party/pcre-8.39/pcre_stringpiece_unittest.cc +++ b/src/third_party/pcre-8.41/pcre_stringpiece_unittest.cc @@ -24,6 +24,7 @@ } \ } while (0) +using std::string; using pcrecpp::StringPiece; static void CheckSTLComparator() { diff --git a/src/third_party/pcre-8.39/pcre_study.c b/src/third_party/pcre-8.41/pcre_study.c index d9d4960d84e..d9d4960d84e 100644 --- a/src/third_party/pcre-8.39/pcre_study.c +++ b/src/third_party/pcre-8.41/pcre_study.c diff --git a/src/third_party/pcre-8.39/pcre_tables.c b/src/third_party/pcre-8.41/pcre_tables.c index 4960af57c4d..5e18e8cf904 100644 --- a/src/third_party/pcre-8.39/pcre_tables.c +++ b/src/third_party/pcre-8.41/pcre_tables.c @@ -6,7 +6,7 @@ and semantics are as close as possible to those of the Perl 5 language. Written by Philip Hazel - Copyright (c) 1997-2012 University of Cambridge + Copyright (c) 1997-2017 University of Cambridge ----------------------------------------------------------------------------- Redistribution and use in source and binary forms, with or without @@ -161,7 +161,7 @@ const pcre_uint32 PRIV(ucp_gbtable[]) = { (1<<ucp_gbExtend)|(1<<ucp_gbSpacingMark), /* 5 SpacingMark */ (1<<ucp_gbExtend)|(1<<ucp_gbSpacingMark)|(1<<ucp_gbL)| /* 6 L */ - (1<<ucp_gbL)|(1<<ucp_gbV)|(1<<ucp_gbLV)|(1<<ucp_gbLVT), + (1<<ucp_gbV)|(1<<ucp_gbLV)|(1<<ucp_gbLVT), (1<<ucp_gbExtend)|(1<<ucp_gbSpacingMark)|(1<<ucp_gbV)| /* 7 V */ (1<<ucp_gbT), diff --git a/src/third_party/pcre-8.39/pcre_ucd.c b/src/third_party/pcre-8.41/pcre_ucd.c index 69c4fd42c34..f22f826c4c2 100644 --- a/src/third_party/pcre-8.39/pcre_ucd.c +++ b/src/third_party/pcre-8.41/pcre_ucd.c @@ -38,6 +38,20 @@ const pcre_uint16 PRIV(ucd_stage2)[] = {0}; const pcre_uint32 PRIV(ucd_caseless_sets)[] = {0}; #else +/* If the 32-bit library is run in non-32-bit mode, character values +greater than 0x10ffff may be encountered. For these we set up a +special record. */ + +#ifdef COMPILE_PCRE32 +const ucd_record PRIV(dummy_ucd_record)[] = {{ + ucp_Common, /* script */ + ucp_Cn, /* type unassigned */ + ucp_gbOther, /* grapheme break property */ + 0, /* case set */ + 0, /* other case */ + }}; +#endif + /* When recompiling tables with a new Unicode version, please check the types in this structure definition from pcre_internal.h (the actual field names will be different): diff --git a/src/third_party/pcre-8.39/pcre_valid_utf8.c b/src/third_party/pcre-8.41/pcre_valid_utf8.c index 3b0f6464a35..3b0f6464a35 100644 --- a/src/third_party/pcre-8.39/pcre_valid_utf8.c +++ b/src/third_party/pcre-8.41/pcre_valid_utf8.c diff --git a/src/third_party/pcre-8.39/pcre_version.c b/src/third_party/pcre-8.41/pcre_version.c index ae86ff28bc8..ae86ff28bc8 100644 --- a/src/third_party/pcre-8.39/pcre_version.c +++ b/src/third_party/pcre-8.41/pcre_version.c diff --git a/src/third_party/pcre-8.39/pcre_xclass.c b/src/third_party/pcre-8.41/pcre_xclass.c index ef759a589a6..ef759a589a6 100644 --- a/src/third_party/pcre-8.39/pcre_xclass.c +++ b/src/third_party/pcre-8.41/pcre_xclass.c diff --git a/src/third_party/pcre-8.39/pcrecpp.cc b/src/third_party/pcre-8.41/pcrecpp.cc index d09c9abc516..d09c9abc516 100644 --- a/src/third_party/pcre-8.39/pcrecpp.cc +++ b/src/third_party/pcre-8.41/pcrecpp.cc diff --git a/src/third_party/pcre-8.39/pcrecpp.h b/src/third_party/pcre-8.41/pcrecpp.h index 3e594b0d439..3e594b0d439 100644 --- a/src/third_party/pcre-8.39/pcrecpp.h +++ b/src/third_party/pcre-8.41/pcrecpp.h diff --git a/src/third_party/pcre-8.39/pcrecpp_internal.h b/src/third_party/pcre-8.41/pcrecpp_internal.h index 827f9e04e2a..827f9e04e2a 100644 --- a/src/third_party/pcre-8.39/pcrecpp_internal.h +++ b/src/third_party/pcre-8.41/pcrecpp_internal.h diff --git a/src/third_party/pcre-8.39/pcrecpp_unittest.cc b/src/third_party/pcre-8.41/pcrecpp_unittest.cc index 92cae8fbea5..4b15fbef1c3 100644 --- a/src/third_party/pcre-8.39/pcrecpp_unittest.cc +++ b/src/third_party/pcre-8.41/pcrecpp_unittest.cc @@ -43,6 +43,7 @@ #include <vector> #include "pcrecpp.h" +using std::string; using pcrecpp::StringPiece; using pcrecpp::RE; using pcrecpp::RE_Options; diff --git a/src/third_party/pcre-8.39/pcrecpparg.h b/src/third_party/pcre-8.41/pcrecpparg.h index b4f9c3f4989..b4f9c3f4989 100644 --- a/src/third_party/pcre-8.39/pcrecpparg.h +++ b/src/third_party/pcre-8.41/pcrecpparg.h diff --git a/src/third_party/pcre-8.39/pcrecpparg.h.in b/src/third_party/pcre-8.41/pcrecpparg.h.in index 61bcab5402c..61bcab5402c 100644 --- a/src/third_party/pcre-8.39/pcrecpparg.h.in +++ b/src/third_party/pcre-8.41/pcrecpparg.h.in diff --git a/src/third_party/pcre-8.39/pcredemo.c b/src/third_party/pcre-8.41/pcredemo.c index 946aba45cdc..946aba45cdc 100644 --- a/src/third_party/pcre-8.39/pcredemo.c +++ b/src/third_party/pcre-8.41/pcredemo.c diff --git a/src/third_party/pcre-8.39/pcregexp.pas b/src/third_party/pcre-8.41/pcregexp.pas index bb2b3da8f3d..bb2b3da8f3d 100644 --- a/src/third_party/pcre-8.39/pcregexp.pas +++ b/src/third_party/pcre-8.41/pcregexp.pas diff --git a/src/third_party/pcre-8.39/pcregrep.c b/src/third_party/pcre-8.41/pcregrep.c index cd53c648da2..317f7454e13 100644 --- a/src/third_party/pcre-8.39/pcregrep.c +++ b/src/third_party/pcre-8.41/pcregrep.c @@ -1803,6 +1803,7 @@ while (ptr < endptr) match = FALSE; if (line_buffered) fflush(stdout); rc = 0; /* Had some success */ + startoffset = offsets[1]; /* Restart after the match */ if (startoffset <= oldstartoffset) { @@ -1812,6 +1813,22 @@ while (ptr < endptr) if (utf8) while ((matchptr[startoffset] & 0xc0) == 0x80) startoffset++; } + + /* If the current match ended past the end of the line (only possible + in multiline mode), we must move on to the line in which it did end + before searching for more matches. */ + + while (startoffset > (int)linelength) + { + matchptr = ptr += linelength + endlinelength; + filepos += (int)(linelength + endlinelength); + linenumber++; + startoffset -= (int)(linelength + endlinelength); + t = end_of_line(ptr, endptr, &endlinelength); + linelength = t - ptr - endlinelength; + length = (size_t)(endptr - ptr); + } + goto ONLY_MATCHING_RESTART; } } @@ -3173,9 +3190,11 @@ for (j = 1, cp = patterns; cp != NULL; j++, cp = cp->next) cp->hint = pcre_study(cp->compiled, study_options, &error); if (error != NULL) { - char s[16]; - if (patterns->next == NULL) s[0] = 0; else sprintf(s, " number %d", j); - fprintf(stderr, "pcregrep: Error while studying regex%s: %s\n", s, error); + if (patterns->next == NULL) + fprintf(stderr, "pcregrep: Error while studying regex: %s\n", error); + else + fprintf(stderr, "pcregrep: Error while studying regex number %d: %s\n", + j, error); goto EXIT2; } #ifdef SUPPORT_PCREGREP_JIT diff --git a/src/third_party/pcre-8.39/pcreposix.c b/src/third_party/pcre-8.41/pcreposix.c index cf75588c40d..7b404a71100 100644 --- a/src/third_party/pcre-8.39/pcreposix.c +++ b/src/third_party/pcre-8.41/pcreposix.c @@ -6,7 +6,7 @@ and semantics are as close as possible to those of the Perl 5 language. Written by Philip Hazel - Copyright (c) 1997-2016 University of Cambridge + Copyright (c) 1997-2017 University of Cambridge ----------------------------------------------------------------------------- Redistribution and use in source and binary forms, with or without @@ -389,8 +389,8 @@ if (rc >= 0) { for (i = 0; i < (size_t)rc; i++) { - pmatch[i].rm_so = ovector[i*2]; - pmatch[i].rm_eo = ovector[i*2+1]; + pmatch[i].rm_so = ovector[i*2] + so; + pmatch[i].rm_eo = ovector[i*2+1] + so; } if (allocated_ovector) free(ovector); for (; i < nmatch; i++) pmatch[i].rm_so = pmatch[i].rm_eo = -1; diff --git a/src/third_party/pcre-8.39/pcreposix.h b/src/third_party/pcre-8.41/pcreposix.h index c77c0b0523c..c77c0b0523c 100644 --- a/src/third_party/pcre-8.39/pcreposix.h +++ b/src/third_party/pcre-8.41/pcreposix.h diff --git a/src/third_party/pcre-8.39/pcretest.c b/src/third_party/pcre-8.41/pcretest.c index 78ef5177df7..f1303037281 100644 --- a/src/third_party/pcre-8.39/pcretest.c +++ b/src/third_party/pcre-8.41/pcretest.c @@ -177,7 +177,7 @@ that differ in their output from isprint() even in the "C" locale. */ #define PRINTABLE(c) ((c) >= 32 && (c) < 127) #endif -#define PRINTOK(c) (locale_set? isprint(c) : PRINTABLE(c)) +#define PRINTOK(c) (locale_set? (((c) < 256) && isprint(c)) : PRINTABLE(c)) /* Posix support is disabled in 16 or 32 bit only mode. */ #if !defined SUPPORT_PCRE8 && !defined NOPOSIX @@ -426,11 +426,11 @@ argument, the casting might be incorrectly applied. */ #define PCRE_COPY_NAMED_SUBSTRING32(rc, re, bptr, offsets, count, \ namesptr, cbuffer, size) \ rc = pcre32_copy_named_substring((pcre32 *)re, (PCRE_SPTR32)bptr, offsets, \ - count, (PCRE_SPTR32)namesptr, (PCRE_UCHAR32 *)cbuffer, size/2) + count, (PCRE_SPTR32)namesptr, (PCRE_UCHAR32 *)cbuffer, size/4) #define PCRE_COPY_SUBSTRING32(rc, bptr, offsets, count, i, cbuffer, size) \ rc = pcre32_copy_substring((PCRE_SPTR32)bptr, offsets, count, i, \ - (PCRE_UCHAR32 *)cbuffer, size/2) + (PCRE_UCHAR32 *)cbuffer, size/4) #define PCRE_DFA_EXEC32(count, re, extra, bptr, len, start_offset, options, \ offsets, size_offsets, workspace, size_workspace) \ @@ -1982,6 +1982,7 @@ return(result); static int pchar(pcre_uint32 c, FILE *f) { int n = 0; +char tempbuffer[16]; if (PRINTOK(c)) { if (f != NULL) fprintf(f, "%c", c); @@ -2003,6 +2004,8 @@ if (c < 0x100) } if (f != NULL) n = fprintf(f, "\\x{%02x}", c); + else n = sprintf(tempbuffer, "\\x{%02x}", c); + return n >= 0 ? n : 0; } @@ -4831,7 +4834,16 @@ while (!done) continue; case 'O': - while(isdigit(*p)) n = n * 10 + *p++ - '0'; + while(isdigit(*p)) + { + if (n > (INT_MAX-10)/10) /* Hack to stop fuzzers */ + { + printf("** \\O argument is too big\n"); + yield = 1; + goto EXIT; + } + n = n * 10 + *p++ - '0'; + } if (n > size_offsets_max) { size_offsets_max = n; @@ -5042,7 +5054,7 @@ while (!done) if ((all_use_dfa || use_dfa) && find_match_limit) { - printf("**Match limit not relevant for DFA matching: ignored\n"); + printf("** Match limit not relevant for DFA matching: ignored\n"); find_match_limit = 0; } @@ -5255,10 +5267,17 @@ while (!done) if (do_allcaps) { - if (new_info(re, NULL, PCRE_INFO_CAPTURECOUNT, &count) < 0) - goto SKIP_DATA; - count++; /* Allow for full match */ - if (count * 2 > use_size_offsets) count = use_size_offsets/2; + if (all_use_dfa || use_dfa) + { + fprintf(outfile, "** Show all captures ignored after DFA matching\n"); + } + else + { + if (new_info(re, NULL, PCRE_INFO_CAPTURECOUNT, &count) < 0) + goto SKIP_DATA; + count++; /* Allow for full match */ + if (count * 2 > use_size_offsets) count = use_size_offsets/2; + } } /* Output the captured substrings. Note that, for the matched string, diff --git a/src/third_party/pcre-8.39/perltest.pl b/src/third_party/pcre-8.41/perltest.pl index 29b808b5293..29b808b5293 100755 --- a/src/third_party/pcre-8.39/perltest.pl +++ b/src/third_party/pcre-8.41/perltest.pl diff --git a/src/third_party/pcre-8.39/ucp.h b/src/third_party/pcre-8.41/ucp.h index 2fa00296e42..2fa00296e42 100644 --- a/src/third_party/pcre-8.39/ucp.h +++ b/src/third_party/pcre-8.41/ucp.h diff --git a/src/third_party/scripts/pcre_get_sources.sh b/src/third_party/scripts/pcre_get_sources.sh index da73652ef66..16c79a3e911 100644 --- a/src/third_party/scripts/pcre_get_sources.sh +++ b/src/third_party/scripts/pcre_get_sources.sh @@ -16,7 +16,7 @@ if [ "$#" -ne 0 ]; then exit 1 fi -VERSION=8.39 +VERSION=8.41 NAME=pcre TARBALL=$NAME-$VERSION.tar.gz TARBALL_DIR=$NAME-$VERSION diff --git a/src/third_party/wiredtiger/build_linux/wiredtiger_config.h b/src/third_party/wiredtiger/build_linux/wiredtiger_config.h index 1122e1e319d..6fffea61ad1 100644 --- a/src/third_party/wiredtiger/build_linux/wiredtiger_config.h +++ b/src/third_party/wiredtiger/build_linux/wiredtiger_config.h @@ -82,6 +82,9 @@ /* Define to 1 if you have the `posix_memalign' function. */ #define HAVE_POSIX_MEMALIGN 1 +/* Define to 1 if pthread condition variables support monotonic clocks. */ +#define HAVE_PTHREAD_COND_MONOTONIC 1 + /* Define to 1 if you have the <pthread_np.h> header file. */ /* #undef HAVE_PTHREAD_NP_H */ diff --git a/src/third_party/wiredtiger/build_posix/configure.ac.in b/src/third_party/wiredtiger/build_posix/configure.ac.in index 0fef587b4b8..415545a0d56 100644 --- a/src/third_party/wiredtiger/build_posix/configure.ac.in +++ b/src/third_party/wiredtiger/build_posix/configure.ac.in @@ -160,6 +160,44 @@ AS_CASE([$host_os], [darwin*], [], [AC_CHECK_FUNCS([fdatasync])]) # the generic declaration in AC_CHECK_FUNCS is incompatible. AX_FUNC_POSIX_MEMALIGN +# Check for POSIX condition variables with monotonic clock support +AC_CACHE_CHECK([for condition waits with monotonic clock support], + [wt_cv_pthread_cond_monotonic], + [AC_RUN_IFELSE([AC_LANG_SOURCE([[ +#include <errno.h> +#include <pthread.h> +#include <stdlib.h> +#include <time.h> + +int main() +{ + int ret; + pthread_condattr_t condattr; + pthread_cond_t cond; + pthread_mutex_t mtx; + struct timespec ts; + + if ((ret = pthread_condattr_init(&condattr)) != 0) exit(1); + if ((ret = pthread_condattr_setclock(&condattr, CLOCK_MONOTONIC)) != 0) exit(1); + if ((ret = pthread_cond_init(&cond, &condattr)) != 0) exit(1); + if ((ret = pthread_mutex_init(&mtx, NULL)) != 0) exit(1); + if ((ret = clock_gettime(CLOCK_MONOTONIC, &ts)) != 0) exit(1); + ts.tv_sec += 1; + if ((ret = pthread_mutex_lock(&mtx)) != 0) exit(1); + if ((ret = pthread_cond_timedwait(&cond, &mtx, &ts)) != 0 && ret != EINTR && ret != ETIMEDOUT) exit(1); + + exit(0); +} + ]])], + [wt_pthread_cond_monotonic=yes], + [wt_pthread_cond_monotonic=no], + [wt_pthread_cond_monotonic=no])]) +AC_MSG_RESULT($wt_pthread_cond_monotonic) +if test "$wt_pthread_cond_monotonic" = "yes" ; then + AC_DEFINE([HAVE_PTHREAD_COND_MONOTONIC], [1], + [Define to 1 if pthread condition variables support monotonic clocks.]) +fi + AC_SYS_LARGEFILE AC_C_BIGENDIAN diff --git a/src/third_party/wiredtiger/build_win/wiredtiger_config.h b/src/third_party/wiredtiger/build_win/wiredtiger_config.h index 78d2784cb70..8babdbfdc1b 100644 --- a/src/third_party/wiredtiger/build_win/wiredtiger_config.h +++ b/src/third_party/wiredtiger/build_win/wiredtiger_config.h @@ -79,6 +79,9 @@ /* Define to 1 if you have the <memory.h> header file. */ /* #undef HAVE_MEMORY_H */ +/* Define to 1 if pthread condition variables support monotonic clocks. */ +/* #undef HAVE_PTHREAD_COND_MONOTONIC */ + /* Define to 1 if you have the `posix_fadvise' function. */ /* #undef HAVE_POSIX_FADVISE */ diff --git a/src/third_party/wiredtiger/dist/api_data.py b/src/third_party/wiredtiger/dist/api_data.py index 22600dd5e29..596099647be 100644 --- a/src/third_party/wiredtiger/dist/api_data.py +++ b/src/third_party/wiredtiger/dist/api_data.py @@ -753,6 +753,9 @@ wiredtiger_open_common =\ Config('session_scratch_max', '2MB', r''' maximum memory to cache in each session''', type='int', undoc=True), + Config('session_table_cache', 'true', r''' + Maintain a per-session cache of tables''', + type='boolean'), Config('transaction_sync', '', r''' how to sync log records when the transaction commits''', type='category', subconfig=[ diff --git a/src/third_party/wiredtiger/dist/filelist b/src/third_party/wiredtiger/dist/filelist index 5a3348b940a..f53509e96ec 100644 --- a/src/third_party/wiredtiger/dist/filelist +++ b/src/third_party/wiredtiger/dist/filelist @@ -191,6 +191,7 @@ src/support/rand.c src/support/scratch.c src/support/stat.c src/support/thread_group.c +src/support/time.c src/txn/txn.c src/txn/txn_ckpt.c src/txn/txn_ext.c diff --git a/src/third_party/wiredtiger/dist/flags.py b/src/third_party/wiredtiger/dist/flags.py index 64b5d789e72..1ce717f3586 100644 --- a/src/third_party/wiredtiger/dist/flags.py +++ b/src/third_party/wiredtiger/dist/flags.py @@ -32,7 +32,6 @@ flags = { 'READ_PREV', 'READ_RESTART_OK', 'READ_SKIP_INTL', - 'READ_SKIP_LEAF', 'READ_TRUNCATE', 'READ_WONT_NEED', ], @@ -111,6 +110,7 @@ flags = { 'CONN_SERVER_LSM', 'CONN_SERVER_STATISTICS', 'CONN_SERVER_SWEEP', + 'CONN_TABLE_CACHE', 'CONN_WAS_BACKUP', ], 'session' : [ diff --git a/src/third_party/wiredtiger/dist/s_string.ok b/src/third_party/wiredtiger/dist/s_string.ok index f3852d00ac8..fad8c2f021a 100644 --- a/src/third_party/wiredtiger/dist/s_string.ok +++ b/src/third_party/wiredtiger/dist/s_string.ok @@ -807,6 +807,7 @@ intl intnum intpack intptr +intr intrin inuse io @@ -1060,6 +1061,7 @@ rebalancing recno recnos reconfig +reconfigures reconfiguring recsize rectype diff --git a/src/third_party/wiredtiger/dist/stat_data.py b/src/third_party/wiredtiger/dist/stat_data.py index 512892eb44d..b66e95ce49b 100644 --- a/src/third_party/wiredtiger/dist/stat_data.py +++ b/src/third_party/wiredtiger/dist/stat_data.py @@ -431,11 +431,19 @@ connection_stats = [ ########################################## YieldStat('application_cache_time', 'application thread time waiting for cache (usecs)'), YieldStat('application_evict_time', 'application thread time evicting (usecs)'), + YieldStat('child_modify_blocked_page', 'page reconciliation yielded due to child modification'), + YieldStat('conn_close_blocked_lsm', 'connection close yielded for lsm manager shutdown'), + YieldStat('dhandle_lock_blocked', 'data handle lock yielded'), + YieldStat('log_server_sync_blocked', 'log server sync yielded for log write'), YieldStat('page_busy_blocked', 'page acquire busy blocked'), + YieldStat('page_del_rollback_blocked', 'page delete rollback yielded for instantiation'), YieldStat('page_forcible_evict_blocked', 'page acquire eviction blocked'), + YieldStat('page_index_slot_blocked', 'reference for page index and slot yielded'), YieldStat('page_locked_blocked', 'page acquire locked blocked'), YieldStat('page_read_blocked', 'page acquire read blocked'), YieldStat('page_sleep', 'page acquire time sleeping (usecs)'), + YieldStat('tree_descend_blocked', 'tree descend one level yielded for split page index update'), + YieldStat('txn_release_blocked', 'connection close blocked waiting for transaction state stabilization'), ] connection_stats = sorted(connection_stats, key=attrgetter('desc')) diff --git a/src/third_party/wiredtiger/import.data b/src/third_party/wiredtiger/import.data index 03d5746ffde..e867147e59e 100644 --- a/src/third_party/wiredtiger/import.data +++ b/src/third_party/wiredtiger/import.data @@ -1,5 +1,5 @@ { - "commit": "d2f2eae6d7718a53ac5bacf7141773ee0696f3c6", + "commit": "0b36171f4aa0bea6ab6118b0d3bcf6329a24939e", "github": "wiredtiger/wiredtiger.git", "vendor": "wiredtiger", "branch": "mongodb-3.4" diff --git a/src/third_party/wiredtiger/src/async/async_worker.c b/src/third_party/wiredtiger/src/async/async_worker.c index 11f59ed14f1..89877652f1e 100644 --- a/src/third_party/wiredtiger/src/async/async_worker.c +++ b/src/third_party/wiredtiger/src/async/async_worker.c @@ -301,11 +301,10 @@ __wt_async_worker(void *arg) WT_ERR(__async_op_dequeue(conn, session, &op)); if (op != NULL && op != &async->flush_op) { /* - * If an operation fails, we want the worker thread to - * keep running, unless there is a panic. + * Operation failure doesn't cause the worker thread to + * exit. */ (void)__async_worker_op(session, op, &worker); - WT_ERR(WT_SESSION_CHECK_PANIC(session)); } else if (async->flush_state == WT_ASYNC_FLUSHING) { /* * Worker flushing going on. Last worker to the party diff --git a/src/third_party/wiredtiger/src/btree/bt_cursor.c b/src/third_party/wiredtiger/src/btree/bt_cursor.c index f0aa632551b..28977eee9f5 100644 --- a/src/third_party/wiredtiger/src/btree/bt_cursor.c +++ b/src/third_party/wiredtiger/src/btree/bt_cursor.c @@ -52,15 +52,18 @@ __cursor_state_restore(WT_CURSOR *cursor, WT_CURFILE_STATE *state) /* * __cursor_page_pinned -- - * Return if we have a page pinned and it's not been flagged for forced - * eviction (the forced eviction test is so we periodically release pages - * grown too large). + * Return if we have a page pinned. */ static inline bool -__cursor_page_pinned(WT_CURSOR_BTREE *cbt) +__cursor_page_pinned(WT_CURSOR_BTREE *cbt, bool eviction_ok) { + /* + * Optionally fail the page-pinned test when the page is flagged for + * forced eviction (so we periodically release pages grown too large). + * The test is optional as not all callers can release pinned pages. + */ return (F_ISSET(cbt, WT_CBT_ACTIVE) && - cbt->ref->page->read_gen != WT_READGEN_OLDEST); + (!eviction_ok || cbt->ref->page->read_gen != WT_READGEN_OLDEST)); } /* @@ -156,8 +159,10 @@ __cursor_disable_bulk(WT_SESSION_IMPL *session, WT_BTREE *btree) * into a tree. Eviction is disabled when an empty tree is opened, and * it must only be enabled once. */ - if (__wt_atomic_cas8(&btree->original, 1, 0)) + if (__wt_atomic_cas8(&btree->original, 1, 0)) { + btree->evict_disabled_open = false; __wt_evict_file_exclusive_off(session); + } } /* @@ -443,7 +448,7 @@ __wt_btcur_search(WT_CURSOR_BTREE *cbt) * from the root. */ valid = false; - if (__cursor_page_pinned(cbt)) { + if (__cursor_page_pinned(cbt, true)) { __wt_txn_cursor_op(session); WT_ERR(btree->type == BTREE_ROW ? @@ -535,7 +540,7 @@ __wt_btcur_search_near(WT_CURSOR_BTREE *cbt, int *exactp) * existing record. */ valid = false; - if (btree->type == BTREE_ROW && __cursor_page_pinned(cbt)) { + if (btree->type == BTREE_ROW && __cursor_page_pinned(cbt, true)) { __wt_txn_cursor_op(session); WT_ERR(__cursor_row_search(session, cbt, cbt->ref, true)); @@ -665,7 +670,7 @@ __wt_btcur_insert(WT_CURSOR_BTREE *cbt) * configured for append aren't included, regardless of whether or not * they meet all other criteria. */ - if (__cursor_page_pinned(cbt) && + if (__cursor_page_pinned(cbt, true) && F_ISSET_ALL(cursor, WT_CURSTD_KEY_INT | WT_CURSTD_OVERWRITE) && !append_key) { WT_ERR(__wt_txn_autocommit_check(session)); @@ -880,8 +885,22 @@ __wt_btcur_remove(WT_CURSOR_BTREE *cbt) * removed, and the record must exist with a positioned cursor. The * cursor won't be positioned on a page with an external key set, but * be sure. + * + * There's trickiness in the page-pinned check. By definition a remove + * operation leaves a cursor positioned if it's initially positioned. + * However, if every item on the page is deleted and we unpin the page, + * eviction might delete the page and our search will re-instantiate an + * empty page for us. Cursor remove returns not-found whether or not + * that eviction/deletion happens and it's OK unless cursor-overwrite + * is configured (which means we return success even if there's no item + * to delete). In that case, we'll fail when we try to point the cursor + * at the key on the page to satisfy the positioned requirement. It's + * arguably safe to simply leave the key initialized in the cursor (as + * that's all a positioned cursor implies), but it's probably safer to + * avoid page eviction entirely in the positioned case. */ - if (__cursor_page_pinned(cbt) && F_ISSET(cursor, WT_CURSTD_KEY_INT)) { + if (__cursor_page_pinned(cbt, !positioned) && + F_ISSET(cursor, WT_CURSTD_KEY_INT)) { WT_ERR(__wt_txn_autocommit_check(session)); /* @@ -1024,7 +1043,8 @@ __wt_btcur_update(WT_CURSOR_BTREE *cbt) * cursor won't be positioned on a page with an external key set, but * be sure. */ - if (__cursor_page_pinned(cbt) && F_ISSET(cursor, WT_CURSTD_KEY_INT)) { + if (__cursor_page_pinned(cbt, true) && + F_ISSET(cursor, WT_CURSTD_KEY_INT)) { WT_ERR(__wt_txn_autocommit_check(session)); /* * The cursor position may not be exact (the cursor's comparison diff --git a/src/third_party/wiredtiger/src/btree/bt_delete.c b/src/third_party/wiredtiger/src/btree/bt_delete.c index b55ad291c5e..5c4625044d3 100644 --- a/src/third_party/wiredtiger/src/btree/bt_delete.c +++ b/src/third_party/wiredtiger/src/btree/bt_delete.c @@ -153,6 +153,7 @@ void __wt_delete_page_rollback(WT_SESSION_IMPL *session, WT_REF *ref) { WT_UPDATE **upd; + uint64_t yield_count; /* * If the page is still "deleted", it's as we left it, reset the state @@ -160,7 +161,7 @@ __wt_delete_page_rollback(WT_SESSION_IMPL *session, WT_REF *ref) * instantiated or being instantiated. Loop because it's possible for * the page to return to the deleted state if instantiation fails. */ - for (;; __wt_yield()) + for (yield_count = 0;; yield_count++, __wt_yield()) switch (ref->state) { case WT_REF_DISK: case WT_REF_READING: @@ -173,7 +174,7 @@ __wt_delete_page_rollback(WT_SESSION_IMPL *session, WT_REF *ref) */ if (__wt_atomic_casv32( &ref->state, WT_REF_DELETED, WT_REF_DISK)) - return; + goto done; break; case WT_REF_LOCKED: /* @@ -203,8 +204,10 @@ __wt_delete_page_rollback(WT_SESSION_IMPL *session, WT_REF *ref) */ __wt_free(session, ref->page_del->update_list); __wt_free(session, ref->page_del); - return; + goto done; } + +done: WT_STAT_CONN_INCRV(session, page_del_rollback_blocked, yield_count); } /* diff --git a/src/third_party/wiredtiger/src/btree/bt_handle.c b/src/third_party/wiredtiger/src/btree/bt_handle.c index a0da7df0998..8637a8cd751 100644 --- a/src/third_party/wiredtiger/src/btree/bt_handle.c +++ b/src/third_party/wiredtiger/src/btree/bt_handle.c @@ -66,7 +66,6 @@ __wt_btree_open(WT_SESSION_IMPL *session, const char *op_cfg[]) WT_DATA_HANDLE *dhandle; WT_DECL_RET; size_t root_addr_size; - uint32_t mask; uint8_t root_addr[WT_BTREE_MAX_ADDR_COOKIE]; const char *filename; bool creation, forced_salvage, readonly; @@ -75,15 +74,14 @@ __wt_btree_open(WT_SESSION_IMPL *session, const char *op_cfg[]) dhandle = session->dhandle; /* - * This may be a re-open of an underlying object and we have to clean - * up. We can't clear the operation flags, however, they're set by the - * connection handle software that called us. + * This may be a re-open, clean up the btree structure. + * Clear the fields that don't persist across a re-open. + * Clear all flags other than the operation flags (which are set by the + * connection handle software that called us). */ WT_RET(__btree_clear(session)); - - mask = F_MASK(btree, WT_BTREE_SPECIAL_FLAGS); - memset(btree, 0, sizeof(*btree)); - btree->flags = mask; + memset(btree, 0, WT_BTREE_CLEAR_SIZE); + F_CLR(btree, ~WT_BTREE_SPECIAL_FLAGS); /* Set the data handle first, our called functions reasonably use it. */ btree->dhandle = dhandle; @@ -185,13 +183,19 @@ __wt_btree_open(WT_SESSION_IMPL *session, const char *op_cfg[]) * * Files that can still be bulk-loaded cannot be evicted. * Permanently cache-resident files can never be evicted. - * Special operations don't enable eviction. (The underlying commands - * may turn on eviction, but it's their decision.) + * Special operations don't enable eviction. The underlying commands may + * turn on eviction (for example, verify turns on eviction while working + * a file to keep from consuming the cache), but it's their decision. If + * an underlying command reconfigures eviction, it must either clear the + * evict-disabled-open flag or restore the eviction configuration when + * finished so that handle close behaves correctly. */ if (btree->original || F_ISSET(btree, WT_BTREE_IN_MEMORY | WT_BTREE_REBALANCE | - WT_BTREE_SALVAGE | WT_BTREE_UPGRADE | WT_BTREE_VERIFY)) + WT_BTREE_SALVAGE | WT_BTREE_UPGRADE | WT_BTREE_VERIFY)) { WT_ERR(__wt_evict_file_exclusive_on(session)); + btree->evict_disabled_open = true; + } if (0) { err: WT_TRET(__wt_btree_close(session)); @@ -228,6 +232,15 @@ __wt_btree_close(WT_SESSION_IMPL *session) return (0); F_SET(btree, WT_BTREE_CLOSED); + /* + * If we turned eviction off and never turned it back on, do that now, + * otherwise the counter will be off. + */ + if (btree->evict_disabled_open) { + btree->evict_disabled_open = false; + __wt_evict_file_exclusive_off(session); + } + /* Discard any underlying block manager resources. */ if ((bm = btree->bm) != NULL) { btree->bm = NULL; @@ -447,9 +460,11 @@ __btree_conf(WT_SESSION_IMPL *session, WT_CKPT *ckpt) WT_RET(__wt_rwlock_init(session, &btree->ovfl_lock)); WT_RET(__wt_spin_init(session, &btree->flush_lock, "btree flush")); - btree->checkpointing = WT_CKPT_OFF; /* Not checkpointing */ btree->modified = false; /* Clean */ - btree->write_gen = ckpt->write_gen; /* Write generation */ + + btree->checkpointing = WT_CKPT_OFF; /* Not checkpointing */ + btree->write_gen = ckpt->write_gen; /* Write generation */ + btree->checkpoint_gen = S2C(session)->txn_global.checkpoint_gen; return (0); } diff --git a/src/third_party/wiredtiger/src/btree/bt_random.c b/src/third_party/wiredtiger/src/btree/bt_random.c index c5948ec4ab5..b4f05c440ba 100644 --- a/src/third_party/wiredtiger/src/btree/bt_random.c +++ b/src/third_party/wiredtiger/src/btree/bt_random.c @@ -395,8 +395,7 @@ __wt_btcur_next_random(WT_CURSOR_BTREE *cbt) */ for (skip = cbt->next_random_leaf_skip; cbt->ref == NULL || skip > 0;) { n = skip; - WT_ERR(__wt_tree_walk_skip(session, &cbt->ref, &skip, - WT_READ_NO_GEN | WT_READ_SKIP_INTL | WT_READ_WONT_NEED)); + WT_ERR(__wt_tree_walk_skip(session, &cbt->ref, &skip)); if (n == skip) { if (skip == 0) break; diff --git a/src/third_party/wiredtiger/src/btree/bt_walk.c b/src/third_party/wiredtiger/src/btree/bt_walk.c index 86484feb7c9..c22b99c55d0 100644 --- a/src/third_party/wiredtiger/src/btree/bt_walk.c +++ b/src/third_party/wiredtiger/src/btree/bt_walk.c @@ -18,9 +18,16 @@ __ref_index_slot(WT_SESSION_IMPL *session, { WT_PAGE_INDEX *pindex; WT_REF **start, **stop, **p, **t; + uint64_t yield_count; uint32_t entries, slot; - for (;;) { + /* + * If we don't find our reference, the page split and our home + * pointer references the wrong page. When internal pages + * split, their WT_REF structure home values are updated; yield + * and wait for that to happen. + */ + for (yield_count = 0;; yield_count++, __wt_yield()) { /* * Copy the parent page's index value: the page can split at * any time, but the index's value is always valid, even if @@ -59,18 +66,13 @@ __ref_index_slot(WT_SESSION_IMPL *session, } } - /* - * If we don't find our reference, the page split and our home - * pointer references the wrong page. When internal pages - * split, their WT_REF structure home values are updated; yield - * and wait for that to happen. - */ - __wt_yield(); } found: WT_ASSERT(session, pindex->index[slot] == ref); *pindexp = pindex; *slotp = slot; + + WT_STAT_CONN_INCRV(session, page_index_slot_blocked, yield_count); } /* @@ -177,12 +179,13 @@ __ref_descend_prev( WT_SESSION_IMPL *session, WT_REF *ref, WT_PAGE_INDEX **pindexp) { WT_PAGE_INDEX *pindex; + uint64_t yield_count; /* * We're passed a child page into which we're descending, and on which * we have a hazard pointer. */ - for (;; __wt_yield()) { + for (yield_count = 0;; yield_count++, __wt_yield()) { /* * There's a split race when a cursor moving backwards through * the tree descends the tree. If we're splitting an internal @@ -242,6 +245,7 @@ __ref_descend_prev( break; } *pindexp = pindex; + WT_STAT_CONN_INCRV(session, tree_descend_blocked, yield_count); } /* @@ -497,29 +501,21 @@ restart: /* } /* - * Optionally skip leaf pages: skip all leaf pages if - * WT_READ_SKIP_LEAF is set, when the skip-leaf-count - * variable is non-zero, skip some count of leaf pages. - * If this page is disk-based, crack the cell to figure - * out it's a leaf page without reading it. + * Optionally skip leaf pages: when the skip-leaf-count + * variable is non-zero, skip some count of leaf pages, + * then take the next leaf page we can. * - * If skipping some number of leaf pages, decrement the - * count of pages to zero, and then take the next leaf - * page we can. Be cautious around the page decrement, - * if for some reason don't take this particular page, - * we can take the next one, and, there are additional - * tests/decrements when we're about to return a leaf - * page. + * The reason to do some of this work here (rather than + * in our caller), is because we can look at the cell + * and know it's a leaf page without reading it into + * memory. If this page is disk-based, crack the cell + * to figure out it's a leaf page without reading it. */ - if (skipleafcntp != NULL || LF_ISSET(WT_READ_SKIP_LEAF)) - if (__ref_is_leaf(ref)) { - if (LF_ISSET(WT_READ_SKIP_LEAF)) - break; - if (*skipleafcntp > 0) { - --*skipleafcntp; - break; - } - } + if (skipleafcntp != NULL && + *skipleafcntp > 0 && __ref_is_leaf(ref)) { + --*skipleafcntp; + break; + } ret = __wt_page_swap(session, couple, ref, WT_READ_NOTFOUND_OK | WT_READ_RESTART_OK | flags); @@ -626,34 +622,18 @@ descend: empty_internal = true; session, ref, &pindex); slot = pindex->entries - 1; } - } else { - /* - * At the lowest tree level (considering a leaf - * page), turn off the initial-descent state. - * Descent race tests are different when moving - * through the tree vs. the initial descent. - */ - initial_descent = false; - - /* - * Optionally skip leaf pages, the second half. - * We didn't have an on-page cell to figure out - * if it was a leaf page, we had to acquire the - * hazard pointer and look at the page. - */ - if (skipleafcntp != NULL || - LF_ISSET(WT_READ_SKIP_LEAF)) { - if (LF_ISSET(WT_READ_SKIP_LEAF)) - break; - if (*skipleafcntp > 0) { - --*skipleafcntp; - break; - } - } - - *refp = ref; - goto done; + continue; } + + /* + * The tree-walk restart code knows we return any leaf + * page we acquire (never hazard-pointer coupling on + * after acquiring a leaf page), and asserts no restart + * happens while holding a leaf page. This page must be + * returned to our caller. + */ + *refp = ref; + goto done; } } @@ -690,8 +670,29 @@ __wt_tree_walk_count(WT_SESSION_IMPL *session, * of leaf pages before returning. */ int -__wt_tree_walk_skip(WT_SESSION_IMPL *session, - WT_REF **refp, uint64_t *skipleafcntp, uint32_t flags) +__wt_tree_walk_skip( + WT_SESSION_IMPL *session, WT_REF **refp, uint64_t *skipleafcntp) { - return (__tree_walk_internal(session, refp, NULL, skipleafcntp, flags)); + /* + * Optionally skip leaf pages, the second half. The tree-walk function + * didn't have an on-page cell it could use to figure out if the page + * was a leaf page or not, it had to acquire the hazard pointer and look + * at the page. The tree-walk code never acquires a hazard pointer on a + * leaf page without returning it, and it's not trivial to change that. + * So, the tree-walk code returns all leaf pages here and we deal with + * decrementing the count. + */ + do { + WT_RET(__tree_walk_internal(session, refp, NULL, skipleafcntp, + WT_READ_NO_GEN | WT_READ_SKIP_INTL | WT_READ_WONT_NEED)); + + /* + * The walk skipped internal pages, any page returned must be a + * leaf page. + */ + if (*skipleafcntp > 0) + --*skipleafcntp; + } while (*skipleafcntp > 0); + + return (0); } diff --git a/src/third_party/wiredtiger/src/config/config_def.c b/src/third_party/wiredtiger/src/config/config_def.c index f152fbacad4..c8a8c525751 100644 --- a/src/third_party/wiredtiger/src/config/config_def.c +++ b/src/third_party/wiredtiger/src/config/config_def.c @@ -733,6 +733,7 @@ static const WT_CONFIG_CHECK confchk_wiredtiger_open[] = { { "readonly", "boolean", NULL, NULL, NULL, 0 }, { "session_max", "int", NULL, "min=1", NULL, 0 }, { "session_scratch_max", "int", NULL, NULL, NULL, 0 }, + { "session_table_cache", "boolean", NULL, NULL, NULL, 0 }, { "shared_cache", "category", NULL, NULL, confchk_wiredtiger_open_shared_cache_subconfigs, 5 }, @@ -820,6 +821,7 @@ static const WT_CONFIG_CHECK confchk_wiredtiger_open_all[] = { { "readonly", "boolean", NULL, NULL, NULL, 0 }, { "session_max", "int", NULL, "min=1", NULL, 0 }, { "session_scratch_max", "int", NULL, NULL, NULL, 0 }, + { "session_table_cache", "boolean", NULL, NULL, NULL, 0 }, { "shared_cache", "category", NULL, NULL, confchk_wiredtiger_open_shared_cache_subconfigs, 5 }, @@ -904,6 +906,7 @@ static const WT_CONFIG_CHECK confchk_wiredtiger_open_basecfg[] = { { "readonly", "boolean", NULL, NULL, NULL, 0 }, { "session_max", "int", NULL, "min=1", NULL, 0 }, { "session_scratch_max", "int", NULL, NULL, NULL, 0 }, + { "session_table_cache", "boolean", NULL, NULL, NULL, 0 }, { "shared_cache", "category", NULL, NULL, confchk_wiredtiger_open_shared_cache_subconfigs, 5 }, @@ -986,6 +989,7 @@ static const WT_CONFIG_CHECK confchk_wiredtiger_open_usercfg[] = { { "readonly", "boolean", NULL, NULL, NULL, 0 }, { "session_max", "int", NULL, "min=1", NULL, 0 }, { "session_scratch_max", "int", NULL, NULL, NULL, 0 }, + { "session_table_cache", "boolean", NULL, NULL, NULL, 0 }, { "shared_cache", "category", NULL, NULL, confchk_wiredtiger_open_shared_cache_subconfigs, 5 }, @@ -1276,14 +1280,14 @@ static const WT_CONFIG_ENTRY config_entries[] = { "file_max=100MB,path=\".\",prealloc=true,recover=on," "zero_fill=false),lsm_manager=(merge=true,worker_thread_max=4)," "lsm_merge=true,mmap=true,multiprocess=false,readonly=false," - "session_max=100,session_scratch_max=2MB,shared_cache=(chunk=10MB" - ",name=,quota=0,reserve=0,size=500MB),statistics=none," - "statistics_log=(json=false,on_close=false,path=\".\",sources=," - "timestamp=\"%b %d %H:%M:%S\",wait=0)," + "session_max=100,session_scratch_max=2MB,session_table_cache=true" + ",shared_cache=(chunk=10MB,name=,quota=0,reserve=0,size=500MB)," + "statistics=none,statistics_log=(json=false,on_close=false," + "path=\".\",sources=,timestamp=\"%b %d %H:%M:%S\",wait=0)," "transaction_sync=(enabled=false,method=fsync)," "use_environment=true,use_environment_priv=false,verbose=," "write_through=", - confchk_wiredtiger_open, 40 + confchk_wiredtiger_open, 41 }, { "wiredtiger_open_all", "async=(enabled=false,ops_max=1024,threads=2),buffer_alignment=-1" @@ -1300,14 +1304,14 @@ static const WT_CONFIG_ENTRY config_entries[] = { "file_max=100MB,path=\".\",prealloc=true,recover=on," "zero_fill=false),lsm_manager=(merge=true,worker_thread_max=4)," "lsm_merge=true,mmap=true,multiprocess=false,readonly=false," - "session_max=100,session_scratch_max=2MB,shared_cache=(chunk=10MB" - ",name=,quota=0,reserve=0,size=500MB),statistics=none," - "statistics_log=(json=false,on_close=false,path=\".\",sources=," - "timestamp=\"%b %d %H:%M:%S\",wait=0)," + "session_max=100,session_scratch_max=2MB,session_table_cache=true" + ",shared_cache=(chunk=10MB,name=,quota=0,reserve=0,size=500MB)," + "statistics=none,statistics_log=(json=false,on_close=false," + "path=\".\",sources=,timestamp=\"%b %d %H:%M:%S\",wait=0)," "transaction_sync=(enabled=false,method=fsync)," "use_environment=true,use_environment_priv=false,verbose=," "version=(major=0,minor=0),write_through=", - confchk_wiredtiger_open_all, 41 + confchk_wiredtiger_open_all, 42 }, { "wiredtiger_open_basecfg", "async=(enabled=false,ops_max=1024,threads=2),buffer_alignment=-1" @@ -1323,12 +1327,13 @@ static const WT_CONFIG_ENTRY config_entries[] = { "path=\".\",prealloc=true,recover=on,zero_fill=false)," "lsm_manager=(merge=true,worker_thread_max=4),lsm_merge=true," "mmap=true,multiprocess=false,readonly=false,session_max=100," - "session_scratch_max=2MB,shared_cache=(chunk=10MB,name=,quota=0," - "reserve=0,size=500MB),statistics=none,statistics_log=(json=false" - ",on_close=false,path=\".\",sources=,timestamp=\"%b %d %H:%M:%S\"" - ",wait=0),transaction_sync=(enabled=false,method=fsync),verbose=," + "session_scratch_max=2MB,session_table_cache=true," + "shared_cache=(chunk=10MB,name=,quota=0,reserve=0,size=500MB)," + "statistics=none,statistics_log=(json=false,on_close=false," + "path=\".\",sources=,timestamp=\"%b %d %H:%M:%S\",wait=0)," + "transaction_sync=(enabled=false,method=fsync),verbose=," "version=(major=0,minor=0),write_through=", - confchk_wiredtiger_open_basecfg, 35 + confchk_wiredtiger_open_basecfg, 36 }, { "wiredtiger_open_usercfg", "async=(enabled=false,ops_max=1024,threads=2),buffer_alignment=-1" @@ -1344,12 +1349,13 @@ static const WT_CONFIG_ENTRY config_entries[] = { "path=\".\",prealloc=true,recover=on,zero_fill=false)," "lsm_manager=(merge=true,worker_thread_max=4),lsm_merge=true," "mmap=true,multiprocess=false,readonly=false,session_max=100," - "session_scratch_max=2MB,shared_cache=(chunk=10MB,name=,quota=0," - "reserve=0,size=500MB),statistics=none,statistics_log=(json=false" - ",on_close=false,path=\".\",sources=,timestamp=\"%b %d %H:%M:%S\"" - ",wait=0),transaction_sync=(enabled=false,method=fsync),verbose=," + "session_scratch_max=2MB,session_table_cache=true," + "shared_cache=(chunk=10MB,name=,quota=0,reserve=0,size=500MB)," + "statistics=none,statistics_log=(json=false,on_close=false," + "path=\".\",sources=,timestamp=\"%b %d %H:%M:%S\",wait=0)," + "transaction_sync=(enabled=false,method=fsync),verbose=," "write_through=", - confchk_wiredtiger_open_usercfg, 34 + confchk_wiredtiger_open_usercfg, 35 }, { NULL, NULL, NULL, 0 } }; diff --git a/src/third_party/wiredtiger/src/conn/conn_api.c b/src/third_party/wiredtiger/src/conn/conn_api.c index 68d45678965..103e4a68f04 100644 --- a/src/third_party/wiredtiger/src/conn/conn_api.c +++ b/src/third_party/wiredtiger/src/conn/conn_api.c @@ -1086,6 +1086,41 @@ err: /* WT_TRET(wt_session->close(wt_session, config)); } + /* + * Perform a system-wide checkpoint so that all tables are consistent + * with each other. Do this before shutting down all the subsystems. + * We have shut down all user sessions, but send in true for waiting + * for internal races. + */ + if (!F_ISSET(conn, WT_CONN_IN_MEMORY | WT_CONN_READONLY)) { + s = NULL; + WT_TRET(__wt_open_internal_session( + conn, "close_ckpt", true, 0, &s)); + if (s != NULL) { + const char *checkpoint_cfg[] = { + WT_CONFIG_BASE(session, WT_SESSION_checkpoint), + NULL + }; + wt_session = &s->iface; + WT_TRET(__wt_txn_checkpoint(s, checkpoint_cfg, true)); + + /* + * Mark the metadata dirty so we flush it on close, + * allowing recovery to be skipped. + */ + WT_WITH_DHANDLE(s, WT_SESSION_META_DHANDLE(s), + __wt_tree_modify_set(s)); + + WT_TRET(wt_session->close(wt_session, config)); + } + } + + if (ret != 0) { + __wt_err(session, ret, + "failure during close, disabling further writes"); + F_SET(conn, WT_CONN_PANIC); + } + WT_TRET(__wt_connection_close(conn)); /* We no longer have a session, don't try to update it. */ @@ -2185,6 +2220,9 @@ wiredtiger_open(const char *home, WT_EVENT_HANDLER *event_handler, WT_ERR(__wt_config_gets(session, cfg, "readonly", &cval)); if (cval.val) F_SET(conn, WT_CONN_READONLY); + WT_ERR(__wt_config_gets(session, cfg, "session_table_cache", &cval)); + if (cval.val) + F_SET(conn, WT_CONN_TABLE_CACHE); /* Configure error messages so we get them right early. */ WT_ERR(__wt_config_gets(session, cfg, "error_prefix", &cval)); @@ -2475,8 +2513,15 @@ err: /* Discard the scratch buffers. */ __wt_scr_discard(session); __wt_scr_discard(&conn->dummy_session); - if (ret != 0) + if (ret != 0) { + /* + * Set panic if we're returning the run recovery error so that + * we don't try to checkpoint data handles. + */ + if (ret == WT_RUN_RECOVERY) + F_SET(conn, WT_CONN_PANIC); WT_TRET(__wt_connection_close(conn)); + } return (ret); } diff --git a/src/third_party/wiredtiger/src/conn/conn_dhandle.c b/src/third_party/wiredtiger/src/conn/conn_dhandle.c index 1816e66b0b7..181f12ab2dd 100644 --- a/src/third_party/wiredtiger/src/conn/conn_dhandle.c +++ b/src/third_party/wiredtiger/src/conn/conn_dhandle.c @@ -317,6 +317,9 @@ __wt_conn_btree_open( WT_ASSERT(session, !F_ISSET(S2C(session), WT_CONN_CLOSING_NO_MORE_OPENS)); + /* Turn off eviction. */ + WT_RET(__wt_evict_file_exclusive_on(session)); + /* * If the handle is already open, it has to be closed so it can be * reopened with a new configuration. @@ -330,11 +333,11 @@ __wt_conn_btree_open( * in the tree that can block the close. */ if (F_ISSET(dhandle, WT_DHANDLE_OPEN)) - WT_RET(__wt_conn_btree_sync_and_close(session, false, false)); + WT_ERR(__wt_conn_btree_sync_and_close(session, false, false)); /* Discard any previous configuration, set up the new configuration. */ __conn_btree_config_clear(session); - WT_RET(__conn_btree_config_set(session)); + WT_ERR(__conn_btree_config_set(session)); /* Set any special flags on the handle. */ F_SET(btree, LF_MASK(WT_BTREE_SPECIAL_FLAGS)); @@ -374,6 +377,8 @@ __wt_conn_btree_open( err: F_CLR(btree, WT_BTREE_SPECIAL_FLAGS); } + __wt_evict_file_exclusive_off(session); + return (ret); } @@ -577,7 +582,7 @@ __conn_dhandle_remove(WT_SESSION_IMPL *session, bool final) WT_ASSERT(session, F_ISSET(session, WT_SESSION_LOCKED_HANDLE_LIST_WRITE)); - WT_ASSERT(session, dhandle != conn->cache->evict_file_next); + WT_ASSERT(session, dhandle != conn->cache->walk_tree); /* Check if the handle was reacquired by a session while we waited. */ if (!final && @@ -673,8 +678,8 @@ restart: continue; WT_WITH_DHANDLE(session, dhandle, - WT_TRET(__wt_conn_dhandle_discard_single( - session, true, F_ISSET(conn, WT_CONN_IN_MEMORY)))); + WT_TRET(__wt_conn_dhandle_discard_single(session, true, + F_ISSET(conn, WT_CONN_IN_MEMORY | WT_CONN_PANIC)))); goto restart; } @@ -699,8 +704,8 @@ restart: /* Close the metadata file handle. */ while ((dhandle = TAILQ_FIRST(&conn->dhqh)) != NULL) WT_WITH_DHANDLE(session, dhandle, - WT_TRET(__wt_conn_dhandle_discard_single( - session, true, F_ISSET(conn, WT_CONN_IN_MEMORY)))); + WT_TRET(__wt_conn_dhandle_discard_single(session, true, + F_ISSET(conn, WT_CONN_IN_MEMORY | WT_CONN_PANIC)))); return (ret); } diff --git a/src/third_party/wiredtiger/src/conn/conn_log.c b/src/third_party/wiredtiger/src/conn/conn_log.c index d2ed314fd2e..8b47d3b04c0 100644 --- a/src/third_party/wiredtiger/src/conn/conn_log.c +++ b/src/third_party/wiredtiger/src/conn/conn_log.c @@ -375,6 +375,7 @@ __log_file_server(void *arg) WT_LOG *log; WT_LSN close_end_lsn, min_lsn; WT_SESSION_IMPL *session; + uint64_t yield_count; uint32_t filenum; bool locked; @@ -382,6 +383,7 @@ __log_file_server(void *arg) conn = S2C(session); log = conn->log; locked = false; + yield_count = 0; while (F_ISSET(conn, WT_CONN_SERVER_LOG)) { /* * If there is a log file to close, make sure any outstanding @@ -512,6 +514,7 @@ __log_file_server(void *arg) * thread a chance to run and try again in * this case. */ + yield_count++; __wt_yield(); continue; } @@ -522,8 +525,9 @@ __log_file_server(void *arg) } if (0) { -err: __wt_err(session, ret, "log close server error"); +err: WT_PANIC_MSG(session, ret, "log close server error"); } + WT_STAT_CONN_INCRV(session, log_server_sync_blocked, yield_count); if (locked) __wt_spin_unlock(session, &log->log_sync_lock); return (WT_THREAD_RET_VALUE); @@ -740,7 +744,8 @@ __log_wrlsn_server(void *arg) WT_ERR(__wt_log_force_write(session, 1, NULL)); __wt_log_wrlsn(session, NULL); if (0) { -err: __wt_err(session, ret, "log wrlsn server error"); +err: WT_PANIC_MSG(session, ret, "log wrlsn server error"); + } return (WT_THREAD_RET_VALUE); } @@ -757,7 +762,7 @@ __log_server(void *arg) WT_DECL_RET; WT_LOG *log; WT_SESSION_IMPL *session; - uint64_t timediff; + uint64_t retry, timediff; bool did_work, signalled; session = arg; @@ -783,6 +788,7 @@ __log_server(void *arg) * takes to sync out an earlier file. */ did_work = true; + retry = 0; while (F_ISSET(conn, WT_CONN_SERVER_LOG)) { /* * Slots depend on future activity. Force out buffered @@ -827,7 +833,24 @@ __log_server(void *arg) ret = __log_archive_once(session, 0); __wt_writeunlock( session, &log->log_archive_lock); - WT_ERR(ret); + /* + * It is possible that an external + * process on some systems may prevent + * removal. If we get a permission + * error, retry a few times. + */ + if (ret == EACCES && + retry < WT_RETRY_MAX) { + retry++; + ret = 0; + } else { + /* + * Return the error if there is + * one or reset on success. + */ + WT_ERR(ret); + retry = 0; + } } else __wt_verbose(session, WT_VERB_LOG, "log_archive: Blocked due to open " @@ -844,7 +867,7 @@ __log_server(void *arg) } if (0) { -err: __wt_err(session, ret, "log server error"); +err: WT_PANIC_MSG(session, ret, "log server error"); } return (WT_THREAD_RET_VALUE); } @@ -902,7 +925,7 @@ __wt_logmgr_create(WT_SESSION_IMPL *session, const char *cfg[]) WT_RET(__wt_cond_alloc(session, "log sync", &log->log_sync_cond)); WT_RET(__wt_cond_alloc(session, "log write", &log->log_write_cond)); WT_RET(__wt_log_open(session)); - WT_RET(__wt_log_slot_init(session)); + WT_RET(__wt_log_slot_init(session, true)); return (0); } diff --git a/src/third_party/wiredtiger/src/conn/conn_open.c b/src/third_party/wiredtiger/src/conn/conn_open.c index eb3c79422a0..649bfa7c81f 100644 --- a/src/third_party/wiredtiger/src/conn/conn_open.c +++ b/src/third_party/wiredtiger/src/conn/conn_open.c @@ -91,6 +91,7 @@ __wt_connection_close(WT_CONNECTION_IMPL *conn) if (txn_global->oldest_id == txn_global->current && txn_global->metadata_pinned == txn_global->current) break; + WT_STAT_CONN_INCR(session, txn_release_blocked); __wt_yield(); } @@ -143,7 +144,7 @@ __wt_connection_close(WT_CONNECTION_IMPL *conn) * conditional because we allocate the log path so that printlog can * run without running logging or recovery. */ - if (FLD_ISSET(conn->log_flags, WT_CONN_LOG_ENABLED) && + if (ret == 0 && FLD_ISSET(conn->log_flags, WT_CONN_LOG_ENABLED) && FLD_ISSET(conn->log_flags, WT_CONN_LOG_RECOVER_DONE)) WT_TRET(__wt_txn_checkpoint_log( session, true, WT_TXN_LOG_CKPT_STOP, NULL)); diff --git a/src/third_party/wiredtiger/src/cursor/cur_index.c b/src/third_party/wiredtiger/src/cursor/cur_index.c index 6fc01c0421f..15c1271c992 100644 --- a/src/third_party/wiredtiger/src/cursor/cur_index.c +++ b/src/third_party/wiredtiger/src/cursor/cur_index.c @@ -382,7 +382,7 @@ __curindex_close(WT_CURSOR *cursor) if (cindex->child != NULL) WT_TRET(cindex->child->close(cindex->child)); - __wt_schema_release_table(session, cindex->table); + WT_TRET(__wt_schema_release_table(session, cindex->table)); /* The URI is owned by the index. */ cursor->internal_uri = NULL; WT_TRET(__wt_cursor_close(cursor)); @@ -485,7 +485,7 @@ __wt_curindex_open(WT_SESSION_IMPL *session, if ((ret = __wt_schema_open_index( session, table, idxname, namesize, &idx)) != 0) { - __wt_schema_release_table(session, table); + WT_TRET(__wt_schema_release_table(session, table)); return (ret); } WT_RET(__wt_calloc_one(session, &cindex)); diff --git a/src/third_party/wiredtiger/src/cursor/cur_join.c b/src/third_party/wiredtiger/src/cursor/cur_join.c index 80afaf798dc..3681d36b452 100644 --- a/src/third_party/wiredtiger/src/cursor/cur_join.c +++ b/src/third_party/wiredtiger/src/cursor/cur_join.c @@ -325,7 +325,7 @@ __curjoin_close(WT_CURSOR *cursor) JOINABLE_CURSOR_API_CALL(cursor, session, close, NULL); - __wt_schema_release_table(session, cjoin->table); + WT_TRET(__wt_schema_release_table(session, cjoin->table)); /* This is owned by the table */ cursor->key_format = NULL; if (cjoin->projection != NULL) { diff --git a/src/third_party/wiredtiger/src/cursor/cur_table.c b/src/third_party/wiredtiger/src/cursor/cur_table.c index f6855172e90..ffa9ca35926 100644 --- a/src/third_party/wiredtiger/src/cursor/cur_table.c +++ b/src/third_party/wiredtiger/src/cursor/cur_table.c @@ -786,7 +786,7 @@ __curtable_close(WT_CURSOR *cursor) __wt_free(session, ctable->cg_cursors); __wt_free(session, ctable->cg_valcopy); __wt_free(session, ctable->idx_cursors); - __wt_schema_release_table(session, ctable->table); + WT_TRET(__wt_schema_release_table(session, ctable->table)); /* The URI is owned by the table. */ cursor->internal_uri = NULL; WT_TRET(__wt_cursor_close(cursor)); @@ -942,7 +942,7 @@ __wt_curtable_open(WT_SESSION_IMPL *session, ret = __wt_open_cursor(session, table->cgroups[0]->source, NULL, cfg, cursorp); - __wt_schema_release_table(session, table); + WT_TRET(__wt_schema_release_table(session, table)); if (ret == 0) { /* Fix up the public URI to match what was passed in. */ cursor = *cursorp; diff --git a/src/third_party/wiredtiger/src/evict/evict_lru.c b/src/third_party/wiredtiger/src/evict/evict_lru.c index 26bbf9f679b..6e850f67b3e 100644 --- a/src/third_party/wiredtiger/src/evict/evict_lru.c +++ b/src/third_party/wiredtiger/src/evict/evict_lru.c @@ -17,7 +17,7 @@ static int __evict_pass(WT_SESSION_IMPL *); static int __evict_server(WT_SESSION_IMPL *, bool *); static int __evict_tune_workers(WT_SESSION_IMPL *session); static int __evict_walk(WT_SESSION_IMPL *, WT_EVICT_QUEUE *); -static int __evict_walk_file( +static int __evict_walk_tree( WT_SESSION_IMPL *, WT_EVICT_QUEUE *, u_int, u_int *); #define WT_EVICT_HAS_WORKERS(s) \ @@ -767,8 +767,10 @@ __evict_clear_walk(WT_SESSION_IMPL *session) cache = S2C(session)->cache; WT_ASSERT(session, F_ISSET(session, WT_SESSION_LOCKED_PASS)); - if (session->dhandle == cache->evict_file_next) - cache->evict_file_next = NULL; + if (session->dhandle == cache->walk_tree) { + cache->walk_tree = NULL; + cache->walk_target = 0; + } if ((ref = btree->evict_ref) == NULL) return (0); @@ -880,10 +882,8 @@ void __wt_evict_file_exclusive_off(WT_SESSION_IMPL *session) { WT_BTREE *btree; - WT_CACHE *cache; btree = S2BT(session); - cache = S2C(session)->cache; /* * We have seen subtle bugs with multiple threads racing to turn @@ -891,12 +891,26 @@ __wt_evict_file_exclusive_off(WT_SESSION_IMPL *session) */ WT_DIAGNOSTIC_YIELD; - /* Hold the walk lock to turn on eviction. */ - __wt_spin_lock(session, &cache->evict_walk_lock); - WT_ASSERT(session, - btree->evict_ref == NULL && btree->evict_disabled > 0); - --btree->evict_disabled; - __wt_spin_unlock(session, &cache->evict_walk_lock); + /* + * Atomically decrement the evict-disabled count, without acquiring the + * eviction walk-lock. We can't acquire that lock here because there's + * a potential deadlock. When acquiring exclusive eviction access, we + * acquire the eviction walk-lock and then the cache's pass-intr lock. + * The current eviction implementation can hold the pass-intr lock and + * call into this function (see WT-3303 for the details), which might + * deadlock with another thread trying to get exclusive eviction access. + */ +#if defined(HAVE_DIAGNOSTIC) + { + int32_t v; + + WT_ASSERT(session, btree->evict_ref == NULL); + v = __wt_atomic_subi32(&btree->evict_disabled, 1); + WT_ASSERT(session, v >= 0); + } +#else + (void)__wt_atomic_subi32(&btree->evict_disabled, 1); +#endif } #define EVICT_TUNE_BATCH 1 /* Max workers to add each period */ @@ -941,6 +955,13 @@ __evict_tune_workers(WT_SESSION_IMPL *session) conn = S2C(session); cache = conn->cache; + /* + * If we have a fixed number of eviction threads, there is no value in + * calculating if we should do any tuning. + */ + if (conn->evict_threads_max == conn->evict_threads_min) + return (0); + WT_ASSERT(session, conn->evict_threads.threads[0]->session == session); pgs_evicted_cur = pgs_evicted_persec_cur = 0; @@ -1371,19 +1392,22 @@ retry: while (slot < max_entries) { * scan last time through. If we don't have a saved * handle, start from the beginning of the list. */ - if ((dhandle = cache->evict_file_next) != NULL) - cache->evict_file_next = NULL; - else + if ((dhandle = cache->walk_tree) != NULL) + cache->walk_tree = NULL; + else { dhandle = TAILQ_FIRST(&conn->dhqh); + cache->walk_target = 0; + } } else { if (incr) { WT_ASSERT(session, dhandle->session_inuse > 0); (void)__wt_atomic_subi32( &dhandle->session_inuse, 1); incr = false; - cache->evict_file_next = NULL; + cache->walk_tree = NULL; } dhandle = TAILQ_NEXT(dhandle, q); + cache->walk_target = 0; } /* If we reach the end of the list, we're done. */ @@ -1445,29 +1469,26 @@ retry: while (slot < max_entries) { /* * Re-check the "no eviction" flag, used to enforce exclusive - * access when a handle is being closed. If not set, remember - * the file to visit first, next loop. + * access when a handle is being closed. * * Only try to acquire the lock and simply continue if we fail; * the lock is held while the thread turning off eviction clears * the tree's current eviction point, and part of the process is * waiting on this thread to acknowledge that action. + * + * If a handle is being discarded, it will still be marked open, + * but won't have a root page. */ if (btree->evict_disabled == 0 && !__wt_spin_trylock(session, &cache->evict_walk_lock)) { - if (btree->evict_disabled == 0) { + if (btree->evict_disabled == 0 && + btree->root.page != NULL) { /* - * Assert the handle has a root page: eviction - * should have been locked out if the tree is - * being discarded or the root page is changing. - * As this has not always been the case, assert - * to debug that change. + * Remember the file to visit first, next loop. */ - WT_ASSERT(session, btree->root.page != NULL); - - cache->evict_file_next = dhandle; + cache->walk_tree = dhandle; WT_WITH_DHANDLE(session, dhandle, - ret = __evict_walk_file( + ret = __evict_walk_tree( session, queue, max_entries, &slot)); WT_ASSERT(session, session->split_gen == 0); @@ -1489,7 +1510,7 @@ retry: while (slot < max_entries) { * candidates and we aren't finding more. */ if (slot < max_entries && (retries < 2 || - (retries < 10 && + (retries < WT_RETRY_MAX && (slot == queue->evict_entries || slot > start_slot)))) { start_slot = slot; ++retries; @@ -1556,45 +1577,21 @@ __evict_push_candidate(WT_SESSION_IMPL *session, } /* - * __evict_walk_file -- - * Get a few page eviction candidates from a single underlying file. + * __evict_walk_target -- + * Calculate how many pages to queue for a given tree. */ -static int -__evict_walk_file(WT_SESSION_IMPL *session, - WT_EVICT_QUEUE *queue, u_int max_entries, u_int *slotp) +static uint32_t +__evict_walk_target( + WT_SESSION_IMPL *session, WT_EVICT_QUEUE *queue, u_int max_entries) { - WT_BTREE *btree; WT_CACHE *cache; - WT_CONNECTION_IMPL *conn; - WT_DECL_RET; - WT_EVICT_ENTRY *end, *evict, *start; - WT_PAGE *page; - WT_PAGE_MODIFY *mod; - WT_REF *ref; - WT_TXN_GLOBAL *txn_global; - uint64_t btree_inuse, bytes_per_slot, cache_inuse, min_pages; - uint64_t pages_seen, pages_queued, refs_walked; - uint32_t remaining_slots, total_slots, walk_flags; + uint64_t btree_inuse, bytes_per_slot, cache_inuse; uint32_t target_pages_clean, target_pages_dirty, target_pages; - int internal_pages, restarts; - bool give_up, modified, urgent_queued; + uint32_t total_slots; - conn = S2C(session); - btree = S2BT(session); - cache = conn->cache; - txn_global = &conn->txn_global; - internal_pages = restarts = 0; - give_up = urgent_queued = false; - - /* - * Figure out how many slots to fill from this tree. - * Note that some care is taken in the calculation to avoid overflow. - */ - start = queue->evict_queue + *slotp; - remaining_slots = max_entries - *slotp; - total_slots = max_entries - queue->evict_entries; - btree_inuse = cache_inuse = 0; + cache = S2C(session)->cache; target_pages_clean = target_pages_dirty = 0; + total_slots = max_entries - queue->evict_entries; /* * The number of times we should fill the queue by the end of @@ -1640,26 +1637,16 @@ __evict_walk_file(WT_SESSION_IMPL *session, QUEUE_FILLS_PER_PASS; /* - * Randomly walk trees with a small fraction of the cache in case there - * are so many trees that none of them use enough of the cache to be - * allocated slots. - * - * The chance of walking a tree is equal to the chance that a random - * byte in cache belongs to the tree, weighted by how many times we - * want to fill queues during a pass through all the trees in cache. + * Walk trees with a small fraction of the cache in case there are so + * many trees that none of them use enough of the cache to be allocated + * slots. Only skip a tree if it has no bytes of interest. */ if (target_pages == 0) { - if (F_ISSET(cache, WT_CACHE_EVICT_CLEAN)) { - btree_inuse = __wt_btree_bytes_evictable(session); - cache_inuse = __wt_cache_bytes_inuse(cache); - } else { - btree_inuse = __wt_btree_dirty_leaf_inuse(session); - cache_inuse = __wt_cache_dirty_leaf_inuse(cache); - } - if (btree_inuse == 0 || cache_inuse == 0) - return (0); - if (__wt_random64(&session->rnd) % cache_inuse > - btree_inuse * QUEUE_FILLS_PER_PASS) + btree_inuse = F_ISSET(cache, WT_CACHE_EVICT_CLEAN) ? + __wt_btree_bytes_evictable(session) : + __wt_btree_dirty_leaf_inuse(session); + + if (btree_inuse == 0) return (0); } @@ -1670,13 +1657,64 @@ __evict_walk_file(WT_SESSION_IMPL *session, if (target_pages < MIN_PAGES_PER_TREE) target_pages = MIN_PAGES_PER_TREE; + /* If the tree is dead, take a lot of pages. */ + if (F_ISSET(session->dhandle, WT_DHANDLE_DEAD)) + target_pages *= 10; + + return (target_pages); +} + +/* + * __evict_walk_tree -- + * Get a few page eviction candidates from a single underlying file. + */ +static int +__evict_walk_tree(WT_SESSION_IMPL *session, + WT_EVICT_QUEUE *queue, u_int max_entries, u_int *slotp) +{ + WT_BTREE *btree; + WT_CACHE *cache; + WT_CONNECTION_IMPL *conn; + WT_DECL_RET; + WT_EVICT_ENTRY *end, *evict, *start; + WT_PAGE *page; + WT_PAGE_MODIFY *mod; + WT_REF *ref; + WT_TXN_GLOBAL *txn_global; + uint64_t min_pages, pages_seen, pages_queued, refs_walked; + uint32_t remaining_slots, target_pages, walk_flags; + int internal_pages, restarts; + bool give_up, modified, urgent_queued; + + conn = S2C(session); + btree = S2BT(session); + cache = conn->cache; + txn_global = &conn->txn_global; + internal_pages = restarts = 0; + give_up = urgent_queued = false; + /* - * If the tree is dead or we're near the end of the queue, fill the - * remaining slots. + * Figure out how many slots to fill from this tree. + * Note that some care is taken in the calculation to avoid overflow. */ - if (F_ISSET(session->dhandle, WT_DHANDLE_DEAD) || - target_pages > remaining_slots) + start = queue->evict_queue + *slotp; + remaining_slots = max_entries - *slotp; + if (cache->walk_target != 0) { + WT_ASSERT(session, cache->walk_progress <= cache->walk_target); + target_pages = cache->walk_target - cache->walk_progress; + } else { + target_pages = cache->walk_target = + __evict_walk_target(session, queue, max_entries); + cache->walk_progress = 0; + } + + if (target_pages > remaining_slots) target_pages = remaining_slots; + + /* If we don't want any pages from this tree, move on. */ + if (target_pages == 0) + return (0); + end = start + target_pages; /* @@ -1849,6 +1887,7 @@ fast: /* If the page can't be evicted, give up. */ continue; ++evict; ++pages_queued; + ++cache->walk_progress; if (WT_PAGE_IS_INTERNAL(page)) ++internal_pages; @@ -1863,6 +1902,10 @@ fast: /* If the page can't be evicted, give up. */ WT_STAT_CONN_INCRV( session, cache_eviction_pages_queued, (u_int)(evict - start)); + __wt_verbose(session, WT_VERB_EVICTSERVER, + "%s walk: seen %" PRIu64 ", queued %" PRIu64, + session->dhandle->name, pages_seen, pages_queued); + /* * If we couldn't find the number of pages we were looking for, skip * the tree next time. @@ -2445,14 +2488,23 @@ __wt_verbose_dump_cache(WT_SESSION_IMPL *session) WT_CONNECTION_IMPL *conn; WT_DATA_HANDLE *dhandle; WT_DECL_RET; + u_int pct; uint64_t total_bytes, total_dirty_bytes; conn = S2C(session); total_bytes = total_dirty_bytes = 0; + pct = 0; /* [-Werror=uninitialized] */ WT_RET(__wt_msg(session, "%s", WT_DIVIDER)); WT_RET(__wt_msg(session, "cache dump")); + WT_RET(__wt_msg(session, + "cache full: %s", __wt_cache_full(session) ? "yes" : "no")); + WT_RET(__wt_msg(session, "cache clean check: %s (%u%%)", + __wt_eviction_clean_needed(session, &pct) ? "yes" : "no", pct)); + WT_RET(__wt_msg(session, "cache dirty check: %s (%u%%)", + __wt_eviction_dirty_needed(session, &pct) ? "yes" : "no", pct)); + for (dhandle = NULL;;) { WT_WITH_HANDLE_LIST_READ_LOCK(session, WT_DHANDLE_NEXT(session, dhandle, &conn->dhqh, q)); diff --git a/src/third_party/wiredtiger/src/evict/evict_page.c b/src/third_party/wiredtiger/src/evict/evict_page.c index 85689efd0b1..70a16442dc4 100644 --- a/src/third_party/wiredtiger/src/evict/evict_page.c +++ b/src/third_party/wiredtiger/src/evict/evict_page.c @@ -156,7 +156,7 @@ __wt_evict(WT_SESSION_IMPL *session, WT_REF *ref, bool closing) /* Update the reference and discard the page. */ if (__wt_ref_is_root(ref)) __wt_ref_out(session, ref); - else if ((clean_page && !LF_ISSET(WT_EVICT_IN_MEMORY)) || tree_dead) + else if ((clean_page && !F_ISSET(conn, WT_CONN_IN_MEMORY)) || tree_dead) /* * Pages that belong to dead trees never write back to disk * and can't support page splits. @@ -202,8 +202,8 @@ __evict_delete_ref(WT_SESSION_IMPL *session, WT_REF *ref, bool closing) return (0); /* - * Avoid doing reverse splits when closing the file, it is - * wasted work and some structure may already have been freed. + * Avoid doing reverse splits when closing the file, it is wasted work + * and some structures may have already been freed. */ if (!closing) { parent = ref->home; @@ -393,11 +393,13 @@ __evict_review( WT_SESSION_IMPL *session, WT_REF *ref, uint32_t *flagsp, bool closing) { WT_CACHE *cache; + WT_CONNECTION_IMPL *conn; WT_DECL_RET; WT_PAGE *page; uint32_t flags; - bool lookaside_retry, modified; + bool lookaside_retry, *lookaside_retryp, modified; + conn = S2C(session); flags = WT_EVICTING; *flagsp = flags; @@ -453,7 +455,7 @@ __evict_review( * Clean pages can't be evicted when running in memory only. This * should be uncommon - we don't add clean pages to the queue. */ - if (F_ISSET(S2C(session), WT_CONN_IN_MEMORY) && !modified && !closing) + if (F_ISSET(conn, WT_CONN_IN_MEMORY) && !modified && !closing) return (EBUSY); /* Check if the page can be evicted. */ @@ -519,11 +521,14 @@ __evict_review( * Additionally, if we aren't trying to free space in the cache, scrub * the page and keep it in memory. */ - cache = S2C(session)->cache; + cache = conn->cache; + lookaside_retry = false; + lookaside_retryp = NULL; + if (closing) LF_SET(WT_VISIBILITY_ERR); else if (!WT_PAGE_IS_INTERNAL(page)) { - if (F_ISSET(S2C(session), WT_CONN_IN_MEMORY)) + if (F_ISSET(conn, WT_CONN_IN_MEMORY)) LF_SET(WT_EVICT_IN_MEMORY | WT_EVICT_SCRUB | WT_EVICT_UPDATE_RESTORE); else { @@ -531,21 +536,26 @@ __evict_review( if (F_ISSET(cache, WT_CACHE_EVICT_SCRUB)) LF_SET(WT_EVICT_SCRUB); + + /* + * Check if reconciliation suggests trying the + * lookaside table. + */ + lookaside_retryp = &lookaside_retry; } } /* Reconcile the page. */ - ret = __wt_reconcile(session, ref, NULL, flags, &lookaside_retry); + ret = __wt_reconcile(session, ref, NULL, flags, lookaside_retryp); /* - * If reconciliation fails, eviction is stuck and reconciliation reports - * it might succeed if we use the lookaside table (the page didn't have - * uncommitted updates, it was not-yet-globally visible updates causing - * the problem), configure reconciliation to write those updates to the - * lookaside table, allowing the eviction of pages we'd otherwise have - * to retain in cache to support older readers. + * If reconciliation fails, eviction is stuck and reconciliation + * reports it might succeed if we use the lookaside table, then + * configure reconciliation to write those updates to the lookaside + * table, allowing the eviction of pages we'd otherwise have to retain + * in cache to support older readers. */ - if (ret == EBUSY && __wt_cache_stuck(session) && lookaside_retry) { + if (ret == EBUSY && lookaside_retry && __wt_cache_stuck(session)) { LF_CLR(WT_EVICT_SCRUB | WT_EVICT_UPDATE_RESTORE); LF_SET(WT_EVICT_LOOKASIDE); ret = __wt_reconcile(session, ref, NULL, flags, NULL); diff --git a/src/third_party/wiredtiger/src/include/btmem.h b/src/third_party/wiredtiger/src/include/btmem.h index d0b21b17965..e965724dffe 100644 --- a/src/third_party/wiredtiger/src/include/btmem.h +++ b/src/third_party/wiredtiger/src/include/btmem.h @@ -714,7 +714,7 @@ struct __wt_page { * Related information for fast-delete, on-disk pages. */ struct __wt_page_deleted { - uint64_t txnid; /* Transaction ID */ + volatile uint64_t txnid; /* Transaction ID */ WT_UPDATE **update_list; /* List of updates for abort */ }; @@ -904,7 +904,7 @@ struct __wt_ikey { * list. */ WT_PACKED_STRUCT_BEGIN(__wt_update) - uint64_t txnid; /* update transaction */ + volatile uint64_t txnid; /* Transaction ID */ WT_UPDATE *next; /* forward-linked list */ diff --git a/src/third_party/wiredtiger/src/include/btree.h b/src/third_party/wiredtiger/src/include/btree.h index 28fe1b94b23..5b1b4d68976 100644 --- a/src/third_party/wiredtiger/src/include/btree.h +++ b/src/third_party/wiredtiger/src/include/btree.h @@ -134,34 +134,44 @@ struct __wt_btree { WT_BM *bm; /* Block manager reference */ u_int block_header; /* WT_PAGE_HEADER_BYTE_SIZE */ - uint64_t checkpoint_gen; /* Checkpoint generation */ - uint64_t rec_max_txn; /* Maximum txn seen (clean trees) */ uint64_t write_gen; /* Write generation */ + uint64_t rec_max_txn; /* Maximum txn seen (clean trees) */ + uint64_t checkpoint_gen; /* Checkpoint generation */ + volatile enum { + WT_CKPT_OFF, WT_CKPT_PREPARE, WT_CKPT_RUNNING + } checkpointing; /* Checkpoint in progress */ uint64_t bytes_inmem; /* Cache bytes in memory. */ uint64_t bytes_dirty_intl; /* Bytes in dirty internal pages. */ uint64_t bytes_dirty_leaf; /* Bytes in dirty leaf pages. */ + /* + * We flush pages from the tree (in order to make checkpoint faster), + * without a high-level lock. To avoid multiple threads flushing at + * the same time, lock the tree. + */ + WT_SPINLOCK flush_lock; /* Lock to flush the tree's pages */ + + /* + * All of the following fields live at the end of the structure so it's + * easier to clear everything but the fields that persist. + */ +#define WT_BTREE_CLEAR_SIZE (offsetof(WT_BTREE, evict_ref)) + + /* + * Eviction information is maintained in the btree handle, but owned by + * eviction, not the btree code. + */ WT_REF *evict_ref; /* Eviction thread's location */ uint64_t evict_priority; /* Relative priority of cached pages */ u_int evict_walk_period; /* Skip this many LRU walks */ u_int evict_walk_saved; /* Saved walk skips for checkpoints */ u_int evict_walk_skips; /* Number of walks skipped */ - int evict_disabled; /* Eviction disabled count */ + int32_t evict_disabled; /* Eviction disabled count */ + bool evict_disabled_open;/* Eviction disabled on open */ volatile uint32_t evict_busy; /* Count of threads in eviction */ int evict_start_type; /* Start position for eviction walk (see WT_EVICT_WALK_START). */ - enum { - WT_CKPT_OFF, WT_CKPT_PREPARE, WT_CKPT_RUNNING - } checkpointing; /* Checkpoint in progress */ - - /* - * We flush pages from the tree (in order to make checkpoint faster), - * without a high-level lock. To avoid multiple threads flushing at - * the same time, lock the tree. - */ - WT_SPINLOCK flush_lock; /* Lock to flush the tree's pages */ - /* Flags values up to 0xff are reserved for WT_DHANDLE_* */ #define WT_BTREE_ALLOW_SPLITS 0x000100 /* Allow splits, even with no evict */ #define WT_BTREE_BULK 0x000200 /* Bulk-load handle */ diff --git a/src/third_party/wiredtiger/src/include/cache.h b/src/third_party/wiredtiger/src/include/cache.h index 04920c3585a..42a152e5f10 100644 --- a/src/third_party/wiredtiger/src/include/cache.h +++ b/src/third_party/wiredtiger/src/include/cache.h @@ -133,7 +133,8 @@ struct __wt_cache { */ WT_SPINLOCK evict_pass_lock; /* Eviction pass lock */ WT_SESSION_IMPL *walk_session; /* Eviction pass session */ - WT_DATA_HANDLE *evict_file_next;/* LRU next file to search */ + WT_DATA_HANDLE *walk_tree; /* LRU walk current tree */ + uint32_t walk_progress, walk_target;/* Progress in current tree */ WT_SPINLOCK evict_queue_lock; /* Eviction current queue lock */ WT_EVICT_QUEUE evict_queues[WT_EVICT_QUEUE_MAX]; diff --git a/src/third_party/wiredtiger/src/include/connection.h b/src/third_party/wiredtiger/src/include/connection.h index f74732684f5..74611de1131 100644 --- a/src/third_party/wiredtiger/src/include/connection.h +++ b/src/third_party/wiredtiger/src/include/connection.h @@ -314,9 +314,10 @@ struct __wt_connection_impl { #define WT_CONN_LOG_ARCHIVE 0x01 /* Archive is enabled */ #define WT_CONN_LOG_ENABLED 0x02 /* Logging is enabled */ #define WT_CONN_LOG_EXISTED 0x04 /* Log files found */ -#define WT_CONN_LOG_RECOVER_DONE 0x08 /* Recovery completed */ -#define WT_CONN_LOG_RECOVER_ERR 0x10 /* Error if recovery required */ -#define WT_CONN_LOG_ZERO_FILL 0x20 /* Manually zero files */ +#define WT_CONN_LOG_RECOVER_DIRTY 0x08 /* Recovering unclean */ +#define WT_CONN_LOG_RECOVER_DONE 0x10 /* Recovery completed */ +#define WT_CONN_LOG_RECOVER_ERR 0x20 /* Error if recovery required */ +#define WT_CONN_LOG_ZERO_FILL 0x40 /* Manually zero files */ uint32_t log_flags; /* Global logging configuration */ WT_CONDVAR *log_cond; /* Log server wait mutex */ WT_SESSION_IMPL *log_session; /* Log server session */ diff --git a/src/third_party/wiredtiger/src/include/extern.h b/src/third_party/wiredtiger/src/include/extern.h index bf3279d0f94..3c45b290dd8 100644 --- a/src/third_party/wiredtiger/src/include/extern.h +++ b/src/third_party/wiredtiger/src/include/extern.h @@ -181,7 +181,7 @@ extern int __wt_verify_dsk_image(WT_SESSION_IMPL *session, const char *tag, cons extern int __wt_verify_dsk(WT_SESSION_IMPL *session, const char *tag, WT_ITEM *buf) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_tree_walk(WT_SESSION_IMPL *session, WT_REF **refp, uint32_t flags) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_tree_walk_count(WT_SESSION_IMPL *session, WT_REF **refp, uint64_t *walkcntp, uint32_t flags) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); -extern int __wt_tree_walk_skip(WT_SESSION_IMPL *session, WT_REF **refp, uint64_t *skipleafcntp, uint32_t flags) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); +extern int __wt_tree_walk_skip( WT_SESSION_IMPL *session, WT_REF **refp, uint64_t *skipleafcntp) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_col_modify(WT_SESSION_IMPL *session, WT_CURSOR_BTREE *cbt, uint64_t recno, WT_ITEM *value, WT_UPDATE *upd_arg, bool is_remove, bool exclusive) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_col_search(WT_SESSION_IMPL *session, uint64_t search_recno, WT_REF *leaf, WT_CURSOR_BTREE *cbt) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_row_leaf_keys(WT_SESSION_IMPL *session, WT_PAGE *page) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); @@ -369,6 +369,7 @@ extern int __wt_log_needs_recovery(WT_SESSION_IMPL *session, WT_LSN *ckp_lsn, bo extern void __wt_log_written_reset(WT_SESSION_IMPL *session); extern int __wt_log_get_all_files(WT_SESSION_IMPL *session, char ***filesp, u_int *countp, uint32_t *maxid, bool active_only) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_log_extract_lognum( WT_SESSION_IMPL *session, const char *name, uint32_t *id) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); +extern int __wt_log_reset(WT_SESSION_IMPL *session, uint32_t lognum) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_log_acquire(WT_SESSION_IMPL *session, uint64_t recsize, WT_LOGSLOT *slot) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_log_allocfile( WT_SESSION_IMPL *session, uint32_t lognum, const char *dest) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_log_remove(WT_SESSION_IMPL *session, const char *file_prefix, uint32_t lognum) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); @@ -405,7 +406,7 @@ extern int __wt_logop_row_truncate_print(WT_SESSION_IMPL *session, const uint8_t extern int __wt_txn_op_printlog(WT_SESSION_IMPL *session, const uint8_t **pp, const uint8_t *end, uint32_t flags) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern void __wt_log_slot_activate(WT_SESSION_IMPL *session, WT_LOGSLOT *slot); extern int __wt_log_slot_switch(WT_SESSION_IMPL *session, WT_MYSLOT *myslot, bool retry, bool forced, bool *did_work) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); -extern int __wt_log_slot_init(WT_SESSION_IMPL *session) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); +extern int __wt_log_slot_init(WT_SESSION_IMPL *session, bool alloc) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_log_slot_destroy(WT_SESSION_IMPL *session) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_log_slot_join(WT_SESSION_IMPL *session, uint64_t mysize, uint32_t flags, WT_MYSLOT *myslot) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int64_t __wt_log_slot_release(WT_SESSION_IMPL *session, WT_MYSLOT *myslot, int64_t size); @@ -564,12 +565,13 @@ extern int __wt_schema_index_source(WT_SESSION_IMPL *session, WT_TABLE *table, c extern int __wt_schema_create( WT_SESSION_IMPL *session, const char *uri, const char *config) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_schema_drop(WT_SESSION_IMPL *session, const char *uri, const char *cfg[]) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_schema_get_table(WT_SESSION_IMPL *session, const char *name, size_t namelen, bool ok_incomplete, WT_TABLE **tablep) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); -extern void __wt_schema_release_table(WT_SESSION_IMPL *session, WT_TABLE *table); +extern int __wt_schema_release_table(WT_SESSION_IMPL *session, WT_TABLE *table) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern void __wt_schema_destroy_colgroup(WT_SESSION_IMPL *session, WT_COLGROUP **colgroupp); extern int __wt_schema_destroy_index(WT_SESSION_IMPL *session, WT_INDEX **idxp) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_schema_destroy_table(WT_SESSION_IMPL *session, WT_TABLE **tablep) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_schema_remove_table(WT_SESSION_IMPL *session, WT_TABLE *table) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_schema_close_tables(WT_SESSION_IMPL *session) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); +extern int __wt_schema_sweep_tables(WT_SESSION_IMPL *session) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_schema_colgroup_name(WT_SESSION_IMPL *session, WT_TABLE *table, const char *cgname, size_t len, WT_ITEM *buf) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_schema_open_colgroups(WT_SESSION_IMPL *session, WT_TABLE *table) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_schema_open_index(WT_SESSION_IMPL *session, WT_TABLE *table, const char *idxname, size_t len, WT_INDEX **indexp) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); @@ -691,7 +693,6 @@ extern uint32_t __wt_rduppo2(uint32_t n, uint32_t po2); extern void __wt_random_init(WT_RAND_STATE volatile *rnd_state) WT_GCC_FUNC_DECL_ATTRIBUTE((visibility("default"))); extern void __wt_random_init_seed( WT_SESSION_IMPL *session, WT_RAND_STATE volatile *rnd_state) WT_GCC_FUNC_DECL_ATTRIBUTE((visibility("default"))); extern uint32_t __wt_random(WT_RAND_STATE volatile *rnd_state) WT_GCC_FUNC_DECL_ATTRIBUTE((visibility("default"))); -extern uint64_t __wt_random64(WT_RAND_STATE volatile *rnd_state) WT_GCC_FUNC_DECL_ATTRIBUTE((visibility("default"))); extern int __wt_buf_grow_worker(WT_SESSION_IMPL *session, WT_ITEM *buf, size_t size) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_buf_fmt(WT_SESSION_IMPL *session, WT_ITEM *buf, const char *fmt, ...) WT_GCC_FUNC_DECL_ATTRIBUTE((format (printf, 3, 4))) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_buf_catfmt(WT_SESSION_IMPL *session, WT_ITEM *buf, const char *fmt, ...) WT_GCC_FUNC_DECL_ATTRIBUTE((format (printf, 3, 4))) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); @@ -732,6 +733,8 @@ extern int __wt_thread_group_create( WT_SESSION_IMPL *session, WT_THREAD_GROUP * extern int __wt_thread_group_destroy(WT_SESSION_IMPL *session, WT_THREAD_GROUP *group) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_thread_group_start_one( WT_SESSION_IMPL *session, WT_THREAD_GROUP *group, bool wait) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_thread_group_stop_one( WT_SESSION_IMPL *session, WT_THREAD_GROUP *group, bool wait) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); +extern void __wt_epoch(WT_SESSION_IMPL *session, struct timespec *tsp) WT_GCC_FUNC_DECL_ATTRIBUTE((visibility("default"))); +extern void __wt_seconds(WT_SESSION_IMPL *session, time_t *timep); extern void __wt_txn_release_snapshot(WT_SESSION_IMPL *session); extern void __wt_txn_get_snapshot(WT_SESSION_IMPL *session); extern int __wt_txn_update_oldest(WT_SESSION_IMPL *session, uint32_t flags) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); diff --git a/src/third_party/wiredtiger/src/include/extern_posix.h b/src/third_party/wiredtiger/src/include/extern_posix.h index c0ed056c7b6..9e32e86e64c 100644 --- a/src/third_party/wiredtiger/src/include/extern_posix.h +++ b/src/third_party/wiredtiger/src/include/extern_posix.h @@ -28,5 +28,5 @@ extern int __wt_vsnprintf_len_incr( char *buf, size_t size, size_t *retsizep, co extern int __wt_thread_create(WT_SESSION_IMPL *session, wt_thread_t *tidret, WT_THREAD_CALLBACK(*func)(void *), void *arg) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_thread_join(WT_SESSION_IMPL *session, wt_thread_t tid) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_thread_id(char *buf, size_t buflen) WT_GCC_FUNC_DECL_ATTRIBUTE((visibility("default"))) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); -extern void __wt_epoch(WT_SESSION_IMPL *session, struct timespec *tsp) WT_GCC_FUNC_DECL_ATTRIBUTE((visibility("default"))); +extern void __wt_epoch_raw(WT_SESSION_IMPL *session, struct timespec *tsp); extern void __wt_yield(void) WT_GCC_FUNC_DECL_ATTRIBUTE((visibility("default"))); diff --git a/src/third_party/wiredtiger/src/include/extern_win.h b/src/third_party/wiredtiger/src/include/extern_win.h index d548ee0b2ec..85db8175615 100644 --- a/src/third_party/wiredtiger/src/include/extern_win.h +++ b/src/third_party/wiredtiger/src/include/extern_win.h @@ -26,7 +26,7 @@ extern int __wt_vsnprintf_len_incr( char *buf, size_t size, size_t *retsizep, co extern int __wt_thread_create(WT_SESSION_IMPL *session, wt_thread_t *tidret, WT_THREAD_CALLBACK(*func)(void *), void *arg) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_thread_join(WT_SESSION_IMPL *session, wt_thread_t tid) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_thread_id(char *buf, size_t buflen) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); -extern void __wt_epoch(WT_SESSION_IMPL *session, struct timespec *tsp); +extern void __wt_epoch_raw(WT_SESSION_IMPL *session, struct timespec *tsp); extern int __wt_to_utf16_string( WT_SESSION_IMPL *session, const char*utf8, WT_ITEM **outbuf) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern int __wt_to_utf8_string( WT_SESSION_IMPL *session, const wchar_t*wide, WT_ITEM **outbuf) WT_GCC_FUNC_DECL_ATTRIBUTE((warn_unused_result)); extern DWORD __wt_getlasterror(void); diff --git a/src/third_party/wiredtiger/src/include/flags.h b/src/third_party/wiredtiger/src/include/flags.h index f26a45c68f5..0e6cf7657ed 100644 --- a/src/third_party/wiredtiger/src/include/flags.h +++ b/src/third_party/wiredtiger/src/include/flags.h @@ -21,7 +21,8 @@ #define WT_CONN_SERVER_LSM 0x00008000 #define WT_CONN_SERVER_STATISTICS 0x00010000 #define WT_CONN_SERVER_SWEEP 0x00020000 -#define WT_CONN_WAS_BACKUP 0x00040000 +#define WT_CONN_TABLE_CACHE 0x00040000 +#define WT_CONN_WAS_BACKUP 0x00080000 #define WT_EVICTING 0x00000002 #define WT_EVICT_INMEM_SPLIT 0x00000004 #define WT_EVICT_IN_MEMORY 0x00000008 @@ -47,9 +48,8 @@ #define WT_READ_PREV 0x00000080 #define WT_READ_RESTART_OK 0x00000100 #define WT_READ_SKIP_INTL 0x00000200 -#define WT_READ_SKIP_LEAF 0x00000400 -#define WT_READ_TRUNCATE 0x00000800 -#define WT_READ_WONT_NEED 0x00001000 +#define WT_READ_TRUNCATE 0x00000400 +#define WT_READ_WONT_NEED 0x00000800 #define WT_SESSION_CAN_WAIT 0x00000001 #define WT_SESSION_INTERNAL 0x00000002 #define WT_SESSION_LOCKED_CHECKPOINT 0x00000004 diff --git a/src/third_party/wiredtiger/src/include/misc.i b/src/third_party/wiredtiger/src/include/misc.i index fad10f01103..eb99de3dcab 100644 --- a/src/third_party/wiredtiger/src/include/misc.i +++ b/src/third_party/wiredtiger/src/include/misc.i @@ -41,45 +41,6 @@ __wt_strdup(WT_SESSION_IMPL *session, const char *str, void *retp) } /* - * __wt_seconds -- - * Return the seconds since the Epoch. - */ -static inline void -__wt_seconds(WT_SESSION_IMPL *session, time_t *timep) -{ - struct timespec t; - - __wt_epoch(session, &t); - - *timep = t.tv_sec; -} - -/* - * __wt_time_check_monotonic -- - * Check and prevent time running backward. If we detect that it has, we - * set the time structure to the previous values, making time stand still - * until we see a time in the future of the highest value seen so far. - */ -static inline void -__wt_time_check_monotonic(WT_SESSION_IMPL *session, struct timespec *tsp) -{ - /* - * Detect time going backward. If so, use the last - * saved timestamp. - */ - if (session == NULL) - return; - - if (tsp->tv_sec < session->last_epoch.tv_sec || - (tsp->tv_sec == session->last_epoch.tv_sec && - tsp->tv_nsec < session->last_epoch.tv_nsec)) { - WT_STAT_CONN_INCR(session, time_travel); - *tsp = session->last_epoch; - } else - session->last_epoch = *tsp; -} - -/* * __wt_verbose -- * Verbose message. * diff --git a/src/third_party/wiredtiger/src/include/os.h b/src/third_party/wiredtiger/src/include/os.h index 73d89268392..2c03b115e7c 100644 --- a/src/third_party/wiredtiger/src/include/os.h +++ b/src/third_party/wiredtiger/src/include/os.h @@ -34,9 +34,11 @@ (ret) = __wt_errno(); \ } while (0) +#define WT_RETRY_MAX 10 + #define WT_SYSCALL_RETRY(call, ret) do { \ int __retry; \ - for (__retry = 0; __retry < 10; ++__retry) { \ + for (__retry = 0; __retry < WT_RETRY_MAX; ++__retry) { \ WT_SYSCALL(call, ret); \ switch (ret) { \ case EAGAIN: \ diff --git a/src/third_party/wiredtiger/src/include/session.h b/src/third_party/wiredtiger/src/include/session.h index 0074b4a9d43..d05dee68641 100644 --- a/src/third_party/wiredtiger/src/include/session.h +++ b/src/third_party/wiredtiger/src/include/session.h @@ -98,6 +98,12 @@ struct __wt_session_impl { */ TAILQ_HEAD(__tables, __wt_table) tables; + /* + * Updated when the table cache is swept of all tables older than the + * current schema generation. + */ + uint64_t table_sweep_gen; + /* Current rwlock for callback. */ WT_RWLOCK *current_rwlock; uint8_t current_rwticket; diff --git a/src/third_party/wiredtiger/src/include/stat.h b/src/third_party/wiredtiger/src/include/stat.h index db48a841571..01e622a5695 100644 --- a/src/third_party/wiredtiger/src/include/stat.h +++ b/src/third_party/wiredtiger/src/include/stat.h @@ -475,11 +475,19 @@ struct __wt_connection_stats { int64_t thread_write_active; int64_t application_evict_time; int64_t application_cache_time; + int64_t txn_release_blocked; + int64_t conn_close_blocked_lsm; + int64_t dhandle_lock_blocked; + int64_t log_server_sync_blocked; int64_t page_busy_blocked; int64_t page_forcible_evict_blocked; int64_t page_locked_blocked; int64_t page_read_blocked; int64_t page_sleep; + int64_t page_del_rollback_blocked; + int64_t child_modify_blocked_page; + int64_t page_index_slot_blocked; + int64_t tree_descend_blocked; int64_t txn_snapshots_created; int64_t txn_snapshots_dropped; int64_t txn_begin; diff --git a/src/third_party/wiredtiger/src/include/txn.h b/src/third_party/wiredtiger/src/include/txn.h index 7e802c188ab..fdf9c714afa 100644 --- a/src/third_party/wiredtiger/src/include/txn.h +++ b/src/third_party/wiredtiger/src/include/txn.h @@ -93,6 +93,8 @@ struct __wt_txn_global { * the global transaction state. */ WT_RWLOCK scan_rwlock; + /* Protects logging, checkpoints and transaction visibility. */ + WT_RWLOCK visibility_rwlock; /* * Track information about the running checkpoint. The transaction diff --git a/src/third_party/wiredtiger/src/include/txn.i b/src/third_party/wiredtiger/src/include/txn.i index 314c948e4d1..39273a1995c 100644 --- a/src/third_party/wiredtiger/src/include/txn.i +++ b/src/third_party/wiredtiger/src/include/txn.i @@ -148,16 +148,6 @@ __wt_txn_oldest_id(WT_SESSION_IMPL *session) } /* - * __wt_txn_committed -- - * Return if a transaction has been committed. - */ -static inline bool -__wt_txn_committed(WT_SESSION_IMPL *session, uint64_t id) -{ - return (WT_TXNID_LT(id, S2C(session)->txn_global.last_running)); -} - -/* * __wt_txn_visible_all -- * Check if a given transaction ID is "globally visible". This is, if * all sessions in the system will see the transaction ID including the diff --git a/src/third_party/wiredtiger/src/include/wiredtiger.in b/src/third_party/wiredtiger/src/include/wiredtiger.in index 821efdf5fa1..30bff37017a 100644 --- a/src/third_party/wiredtiger/src/include/wiredtiger.in +++ b/src/third_party/wiredtiger/src/include/wiredtiger.in @@ -2458,6 +2458,8 @@ struct __wt_connection { * readonly for more information., a boolean flag; default \c false.} * @config{session_max, maximum expected number of sessions (including server * threads)., an integer greater than or equal to 1; default \c 100.} + * @config{session_table_cache, Maintain a per-session cache of tables., a + * boolean flag; default \c true.} * @config{shared_cache = (, shared cache configuration options. A database * should configure either a cache_size or a shared_cache not both. Enabling a * shared cache uses a session from the configured session_max., a set of @@ -4806,72 +4808,94 @@ extern int wiredtiger_extension_terminate(WT_CONNECTION *connection); #define WT_STAT_CONN_APPLICATION_EVICT_TIME 1216 /*! thread-yield: application thread time waiting for cache (usecs) */ #define WT_STAT_CONN_APPLICATION_CACHE_TIME 1217 +/*! + * thread-yield: connection close blocked waiting for transaction state + * stabilization + */ +#define WT_STAT_CONN_TXN_RELEASE_BLOCKED 1218 +/*! thread-yield: connection close yielded for lsm manager shutdown */ +#define WT_STAT_CONN_CONN_CLOSE_BLOCKED_LSM 1219 +/*! thread-yield: data handle lock yielded */ +#define WT_STAT_CONN_DHANDLE_LOCK_BLOCKED 1220 +/*! thread-yield: log server sync yielded for log write */ +#define WT_STAT_CONN_LOG_SERVER_SYNC_BLOCKED 1221 /*! thread-yield: page acquire busy blocked */ -#define WT_STAT_CONN_PAGE_BUSY_BLOCKED 1218 +#define WT_STAT_CONN_PAGE_BUSY_BLOCKED 1222 /*! thread-yield: page acquire eviction blocked */ -#define WT_STAT_CONN_PAGE_FORCIBLE_EVICT_BLOCKED 1219 +#define WT_STAT_CONN_PAGE_FORCIBLE_EVICT_BLOCKED 1223 /*! thread-yield: page acquire locked blocked */ -#define WT_STAT_CONN_PAGE_LOCKED_BLOCKED 1220 +#define WT_STAT_CONN_PAGE_LOCKED_BLOCKED 1224 /*! thread-yield: page acquire read blocked */ -#define WT_STAT_CONN_PAGE_READ_BLOCKED 1221 +#define WT_STAT_CONN_PAGE_READ_BLOCKED 1225 /*! thread-yield: page acquire time sleeping (usecs) */ -#define WT_STAT_CONN_PAGE_SLEEP 1222 +#define WT_STAT_CONN_PAGE_SLEEP 1226 +/*! thread-yield: page delete rollback yielded for instantiation */ +#define WT_STAT_CONN_PAGE_DEL_ROLLBACK_BLOCKED 1227 +/*! thread-yield: page reconciliation yielded due to child modification */ +#define WT_STAT_CONN_CHILD_MODIFY_BLOCKED_PAGE 1228 +/*! thread-yield: reference for page index and slot yielded */ +#define WT_STAT_CONN_PAGE_INDEX_SLOT_BLOCKED 1229 +/*! + * thread-yield: tree descend one level yielded for split page index + * update + */ +#define WT_STAT_CONN_TREE_DESCEND_BLOCKED 1230 /*! transaction: number of named snapshots created */ -#define WT_STAT_CONN_TXN_SNAPSHOTS_CREATED 1223 +#define WT_STAT_CONN_TXN_SNAPSHOTS_CREATED 1231 /*! transaction: number of named snapshots dropped */ -#define WT_STAT_CONN_TXN_SNAPSHOTS_DROPPED 1224 +#define WT_STAT_CONN_TXN_SNAPSHOTS_DROPPED 1232 /*! transaction: transaction begins */ -#define WT_STAT_CONN_TXN_BEGIN 1225 +#define WT_STAT_CONN_TXN_BEGIN 1233 /*! transaction: transaction checkpoint currently running */ -#define WT_STAT_CONN_TXN_CHECKPOINT_RUNNING 1226 +#define WT_STAT_CONN_TXN_CHECKPOINT_RUNNING 1234 /*! transaction: transaction checkpoint generation */ -#define WT_STAT_CONN_TXN_CHECKPOINT_GENERATION 1227 +#define WT_STAT_CONN_TXN_CHECKPOINT_GENERATION 1235 /*! transaction: transaction checkpoint max time (msecs) */ -#define WT_STAT_CONN_TXN_CHECKPOINT_TIME_MAX 1228 +#define WT_STAT_CONN_TXN_CHECKPOINT_TIME_MAX 1236 /*! transaction: transaction checkpoint min time (msecs) */ -#define WT_STAT_CONN_TXN_CHECKPOINT_TIME_MIN 1229 +#define WT_STAT_CONN_TXN_CHECKPOINT_TIME_MIN 1237 /*! transaction: transaction checkpoint most recent time (msecs) */ -#define WT_STAT_CONN_TXN_CHECKPOINT_TIME_RECENT 1230 +#define WT_STAT_CONN_TXN_CHECKPOINT_TIME_RECENT 1238 /*! transaction: transaction checkpoint scrub dirty target */ -#define WT_STAT_CONN_TXN_CHECKPOINT_SCRUB_TARGET 1231 +#define WT_STAT_CONN_TXN_CHECKPOINT_SCRUB_TARGET 1239 /*! transaction: transaction checkpoint scrub time (msecs) */ -#define WT_STAT_CONN_TXN_CHECKPOINT_SCRUB_TIME 1232 +#define WT_STAT_CONN_TXN_CHECKPOINT_SCRUB_TIME 1240 /*! transaction: transaction checkpoint total time (msecs) */ -#define WT_STAT_CONN_TXN_CHECKPOINT_TIME_TOTAL 1233 +#define WT_STAT_CONN_TXN_CHECKPOINT_TIME_TOTAL 1241 /*! transaction: transaction checkpoints */ -#define WT_STAT_CONN_TXN_CHECKPOINT 1234 +#define WT_STAT_CONN_TXN_CHECKPOINT 1242 /*! * transaction: transaction checkpoints skipped because database was * clean */ -#define WT_STAT_CONN_TXN_CHECKPOINT_SKIPPED 1235 +#define WT_STAT_CONN_TXN_CHECKPOINT_SKIPPED 1243 /*! transaction: transaction failures due to cache overflow */ -#define WT_STAT_CONN_TXN_FAIL_CACHE 1236 +#define WT_STAT_CONN_TXN_FAIL_CACHE 1244 /*! * transaction: transaction fsync calls for checkpoint after allocating * the transaction ID */ -#define WT_STAT_CONN_TXN_CHECKPOINT_FSYNC_POST 1237 +#define WT_STAT_CONN_TXN_CHECKPOINT_FSYNC_POST 1245 /*! * transaction: transaction fsync duration for checkpoint after * allocating the transaction ID (usecs) */ -#define WT_STAT_CONN_TXN_CHECKPOINT_FSYNC_POST_DURATION 1238 +#define WT_STAT_CONN_TXN_CHECKPOINT_FSYNC_POST_DURATION 1246 /*! transaction: transaction range of IDs currently pinned */ -#define WT_STAT_CONN_TXN_PINNED_RANGE 1239 +#define WT_STAT_CONN_TXN_PINNED_RANGE 1247 /*! transaction: transaction range of IDs currently pinned by a checkpoint */ -#define WT_STAT_CONN_TXN_PINNED_CHECKPOINT_RANGE 1240 +#define WT_STAT_CONN_TXN_PINNED_CHECKPOINT_RANGE 1248 /*! * transaction: transaction range of IDs currently pinned by named * snapshots */ -#define WT_STAT_CONN_TXN_PINNED_SNAPSHOT_RANGE 1241 +#define WT_STAT_CONN_TXN_PINNED_SNAPSHOT_RANGE 1249 /*! transaction: transaction sync calls */ -#define WT_STAT_CONN_TXN_SYNC 1242 +#define WT_STAT_CONN_TXN_SYNC 1250 /*! transaction: transactions committed */ -#define WT_STAT_CONN_TXN_COMMIT 1243 +#define WT_STAT_CONN_TXN_COMMIT 1251 /*! transaction: transactions rolled back */ -#define WT_STAT_CONN_TXN_ROLLBACK 1244 +#define WT_STAT_CONN_TXN_ROLLBACK 1252 /*! * @} diff --git a/src/third_party/wiredtiger/src/log/log.c b/src/third_party/wiredtiger/src/log/log.c index 803d3e8dfab..da179400755 100644 --- a/src/third_party/wiredtiger/src/log/log.c +++ b/src/third_party/wiredtiger/src/log/log.c @@ -8,6 +8,7 @@ #include "wt_internal.h" +static int __log_newfile(WT_SESSION_IMPL *, bool, bool *); static int __log_openfile( WT_SESSION_IMPL *, WT_FH **, const char *, uint32_t, uint32_t); static int __log_write_internal( @@ -442,6 +443,59 @@ __wt_log_extract_lognum( } /* + * __wt_log_reset -- + * Reset the existing log file to after the given file number. + * Called from recovery when toggling logging back on, it was off + * the previous open but it was on earlier before that toggle. + */ +int +__wt_log_reset(WT_SESSION_IMPL *session, uint32_t lognum) +{ + WT_CONNECTION_IMPL *conn; + WT_DECL_RET; + WT_LOG *log; + uint32_t old_lognum; + u_int i, logcount; + char **logfiles; + + conn = S2C(session); + log = conn->log; + + if (!FLD_ISSET(conn->log_flags, WT_CONN_LOG_ENABLED) || + log->fileid > lognum) + return (0); + + WT_ASSERT(session, F_ISSET(conn, WT_CONN_RECOVERING)); + WT_ASSERT(session, !F_ISSET(conn, WT_CONN_READONLY)); + /* + * We know we're single threaded and called from recovery only when + * toggling logging back on. Therefore the only log files we have are + * old and outdated and the new one created when logging opened before + * recovery. We have to remove all old log files first and then create + * the new one so that log file numbers are contiguous in the file + * system. + */ + WT_RET(__wt_close(session, &log->log_fh)); + WT_RET(__log_get_files(session, + WT_LOG_FILENAME, &logfiles, &logcount)); + for (i = 0; i < logcount; i++) { + WT_ERR(__wt_log_extract_lognum( + session, logfiles[i], &old_lognum)); + WT_ASSERT(session, old_lognum < lognum || lognum == 1); + WT_ERR(__wt_log_remove(session, WT_LOG_FILENAME, old_lognum)); + } + log->fileid = lognum; + + /* Send in true to update connection creation LSNs. */ + WT_WITH_SLOT_LOCK(session, log, + ret = __log_newfile(session, true, NULL)); + WT_ERR(__wt_log_slot_init(session, false)); +err: WT_TRET( + __wt_fs_directory_list_free(session, &logfiles, logcount)); + return (ret); +} + +/* * __log_zero -- * Zero a log file. */ @@ -1611,11 +1665,6 @@ __wt_log_scan(WT_SESSION_IMPL *session, WT_LSN *lsnp, uint32_t flags, if (func == NULL) return (0); - if (LF_ISSET(WT_LOGSCAN_RECOVER)) - __wt_verbose(session, WT_VERB_LOG, - "__wt_log_scan truncating to %" PRIu32 "/%" PRIu32, - log->trunc_lsn.l.file, log->trunc_lsn.l.offset); - if (log != NULL) { allocsize = log->allocsize; @@ -1709,9 +1758,14 @@ advance: /* * Truncate this log file before we move to the next. */ - if (LF_ISSET(WT_LOGSCAN_RECOVER)) + if (LF_ISSET(WT_LOGSCAN_RECOVER) && + __wt_log_cmp(&rd_lsn, &log->trunc_lsn) < 0) { + __wt_verbose(session, WT_VERB_LOG, + "Truncate end of log %" PRIu32 "/%" PRIu32, + rd_lsn.l.file, rd_lsn.l.offset); WT_ERR(__log_truncate(session, &rd_lsn, WT_LOG_FILENAME, 1)); + } /* * If we had a partial record, we'll want to break * now after closing and truncating. Although for now diff --git a/src/third_party/wiredtiger/src/log/log_slot.c b/src/third_party/wiredtiger/src/log/log_slot.c index 97e317ce68c..b23d589c8e2 100644 --- a/src/third_party/wiredtiger/src/log/log_slot.c +++ b/src/third_party/wiredtiger/src/log/log_slot.c @@ -401,7 +401,7 @@ __wt_log_slot_switch(WT_SESSION_IMPL *session, * Initialize the slot array. */ int -__wt_log_slot_init(WT_SESSION_IMPL *session) +__wt_log_slot_init(WT_SESSION_IMPL *session, bool alloc) { WT_CONNECTION_IMPL *conn; WT_DECL_RET; @@ -423,15 +423,17 @@ __wt_log_slot_init(WT_SESSION_IMPL *session) * switch log files very aggressively. Scale back the buffer for * small log file sizes. */ - log->slot_buf_size = (uint32_t)WT_MIN( - (size_t)conn->log_file_max / 10, WT_LOG_SLOT_BUF_SIZE); - for (i = 0; i < WT_SLOT_POOL; i++) { - WT_ERR(__wt_buf_init(session, - &log->slot_pool[i].slot_buf, log->slot_buf_size)); - F_SET(&log->slot_pool[i], WT_SLOT_INIT_FLAGS); + if (alloc) { + log->slot_buf_size = (uint32_t)WT_MIN( + (size_t)conn->log_file_max / 10, WT_LOG_SLOT_BUF_SIZE); + for (i = 0; i < WT_SLOT_POOL; i++) { + WT_ERR(__wt_buf_init(session, + &log->slot_pool[i].slot_buf, log->slot_buf_size)); + F_SET(&log->slot_pool[i], WT_SLOT_INIT_FLAGS); + } + WT_STAT_CONN_SET(session, + log_buffer_size, log->slot_buf_size * WT_SLOT_POOL); } - WT_STAT_CONN_SET(session, - log_buffer_size, log->slot_buf_size * WT_SLOT_POOL); /* * Set up the available slot from the pool the first time. */ diff --git a/src/third_party/wiredtiger/src/lsm/lsm_manager.c b/src/third_party/wiredtiger/src/lsm/lsm_manager.c index b7d9086d10e..62da094b5f7 100644 --- a/src/third_party/wiredtiger/src/lsm/lsm_manager.c +++ b/src/third_party/wiredtiger/src/lsm/lsm_manager.c @@ -295,8 +295,10 @@ __wt_lsm_manager_destroy(WT_SESSION_IMPL *session) manager->lsm_workers == 0); if (manager->lsm_workers > 0) { /* Wait for the main LSM manager thread to finish. */ - while (!F_ISSET(manager, WT_LSM_MANAGER_SHUTDOWN)) + while (!F_ISSET(manager, WT_LSM_MANAGER_SHUTDOWN)) { + WT_STAT_CONN_INCR(session, conn_close_blocked_lsm); __wt_yield(); + } /* Clean up open LSM handles. */ ret = __wt_lsm_tree_close_all(session); diff --git a/src/third_party/wiredtiger/src/meta/meta_table.c b/src/third_party/wiredtiger/src/meta/meta_table.c index aca69d0e6a2..a970694b9d7 100644 --- a/src/third_party/wiredtiger/src/meta/meta_table.c +++ b/src/third_party/wiredtiger/src/meta/meta_table.c @@ -230,12 +230,23 @@ __wt_metadata_remove(WT_SESSION_IMPL *session, const char *key) WT_RET_MSG(session, EINVAL, "%s: remove not supported on the turtle file", key); + /* + * Take, release, and reacquire the metadata cursor. It's complicated, + * but that way the underlying meta-tracking function doesn't have to + * open a second metadata cursor, it can use the session's cached one. + */ WT_RET(__wt_metadata_cursor(session, &cursor)); cursor->set_key(cursor, key); WT_ERR(cursor->search(cursor)); + WT_ERR(__wt_metadata_cursor_release(session, &cursor)); + if (WT_META_TRACKING(session)) WT_ERR(__wt_meta_track_update(session, key)); - WT_ERR(cursor->remove(cursor)); + + WT_ERR(__wt_metadata_cursor(session, &cursor)); + cursor->set_key(cursor, key); + ret = cursor->remove(cursor); + err: WT_TRET(__wt_metadata_cursor_release(session, &cursor)); return (ret); } @@ -266,7 +277,9 @@ __wt_metadata_search(WT_SESSION_IMPL *session, const char *key, char **valuep) * that Coverity complains a lot, add an error check to get some * peace and quiet. */ - if ((ret = __wt_turtle_read(session, key, valuep)) != 0) + WT_WITH_TURTLE_LOCK(session, + ret = __wt_turtle_read(session, key, valuep)); + if (ret != 0) __wt_free(session, *valuep); return (ret); } diff --git a/src/third_party/wiredtiger/src/meta/meta_turtle.c b/src/third_party/wiredtiger/src/meta/meta_turtle.c index 5a089471059..f7ea6fe99c5 100644 --- a/src/third_party/wiredtiger/src/meta/meta_turtle.c +++ b/src/third_party/wiredtiger/src/meta/meta_turtle.c @@ -246,6 +246,9 @@ __wt_turtle_read(WT_SESSION_IMPL *session, const char *key, char **valuep) *valuep = NULL; + /* Require single-threading. */ + WT_ASSERT(session, F_ISSET(session, WT_SESSION_LOCKED_TURTLE)); + /* * Open the turtle file; there's one case where we won't find the turtle * file, yet still succeed. We create the metadata file before creating @@ -302,6 +305,9 @@ __wt_turtle_update(WT_SESSION_IMPL *session, const char *key, const char *value) fs = NULL; + /* Require single-threading. */ + WT_ASSERT(session, F_ISSET(session, WT_SESSION_LOCKED_TURTLE)); + /* * Create the turtle setup file: we currently re-write it from scratch * every time. diff --git a/src/third_party/wiredtiger/src/os_posix/os_mtx_cond.c b/src/third_party/wiredtiger/src/os_posix/os_mtx_cond.c index fe010b62305..e4a6683dee9 100644 --- a/src/third_party/wiredtiger/src/os_posix/os_mtx_cond.c +++ b/src/third_party/wiredtiger/src/os_posix/os_mtx_cond.c @@ -19,11 +19,19 @@ __wt_cond_alloc(WT_SESSION_IMPL *session, const char *name, WT_CONDVAR **condp) WT_DECL_RET; WT_RET(__wt_calloc_one(session, &cond)); - WT_ERR(pthread_mutex_init(&cond->mtx, NULL)); - /* Initialize the condition variable to permit self-blocking. */ +#ifdef HAVE_PTHREAD_COND_MONOTONIC + { + pthread_condattr_t condattr; + + WT_ERR(pthread_condattr_init(&condattr)); + WT_ERR(pthread_condattr_setclock(&condattr, CLOCK_MONOTONIC)); + WT_ERR(pthread_cond_init(&cond->cond, &condattr)); + } +#else WT_ERR(pthread_cond_init(&cond->cond, NULL)); +#endif cond->name = name; cond->waiters = 0; @@ -79,7 +87,26 @@ __wt_cond_wait_signal(WT_SESSION_IMPL *session, WT_CONDVAR *cond, goto skipping; if (usecs > 0) { - __wt_epoch(session, &ts); + /* + * Get the current time as the basis for calculating when the + * wait should end. Prefer a monotonic clock source to avoid + * unexpectedly long sleeps when the system clock is adjusted. + * + * Failing that, query the time directly and don't attempt to + * correct for the clock moving backwards, which would result + * in a sleep that is too long by however much the clock is + * updated. This isn't as good as a monotonic clock source but + * makes the window of vulnerability smaller (i.e., the + * calculated time is only incorrect if the system clock + * changes in between us querying it and waiting). + */ +#ifdef HAVE_PTHREAD_COND_MONOTONIC + WT_SYSCALL_RETRY(clock_gettime(CLOCK_MONOTONIC, &ts), ret); + if (ret != 0) + WT_PANIC_MSG(session, ret, "clock_gettime"); +#else + __wt_epoch_raw(session, &ts); +#endif ts.tv_sec += (time_t) (((uint64_t)ts.tv_nsec + WT_THOUSAND * usecs) / WT_BILLION); ts.tv_nsec = (long) diff --git a/src/third_party/wiredtiger/src/os_posix/os_time.c b/src/third_party/wiredtiger/src/os_posix/os_time.c index fe337fea7cf..25a08d62355 100644 --- a/src/third_party/wiredtiger/src/os_posix/os_time.c +++ b/src/third_party/wiredtiger/src/os_posix/os_time.c @@ -9,14 +9,12 @@ #include "wt_internal.h" /* - * __wt_epoch -- - * Return the time since the Epoch. + * __wt_epoch_raw -- + * Return the time since the Epoch as reported by a system call. */ void -__wt_epoch(WT_SESSION_IMPL *session, struct timespec *tsp) - WT_GCC_FUNC_ATTRIBUTE((visibility("default"))) +__wt_epoch_raw(WT_SESSION_IMPL *session, struct timespec *tsp) { - struct timespec tmp; WT_DECL_RET; /* @@ -28,19 +26,10 @@ __wt_epoch(WT_SESSION_IMPL *session, struct timespec *tsp) tsp->tv_sec = 0; tsp->tv_nsec = 0; - /* - * Read into a local variable so that we're comparing the correct - * value when we check for monotonic increasing time. There are - * many places we read into an unlocked global variable. - */ #if defined(HAVE_CLOCK_GETTIME) - WT_SYSCALL_RETRY(clock_gettime(CLOCK_REALTIME, &tmp), ret); - if (ret == 0) { - __wt_time_check_monotonic(session, &tmp); - tsp->tv_sec = tmp.tv_sec; - tsp->tv_nsec = tmp.tv_nsec; + WT_SYSCALL_RETRY(clock_gettime(CLOCK_REALTIME, tsp), ret); + if (ret == 0) return; - } WT_PANIC_MSG(session, ret, "clock_gettime"); #elif defined(HAVE_GETTIMEOFDAY) { @@ -48,10 +37,8 @@ __wt_epoch(WT_SESSION_IMPL *session, struct timespec *tsp) WT_SYSCALL_RETRY(gettimeofday(&v, NULL), ret); if (ret == 0) { - tmp.tv_sec = v.tv_sec; - tmp.tv_nsec = v.tv_usec * WT_THOUSAND; - __wt_time_check_monotonic(session, &tmp); - *tsp = tmp; + tsp->tv_sec = v.tv_sec; + tsp->tv_nsec = v.tv_usec * WT_THOUSAND; return; } WT_PANIC_MSG(session, ret, "gettimeofday"); diff --git a/src/third_party/wiredtiger/src/os_win/os_time.c b/src/third_party/wiredtiger/src/os_win/os_time.c index ba71341ab22..84c06bed6e5 100644 --- a/src/third_party/wiredtiger/src/os_win/os_time.c +++ b/src/third_party/wiredtiger/src/os_win/os_time.c @@ -9,24 +9,23 @@ #include "wt_internal.h" /* - * __wt_epoch -- - * Return the time since the Epoch. + * __wt_epoch_raw -- + * Return the time since the Epoch as reported by the system. */ void -__wt_epoch(WT_SESSION_IMPL *session, struct timespec *tsp) +__wt_epoch_raw(WT_SESSION_IMPL *session, struct timespec *tsp) { - struct timespec tmp; FILETIME time; uint64_t ns100; + WT_UNUSED(session); + GetSystemTimeAsFileTime(&time); ns100 = (((int64_t)time.dwHighDateTime << 32) + time.dwLowDateTime) - 116444736000000000LL; - tmp.tv_sec = ns100 / 10000000; - tmp.tv_nsec = (long)((ns100 % 10000000) * 100); - __wt_time_check_monotonic(session, &tmp); - *tsp = tmp; + tsp->tv_sec = ns100 / 10000000; + tsp->tv_nsec = (long)((ns100 % 10000000) * 100); } /* diff --git a/src/third_party/wiredtiger/src/reconcile/rec_write.c b/src/third_party/wiredtiger/src/reconcile/rec_write.c index e59d9796352..688efa10398 100644 --- a/src/third_party/wiredtiger/src/reconcile/rec_write.c +++ b/src/third_party/wiredtiger/src/reconcile/rec_write.c @@ -45,12 +45,14 @@ typedef struct { uint64_t orig_btree_checkpoint_gen; uint64_t orig_txn_checkpoint_gen; - /* Track the page's maximum transaction ID. */ + /* Track the oldest transaction running when reconciliation starts. */ + uint64_t last_running; + uint64_t max_txn; - /* Track if all updates were skipped. */ - uint64_t update_cnt; - uint64_t update_skip_cnt; + uint64_t update_mem_all; /* Total update memory size */ + uint64_t update_mem_saved; /* Saved update memory size */ + uint64_t update_mem_uncommitted;/* Uncommitted update memory size */ /* * When we can't mark the page clean (for example, checkpoint found some @@ -338,7 +340,8 @@ static int __rec_split_write(WT_SESSION_IMPL *, WT_RECONCILE *, WT_BOUNDARY *, WT_ITEM *, bool); static int __rec_update_las( WT_SESSION_IMPL *, WT_RECONCILE *, uint32_t, WT_BOUNDARY *); -static int __rec_write_check_complete(WT_SESSION_IMPL *, WT_RECONCILE *); +static int __rec_write_check_complete( + WT_SESSION_IMPL *, WT_RECONCILE *, bool *); static int __rec_write_init(WT_SESSION_IMPL *, WT_REF *, uint32_t, WT_SALVAGE_COOKIE *, void *); static void __rec_write_page_status(WT_SESSION_IMPL *, WT_RECONCILE *); @@ -437,7 +440,7 @@ __wt_reconcile(WT_SESSION_IMPL *session, WT_REF *ref, /* Checks for a successful reconciliation. */ if (ret == 0) - ret = __rec_write_check_complete(session, r); + ret = __rec_write_check_complete(session, r, lookaside_retryp); /* Wrap up the page reconciliation. */ if (ret == 0 && (ret = __rec_write_wrapup(session, r, page)) == 0) @@ -448,14 +451,6 @@ __wt_reconcile(WT_SESSION_IMPL *session, WT_REF *ref, /* Release the reconciliation lock. */ WT_PAGE_UNLOCK(session, page); - /* - * If our caller can configure lookaside table reconciliation, flag if - * that's worth trying. The lookaside table doesn't help if we skipped - * updates, it can only help with older readers preventing eviction. - */ - if (lookaside_retryp != NULL && r->update_cnt == r->update_skip_cnt) - *lookaside_retryp = true; - /* Update statistics. */ WT_STAT_CONN_INCR(session, rec_pages); WT_STAT_DATA_INCR(session, rec_pages); @@ -558,13 +553,21 @@ __rec_las_checkpoint_test(WT_SESSION_IMPL *session, WT_RECONCILE *r) /* * __rec_write_check_complete -- - * Check that reconciliation should complete + * Check that reconciliation should complete. */ static int -__rec_write_check_complete(WT_SESSION_IMPL *session, WT_RECONCILE *r) +__rec_write_check_complete( + WT_SESSION_IMPL *session, WT_RECONCILE *r, bool *lookaside_retryp) { - WT_BOUNDARY *bnd; - size_t i; + /* + * Tests in this function are lookaside tests and tests to decide if + * rewriting a page in memory is worth doing. In-memory configurations + * can't use a lookaside table, and we ignore page rewrite desirability + * checks for in-memory eviction because a small cache can force us to + * rewrite every possible page. + */ + if (F_ISSET(r, WT_EVICT_IN_MEMORY)) + return (0); /* * If we have used the lookaside table, check for a lookaside table and @@ -574,19 +577,62 @@ __rec_write_check_complete(WT_SESSION_IMPL *session, WT_RECONCILE *r) return (EBUSY); /* - * If we are doing update/restore based eviction, confirm part of the - * page is being discarded, or at least 10% of the updates won't have - * to be re-instantiated. Otherwise, it isn't progress, don't bother. + * Eviction can configure lookaside table reconciliation, consider if + * it's worth giving up this reconciliation attempt and falling back to + * using the lookaside table. We continue with evict/restore if + * switching to the lookaside doesn't make sense for any reason: we + * won't retry an evict/restore reconciliation until/unless the + * transactional system moves forward, so at worst it's a single wasted + * effort. + * + * First, check if the lookaside table is a possible alternative. */ - if (F_ISSET(r, WT_EVICT_UPDATE_RESTORE)) { - for (bnd = r->bnd, i = 0; i < r->bnd_entries; ++bnd, ++i) - if (bnd->supd == NULL) - break; - if (i == r->bnd_entries && - r->update_cnt / 10 >= r->update_skip_cnt) - return (EBUSY); - } - return (0); + if (lookaside_retryp == NULL) + return (0); + + /* + * We only suggest lookaside if currently in an evict/restore attempt + * and some updates were saved. Our caller sets the evict/restore flag + * based on various conditions (like if this is a leaf page), which is + * why we're testing that flag instead of a set of other conditions. + * If no updates were saved, eviction will succeed without needing to + * restore anything. + */ + if (!F_ISSET(r, WT_EVICT_UPDATE_RESTORE) || r->bnd->supd == NULL) + return (0); + + /* + * Check if this reconciliation attempt is making progress. If there's + * any sign of progress, don't fall back to the lookaside table. + * + * Check if the current reconciliation split, in which case we'll + * likely get to write at least one of the blocks. If that page is + * empty, that's also progress. + */ + if (r->bnd_next != 1) + return (0); + + /* + * Check if the current reconciliation applied some updates, in which + * case evict/restore should gain us some space. + */ + if (r->update_mem_saved != r->update_mem_all) + return (0); + + /* + * Check if lookaside eviction is possible. If any of the updates we + * saw were uncommitted, the lookaside table cannot be used: it only + * helps with older readers preventing eviction. + */ + if (r->update_mem_uncommitted != 0) + return (0); + + /* + * The current evict/restore approach shows no signs of being useful, + * lookaside is possible, suggest the lookaside table. + */ + *lookaside_retryp = true; + return (EBUSY); } /* @@ -849,6 +895,16 @@ __rec_write_init(WT_SESSION_IMPL *session, WT_ORDERED_READ(r->orig_write_gen, page->modify->write_gen); /* + * Cache the oldest running transaction ID. This is used to check + * whether updates seen by reconciliation have committed. We keep a + * cached copy to avoid races where a concurrent transaction could + * abort while reconciliation is examining its updates. This way, any + * transaction running when reconciliation starts is considered + * uncommitted. + */ + WT_ORDERED_READ(r->last_running, S2C(session)->txn_global.last_running); + + /* * Lookaside table eviction is configured when eviction gets aggressive, * adjust the flags for cases we don't support. */ @@ -891,7 +947,7 @@ __rec_write_init(WT_SESSION_IMPL *session, r->max_txn = WT_TXN_NONE; /* Track if all updates were skipped. */ - r->update_cnt = r->update_skip_cnt = 0; + r->update_mem_all = r->update_mem_saved = r->update_mem_uncommitted = 0; /* Track if the page can be marked clean. */ r->leave_dirty = false; @@ -1115,7 +1171,7 @@ __rec_txn_read(WT_SESSION_IMPL *session, WT_RECONCILE *r, WT_DECL_ITEM(tmp); WT_PAGE *page; WT_UPDATE *append, *upd, *upd_list; - size_t notused; + size_t notused, update_mem; uint64_t max_txn, min_txn, txnid; bool append_origv, skipped; @@ -1136,36 +1192,64 @@ __rec_txn_read(WT_SESSION_IMPL *session, WT_RECONCILE *r, } else upd_list = ins->upd; - ++r->update_cnt; - for (skipped = false, - max_txn = WT_TXN_NONE, min_txn = UINT64_MAX, - upd = upd_list; upd != NULL; upd = upd->next) { - if ((txnid = upd->txnid) == WT_TXN_ABORTED) - continue; + skipped = false; + update_mem = 0; + max_txn = WT_TXN_NONE; + min_txn = UINT64_MAX; - /* Track the largest/smallest transaction IDs on the list. */ - if (WT_TXNID_LT(max_txn, txnid)) - max_txn = txnid; - if (WT_TXNID_LT(txnid, min_txn)) - min_txn = txnid; + if (F_ISSET(r, WT_EVICTING)) { + /* Discard obsolete updates. */ + if ((upd = __wt_update_obsolete_check( + session, page, upd_list->next)) != NULL) + __wt_update_obsolete_free(session, page, upd); + + for (upd = upd_list; upd != NULL; upd = upd->next) { + /* Track the total memory in the update chain. */ + update_mem += WT_UPDATE_MEMSIZE(upd); + + if ((txnid = upd->txnid) == WT_TXN_ABORTED) + continue; - /* - * Find the first update we can use. - */ - if (F_ISSET(r, WT_EVICTING)) { /* + * Track the largest/smallest transaction IDs on the + * list. + */ + if (WT_TXNID_LT(max_txn, txnid)) + max_txn = txnid; + if (WT_TXNID_LT(txnid, min_txn)) + min_txn = txnid; + + /* + * Find the first update we can use. + * * Eviction can write any committed update. * * When reconciling for eviction, track whether any * uncommitted updates are found. + * + * When reconciling for eviction, track the memory held + * by the update chain. */ - if (__wt_txn_committed(session, txnid)) { - if (*updp == NULL) - *updp = upd; - } else + if (WT_TXNID_LE(r->last_running, txnid)) { skipped = true; - } else { + continue; + } + + if (*updp == NULL) + *updp = upd; + } + } else + for (upd = upd_list; upd != NULL; upd = upd->next) { + if ((txnid = upd->txnid) == WT_TXN_ABORTED) + continue; + + /* Track the largest transaction ID on the list. */ + if (WT_TXNID_LT(max_txn, txnid)) + max_txn = txnid; + /* + * Find the first update we can use. + * * Checkpoint can only write updates visible as of its * snapshot. * @@ -1180,7 +1264,8 @@ __rec_txn_read(WT_SESSION_IMPL *session, WT_RECONCILE *r, skipped = true; } } - } + + r->update_mem_all += update_mem; /* * If all of the updates were aborted, quit. This test is not strictly @@ -1227,12 +1312,6 @@ __rec_txn_read(WT_SESSION_IMPL *session, WT_RECONCILE *r, txnid != S2C(session)->txn_global.checkpoint_txnid || WT_SESSION_IS_CHECKPOINT(session)); #endif - - /* - * Track how many update chains we saw vs. how many update - * chains had an entry we skipped. - */ - ++r->update_skip_cnt; return (0); } @@ -1276,6 +1355,23 @@ __rec_txn_read(WT_SESSION_IMPL *session, WT_RECONCILE *r, if (skipped && !F_ISSET(r, WT_EVICT_UPDATE_RESTORE)) return (EBUSY); + /* + * Track the memory required by the update chain. + * + * A page with no uncommitted (skipped) updates, that can't be evicted + * because some updates aren't yet globally visible, can be evicted by + * writing previous versions of the updates to the lookaside file. That + * test is just checking if the skipped updates memory is zero. + * + * If that's not possible (there are skipped updates), we can rewrite + * the pages in-memory, but we don't want to unless there's memory to + * recover. That test is comparing the memory we'd recover to the memory + * we'd have to re-instantiate as part of the rewrite. + */ + r->update_mem_saved += update_mem; + if (skipped) + r->update_mem_uncommitted += update_mem; + append_origv = false; if (F_ISSET(r, WT_EVICT_UPDATE_RESTORE)) { /* @@ -1562,7 +1658,7 @@ __rec_child_modify(WT_SESSION_IMPL *session, * not reserved for our exclusive use, there are other page states that * must be considered. */ - for (;; __wt_yield()) + for (;; __wt_yield()) { switch (r->tested_ref_state = ref->state) { case WT_REF_DISK: /* On disk, not modified by definition. */ @@ -1673,6 +1769,8 @@ __rec_child_modify(WT_SESSION_IMPL *session, WT_ILLEGAL_VALUE(session); } + WT_STAT_CONN_INCR(session, child_modify_blocked_page); + } in_memory: /* diff --git a/src/third_party/wiredtiger/src/schema/schema_alter.c b/src/third_party/wiredtiger/src/schema/schema_alter.c index 26d800aa98e..edb3c6f77ae 100644 --- a/src/third_party/wiredtiger/src/schema/schema_alter.c +++ b/src/third_party/wiredtiger/src/schema/schema_alter.c @@ -61,13 +61,16 @@ __alter_colgroup( { WT_COLGROUP *colgroup; WT_DECL_RET; + WT_TABLE *table; WT_ASSERT(session, F_ISSET(session, WT_SESSION_LOCKED_TABLE)); /* If we can get the colgroup, perform any potential alterations. */ if ((ret = __wt_schema_get_colgroup( - session, uri, false, NULL, &colgroup)) == 0) + session, uri, false, &table, &colgroup)) == 0) { WT_TRET(__wt_schema_alter(session, colgroup->source, cfg)); + WT_TRET(__wt_schema_release_table(session, table)); + } return (ret); } @@ -82,11 +85,14 @@ __alter_index( { WT_INDEX *idx; WT_DECL_RET; + WT_TABLE *table; /* If we can get the index, perform any potential alterations. */ if ((ret = __wt_schema_get_index( - session, uri, false, NULL, &idx)) == 0) + session, uri, false, &table, &idx)) == 0) { WT_TRET(__wt_schema_alter(session, idx->source, cfg)); + WT_TRET(__wt_schema_release_table(session, table)); + } return (ret); } @@ -127,7 +133,8 @@ __alter_table(WT_SESSION_IMPL *session, const char *uri, const char *cfg[]) WT_ERR(__wt_schema_alter( session, colgroup->source, cfg)); } -err: __wt_schema_release_table(session, table); + +err: WT_TRET(__wt_schema_release_table(session, table)); return (ret); } diff --git a/src/third_party/wiredtiger/src/schema/schema_create.c b/src/third_party/wiredtiger/src/schema/schema_create.c index 0677fa711a5..49c0274994c 100644 --- a/src/third_party/wiredtiger/src/schema/schema_create.c +++ b/src/third_party/wiredtiger/src/schema/schema_create.c @@ -273,7 +273,7 @@ err: __wt_free(session, cgconf); __wt_buf_free(session, &fmt); __wt_buf_free(session, &namebuf); - __wt_schema_release_table(session, table); + WT_TRET(__wt_schema_release_table(session, table)); return (ret); } @@ -540,7 +540,7 @@ err: __wt_free(session, idxconf); __wt_buf_free(session, &fmt); __wt_buf_free(session, &namebuf); - __wt_schema_release_table(session, table); + WT_TRET(__wt_schema_release_table(session, table)); return (ret); } @@ -615,7 +615,7 @@ err: if (table != NULL) { } } if (table != NULL) - __wt_schema_release_table(session, table); + WT_TRET(__wt_schema_release_table(session, table)); __wt_free(session, cgname); __wt_free(session, tableconf); return (ret); diff --git a/src/third_party/wiredtiger/src/schema/schema_drop.c b/src/third_party/wiredtiger/src/schema/schema_drop.c index 49801e4e5f9..18817c4fa2d 100644 --- a/src/third_party/wiredtiger/src/schema/schema_drop.c +++ b/src/third_party/wiredtiger/src/schema/schema_drop.c @@ -67,6 +67,7 @@ __drop_colgroup( session, uri, force, &table, &colgroup)) == 0) { table->cg_complete = false; WT_TRET(__wt_schema_drop(session, colgroup->source, cfg)); + WT_TRET(__wt_schema_release_table(session, table)); } WT_TRET(__wt_metadata_remove(session, uri)); @@ -90,6 +91,7 @@ __drop_index( session, uri, force, &table, &idx)) == 0) { table->idx_complete = false; WT_TRET(__wt_schema_drop(session, idx->source, cfg)); + WT_TRET(__wt_schema_release_table(session, table)); } WT_TRET(__wt_metadata_remove(session, uri)); @@ -151,7 +153,7 @@ __drop_table(WT_SESSION_IMPL *session, const char *uri, const char *cfg[]) WT_ERR(__wt_metadata_remove(session, uri)); err: if (table != NULL) - __wt_schema_release_table(session, table); + WT_TRET(__wt_schema_release_table(session, table)); return (ret); } diff --git a/src/third_party/wiredtiger/src/schema/schema_list.c b/src/third_party/wiredtiger/src/schema/schema_list.c index 74ef5135a4a..434038c6bc8 100644 --- a/src/third_party/wiredtiger/src/schema/schema_list.c +++ b/src/third_party/wiredtiger/src/schema/schema_list.c @@ -30,9 +30,12 @@ __schema_add_table(WT_SESSION_IMPL *session, session, name, namelen, ok_incomplete, &table)); WT_RET(ret); - bucket = table->name_hash % WT_HASH_ARRAY_SIZE; - TAILQ_INSERT_HEAD(&session->tables, table, q); - TAILQ_INSERT_HEAD(&session->tablehash[bucket], table, hashq); + if (!table->is_simple || F_ISSET(S2C(session), WT_CONN_TABLE_CACHE)) { + bucket = table->name_hash % WT_HASH_ARRAY_SIZE; + TAILQ_INSERT_HEAD(&session->tables, table, q); + TAILQ_INSERT_HEAD(&session->tablehash[bucket], table, hashq); + } + *tablep = table; return (0); @@ -112,11 +115,14 @@ __wt_schema_get_table(WT_SESSION_IMPL *session, * __wt_schema_release_table -- * Release a table handle. */ -void +int __wt_schema_release_table(WT_SESSION_IMPL *session, WT_TABLE *table) { WT_ASSERT(session, table->refcnt > 0); - --table->refcnt; + if (--table->refcnt == 0 && + table->is_simple && !F_ISSET(S2C(session), WT_CONN_TABLE_CACHE)) + WT_RET(__wt_schema_destroy_table(session, &table)); + return (0); } /* @@ -229,9 +235,11 @@ __wt_schema_remove_table(WT_SESSION_IMPL *session, WT_TABLE *table) uint64_t bucket; WT_ASSERT(session, table->refcnt <= 1); - bucket = table->name_hash % WT_HASH_ARRAY_SIZE; - TAILQ_REMOVE(&session->tables, table, q); - TAILQ_REMOVE(&session->tablehash[bucket], table, hashq); + if (!table->is_simple || F_ISSET(S2C(session), WT_CONN_TABLE_CACHE)) { + bucket = table->name_hash % WT_HASH_ARRAY_SIZE; + TAILQ_REMOVE(&session->tables, table, q); + TAILQ_REMOVE(&session->tablehash[bucket], table, hashq); + } return (__wt_schema_destroy_table(session, &table)); } @@ -249,3 +257,34 @@ __wt_schema_close_tables(WT_SESSION_IMPL *session) WT_TRET(__wt_schema_remove_table(session, table)); return (ret); } + +/* + * __wt_schema_sweep_tables -- + * Close all idle, obsolete tables in a session. + */ +int +__wt_schema_sweep_tables(WT_SESSION_IMPL *session) +{ + WT_TABLE *table, *next; + uint64_t schema_gen; + bool old_table_busy; + + WT_ORDERED_READ(schema_gen, S2C(session)->schema_gen); + if (schema_gen == session->table_sweep_gen) + return (0); + + old_table_busy = false; + TAILQ_FOREACH_SAFE(table, &session->tables, q, next) + if (table->schema_gen != schema_gen) { + if (table->refcnt == 0) + WT_RET(__wt_schema_remove_table( + session, table)); + else + old_table_busy = true; + } + + if (!old_table_busy) + session->table_sweep_gen = schema_gen; + + return (0); +} diff --git a/src/third_party/wiredtiger/src/schema/schema_open.c b/src/third_party/wiredtiger/src/schema/schema_open.c index 44bd66e011a..2df6bae45f3 100644 --- a/src/third_party/wiredtiger/src/schema/schema_open.c +++ b/src/third_party/wiredtiger/src/schema/schema_open.c @@ -425,37 +425,40 @@ __schema_open_table(WT_SESSION_IMPL *session, WT_DECL_RET; WT_TABLE *table; const char *tconfig; - char *tablename; *tablep = NULL; cursor = NULL; table = NULL; - tablename = NULL; WT_ASSERT(session, F_ISSET(session, WT_SESSION_LOCKED_TABLE)); + WT_ERR(__wt_calloc_one(session, &table)); + table->name_hash = __wt_hash_city64(name, namelen); + WT_ERR(__wt_scr_alloc(session, 0, &buf)); WT_ERR(__wt_buf_fmt(session, buf, "table:%.*s", (int)namelen, name)); - WT_ERR(__wt_strndup(session, buf->data, buf->size, &tablename)); + WT_ERR(__wt_strndup(session, buf->data, buf->size, &table->name)); + /* + * Don't hold the metadata cursor pinned, we call functions that use it + * to retrieve column group information. + */ WT_ERR(__wt_metadata_cursor(session, &cursor)); - cursor->set_key(cursor, tablename); - WT_ERR(cursor->search(cursor)); - WT_ERR(cursor->get_value(cursor, &tconfig)); - - WT_ERR(__wt_calloc_one(session, &table)); - table->name = tablename; - tablename = NULL; - table->name_hash = __wt_hash_city64(name, namelen); - - WT_ERR(__wt_config_getones(session, tconfig, "columns", &cval)); + cursor->set_key(cursor, table->name); + if ((ret = cursor->search(cursor)) == 0 && + (ret = cursor->get_value(cursor, &tconfig)) == 0) + ret = __wt_strdup(session, tconfig, &table->config); + WT_TRET(__wt_metadata_cursor_release(session, &cursor)); + WT_ERR(ret); - WT_ERR(__wt_config_getones(session, tconfig, "key_format", &cval)); + WT_ERR(__wt_config_getones(session, table->config, "columns", &cval)); + WT_ERR(__wt_config_getones( + session, table->config, "key_format", &cval)); WT_ERR(__wt_strndup(session, cval.str, cval.len, &table->key_format)); - WT_ERR(__wt_config_getones(session, tconfig, "value_format", &cval)); + WT_ERR(__wt_config_getones( + session, table->config, "value_format", &cval)); WT_ERR(__wt_strndup(session, cval.str, cval.len, &table->value_format)); - WT_ERR(__wt_strdup(session, tconfig, &table->config)); /* Point to some items in the copy to save re-parsing. */ WT_ERR(__wt_config_getones(session, table->config, @@ -491,7 +494,7 @@ __schema_open_table(WT_SESSION_IMPL *session, if (table->ncolgroups > 0 && table->is_simple) WT_ERR_MSG(session, EINVAL, - "%s requires a table with named columns", tablename); + "%s requires a table with named columns", table->name); WT_ERR(__wt_calloc_def(session, WT_COLGROUPS(table), &table->cgroups)); WT_ERR(__wt_schema_open_colgroups(session, table)); @@ -509,9 +512,7 @@ __schema_open_table(WT_SESSION_IMPL *session, if (0) { err: WT_TRET(__wt_schema_destroy_table(session, &table)); } - WT_TRET(__wt_metadata_cursor_release(session, &cursor)); - __wt_free(session, tablename); __wt_scr_free(session, &buf); return (ret); } @@ -529,8 +530,8 @@ __wt_schema_get_colgroup(WT_SESSION_IMPL *session, const char *tablename, *tend; u_int i; - if (tablep != NULL) - *tablep = NULL; + WT_ASSERT(session, tablep != NULL); + *tablep = NULL; *colgroupp = NULL; tablename = uri; @@ -547,15 +548,12 @@ __wt_schema_get_colgroup(WT_SESSION_IMPL *session, colgroup = table->cgroups[i]; if (strcmp(colgroup->name, uri) == 0) { *colgroupp = colgroup; - if (tablep != NULL) - *tablep = table; - else - __wt_schema_release_table(session, table); + *tablep = table; return (0); } } - __wt_schema_release_table(session, table); + WT_RET(__wt_schema_release_table(session, table)); if (quiet) WT_RET(ENOENT); WT_RET_MSG(session, ENOENT, "%s not found in table", uri); @@ -575,8 +573,8 @@ __wt_schema_get_index(WT_SESSION_IMPL *session, const char *tablename, *tend; u_int i; - if (tablep != NULL) - *tablep = NULL; + WT_ASSERT(session, tablep != NULL); + *tablep = NULL; *indexp = NULL; tablename = uri; @@ -591,11 +589,8 @@ __wt_schema_get_index(WT_SESSION_IMPL *session, for (i = 0; i < table->nindices; i++) { idx = table->indices[i]; if (idx != NULL && strcmp(idx->name, uri) == 0) { - if (tablep != NULL) - *tablep = table; - else - __wt_schema_release_table(session, table); *indexp = idx; + *tablep = table; return (0); } } @@ -603,15 +598,12 @@ __wt_schema_get_index(WT_SESSION_IMPL *session, /* Otherwise, open it. */ WT_ERR(__wt_schema_open_index( session, table, tend + 1, strlen(tend + 1), indexp)); - if (tablep != NULL) - *tablep = table; + *tablep = table; + return (0); -err: __wt_schema_release_table(session, table); +err: WT_TRET(__wt_schema_release_table(session, table)); WT_RET(ret); - if (*indexp != NULL) - return (0); - if (quiet) WT_RET(ENOENT); WT_RET_MSG(session, ENOENT, "%s not found in table", uri); diff --git a/src/third_party/wiredtiger/src/schema/schema_rename.c b/src/third_party/wiredtiger/src/schema/schema_rename.c index a374f4c2831..9effedd2cde 100644 --- a/src/third_party/wiredtiger/src/schema/schema_rename.c +++ b/src/third_party/wiredtiger/src/schema/schema_rename.c @@ -233,7 +233,7 @@ __rename_table(WT_SESSION_IMPL *session, WT_ERR(__metadata_rename(session, uri, newuri)); err: if (table != NULL) - __wt_schema_release_table(session, table); + WT_TRET(__wt_schema_release_table(session, table)); return (ret); } diff --git a/src/third_party/wiredtiger/src/schema/schema_stat.c b/src/third_party/wiredtiger/src/schema/schema_stat.c index 345f9164e9b..9bcd2439619 100644 --- a/src/third_party/wiredtiger/src/schema/schema_stat.c +++ b/src/third_party/wiredtiger/src/schema/schema_stat.c @@ -19,14 +19,17 @@ __wt_curstat_colgroup_init(WT_SESSION_IMPL *session, WT_COLGROUP *colgroup; WT_DECL_ITEM(buf); WT_DECL_RET; + WT_TABLE *table; - WT_RET(__wt_schema_get_colgroup(session, uri, false, NULL, &colgroup)); + WT_RET(__wt_schema_get_colgroup( + session, uri, false, &table, &colgroup)); WT_RET(__wt_scr_alloc(session, 0, &buf)); WT_ERR(__wt_buf_fmt(session, buf, "statistics:%s", colgroup->source)); ret = __wt_curstat_init(session, buf->data, NULL, cfg, cst); err: __wt_scr_free(session, &buf); + WT_TRET(__wt_schema_release_table(session, table)); return (ret); } @@ -41,14 +44,16 @@ __wt_curstat_index_init(WT_SESSION_IMPL *session, WT_DECL_ITEM(buf); WT_DECL_RET; WT_INDEX *idx; + WT_TABLE *table; - WT_RET(__wt_schema_get_index(session, uri, false, NULL, &idx)); + WT_RET(__wt_schema_get_index(session, uri, false, &table, &idx)); WT_RET(__wt_scr_alloc(session, 0, &buf)); WT_ERR(__wt_buf_fmt(session, buf, "statistics:%s", idx->source)); ret = __wt_curstat_init(session, buf->data, NULL, cfg, cst); err: __wt_scr_free(session, &buf); + WT_TRET(__wt_schema_release_table(session, table)); return (ret); } @@ -184,7 +189,7 @@ __wt_curstat_table_init(WT_SESSION_IMPL *session, __wt_curstat_dsrc_final(cst); -err: __wt_schema_release_table(session, table); +err: WT_TRET(__wt_schema_release_table(session, table)); __wt_scr_free(session, &buf); return (ret); diff --git a/src/third_party/wiredtiger/src/schema/schema_truncate.c b/src/third_party/wiredtiger/src/schema/schema_truncate.c index 563bafa8ffc..3046305e819 100644 --- a/src/third_party/wiredtiger/src/schema/schema_truncate.c +++ b/src/third_party/wiredtiger/src/schema/schema_truncate.c @@ -33,7 +33,7 @@ __truncate_table(WT_SESSION_IMPL *session, const char *uri, const char *cfg[]) WT_ERR(__wt_schema_truncate( session, table->indices[i]->source, cfg)); -err: __wt_schema_release_table(session, table); +err: WT_TRET(__wt_schema_release_table(session, table)); return (ret); } diff --git a/src/third_party/wiredtiger/src/schema/schema_worker.c b/src/third_party/wiredtiger/src/schema/schema_worker.c index 62cdd7d367b..9fb68720723 100644 --- a/src/third_party/wiredtiger/src/schema/schema_worker.c +++ b/src/third_party/wiredtiger/src/schema/schema_worker.c @@ -64,14 +64,17 @@ __wt_schema_worker(WT_SESSION_IMPL *session, } } else if (WT_PREFIX_MATCH(uri, "colgroup:")) { WT_ERR(__wt_schema_get_colgroup( - session, uri, false, NULL, &colgroup)); + session, uri, false, &table, &colgroup)); WT_ERR(__wt_schema_worker(session, colgroup->source, file_func, name_func, cfg, open_flags)); + WT_ERR(__wt_schema_release_table(session, table)); } else if (WT_PREFIX_SKIP(tablename, "index:")) { idx = NULL; - WT_ERR(__wt_schema_get_index(session, uri, false, NULL, &idx)); + WT_ERR(__wt_schema_get_index( + session, uri, false, &table, &idx)); WT_ERR(__wt_schema_worker(session, idx->source, file_func, name_func, cfg, open_flags)); + WT_ERR(__wt_schema_release_table(session, table)); } else if (WT_PREFIX_MATCH(uri, "lsm:")) { WT_ERR(__wt_lsm_tree_worker(session, uri, file_func, name_func, cfg, open_flags)); @@ -128,6 +131,6 @@ __wt_schema_worker(WT_SESSION_IMPL *session, WT_ERR(__wt_bad_object_type(session, uri)); err: if (table != NULL) - __wt_schema_release_table(session, table); + WT_TRET(__wt_schema_release_table(session, table)); return (ret); } diff --git a/src/third_party/wiredtiger/src/session/session_api.c b/src/third_party/wiredtiger/src/session/session_api.c index 89a5a2c633d..386084b78d9 100644 --- a/src/third_party/wiredtiger/src/session/session_api.c +++ b/src/third_party/wiredtiger/src/session/session_api.c @@ -328,6 +328,7 @@ __session_open_cursor_int(WT_SESSION_IMPL *session, const char *uri, WT_COLGROUP *colgroup; WT_DATA_SOURCE *dsrc; WT_DECL_RET; + WT_TABLE *table; *cursorp = NULL; @@ -355,9 +356,10 @@ __session_open_cursor_int(WT_SESSION_IMPL *session, const char *uri, * the underlying data source. */ WT_RET(__wt_schema_get_colgroup( - session, uri, false, NULL, &colgroup)); + session, uri, false, &table, &colgroup)); WT_RET(__wt_open_cursor( session, colgroup->source, owner, cfg, cursorp)); + WT_RET(__wt_schema_release_table(session, table)); } else if (WT_PREFIX_MATCH(uri, "config:")) WT_RET(__wt_curconfig_open( session, uri, cfg, cursorp)); @@ -818,6 +820,8 @@ __session_reset(WT_SESSION *wt_session) WT_TRET(__wt_session_reset_cursors(session, true)); + WT_TRET(__wt_schema_sweep_tables(session)); + /* Release common session resources. */ WT_TRET(__wt_session_release_resources(session)); @@ -1209,12 +1213,15 @@ __wt_session_range_truncate(WT_SESSION_IMPL *session, done: err: /* - * Close any locally-opened start cursor. Reset application cursors, - * they've possibly moved and the application cannot use them. + * Close any locally-opened start cursor. + * + * Reset application cursors, they've possibly moved and the + * application cannot use them. Note that we can make it here with a + * NULL start cursor (e.g., if the truncate range is empty). */ if (local_start) WT_TRET(start->close(start)); - else + else if (start != NULL) WT_TRET(start->reset(start)); if (stop != NULL) WT_TRET(stop->reset(stop)); diff --git a/src/third_party/wiredtiger/src/session/session_dhandle.c b/src/third_party/wiredtiger/src/session/session_dhandle.c index ffeb6137766..707e07ac11f 100644 --- a/src/third_party/wiredtiger/src/session/session_dhandle.c +++ b/src/third_party/wiredtiger/src/session/session_dhandle.c @@ -235,6 +235,7 @@ __wt_session_lock_dhandle( lock_busy = true; /* Give other threads a chance to make progress. */ + WT_STAT_CONN_INCR(session, dhandle_lock_blocked); __wt_yield(); } } @@ -597,7 +598,9 @@ __wt_session_lock_checkpoint(WT_SESSION_IMPL *session, const char *checkpoint) * the underlying file are visible to the in-memory pages. */ WT_ERR(__wt_evict_file_exclusive_on(session)); - WT_ERR(__wt_cache_op(session, WT_SYNC_DISCARD)); + ret = __wt_cache_op(session, WT_SYNC_DISCARD); + __wt_evict_file_exclusive_off(session); + WT_ERR(ret); /* * We lock checkpoint handles that we are overwriting, so the handle diff --git a/src/third_party/wiredtiger/src/support/err.c b/src/third_party/wiredtiger/src/support/err.c index 57efde72b23..f98b1943449 100644 --- a/src/third_party/wiredtiger/src/support/err.c +++ b/src/third_party/wiredtiger/src/support/err.c @@ -494,7 +494,18 @@ __wt_panic(WT_SESSION_IMPL *session) WT_GCC_FUNC_ATTRIBUTE((cold)) WT_GCC_FUNC_ATTRIBUTE((visibility("default"))) { - F_SET(S2C(session), WT_CONN_PANIC); + WT_CONNECTION_IMPL *conn; + + conn = S2C(session); + + /* + * If the connection has already be marked for panic, just return the + * error. + */ + if (F_ISSET(conn, WT_CONN_PANIC)) + return (WT_PANIC); + + F_SET(conn, WT_CONN_PANIC); __wt_err(session, WT_PANIC, "the process must exit and restart"); #if defined(HAVE_DIAGNOSTIC) diff --git a/src/third_party/wiredtiger/src/support/rand.c b/src/third_party/wiredtiger/src/support/rand.c index 4fae43edc8e..a5b229b9abc 100644 --- a/src/third_party/wiredtiger/src/support/rand.c +++ b/src/third_party/wiredtiger/src/support/rand.c @@ -120,15 +120,3 @@ __wt_random(WT_RAND_STATE volatile * rnd_state) return ((z << 16) + (w & 65535)); } - -/* - * __wt_random64 -- - * Return a 64-bit pseudo-random number. - */ -uint64_t -__wt_random64(WT_RAND_STATE volatile * rnd_state) - WT_GCC_FUNC_ATTRIBUTE((visibility("default"))) -{ - return (((uint64_t)__wt_random(rnd_state) << 32) + - __wt_random(rnd_state)); -} diff --git a/src/third_party/wiredtiger/src/support/stat.c b/src/third_party/wiredtiger/src/support/stat.c index 8b72e653658..c9e577ac3b6 100644 --- a/src/third_party/wiredtiger/src/support/stat.c +++ b/src/third_party/wiredtiger/src/support/stat.c @@ -842,11 +842,19 @@ static const char * const __stats_connection_desc[] = { "thread-state: active filesystem write calls", "thread-yield: application thread time evicting (usecs)", "thread-yield: application thread time waiting for cache (usecs)", + "thread-yield: connection close blocked waiting for transaction state stabilization", + "thread-yield: connection close yielded for lsm manager shutdown", + "thread-yield: data handle lock yielded", + "thread-yield: log server sync yielded for log write", "thread-yield: page acquire busy blocked", "thread-yield: page acquire eviction blocked", "thread-yield: page acquire locked blocked", "thread-yield: page acquire read blocked", "thread-yield: page acquire time sleeping (usecs)", + "thread-yield: page delete rollback yielded for instantiation", + "thread-yield: page reconciliation yielded due to child modification", + "thread-yield: reference for page index and slot yielded", + "thread-yield: tree descend one level yielded for split page index update", "transaction: number of named snapshots created", "transaction: number of named snapshots dropped", "transaction: transaction begins", @@ -1129,11 +1137,19 @@ __wt_stat_connection_clear_single(WT_CONNECTION_STATS *stats) /* not clearing thread_write_active */ stats->application_evict_time = 0; stats->application_cache_time = 0; + stats->txn_release_blocked = 0; + stats->conn_close_blocked_lsm = 0; + stats->dhandle_lock_blocked = 0; + stats->log_server_sync_blocked = 0; stats->page_busy_blocked = 0; stats->page_forcible_evict_blocked = 0; stats->page_locked_blocked = 0; stats->page_read_blocked = 0; stats->page_sleep = 0; + stats->page_del_rollback_blocked = 0; + stats->child_modify_blocked_page = 0; + stats->page_index_slot_blocked = 0; + stats->tree_descend_blocked = 0; stats->txn_snapshots_created = 0; stats->txn_snapshots_dropped = 0; stats->txn_begin = 0; @@ -1475,12 +1491,25 @@ __wt_stat_connection_aggregate( WT_STAT_READ(from, application_evict_time); to->application_cache_time += WT_STAT_READ(from, application_cache_time); + to->txn_release_blocked += WT_STAT_READ(from, txn_release_blocked); + to->conn_close_blocked_lsm += + WT_STAT_READ(from, conn_close_blocked_lsm); + to->dhandle_lock_blocked += WT_STAT_READ(from, dhandle_lock_blocked); + to->log_server_sync_blocked += + WT_STAT_READ(from, log_server_sync_blocked); to->page_busy_blocked += WT_STAT_READ(from, page_busy_blocked); to->page_forcible_evict_blocked += WT_STAT_READ(from, page_forcible_evict_blocked); to->page_locked_blocked += WT_STAT_READ(from, page_locked_blocked); to->page_read_blocked += WT_STAT_READ(from, page_read_blocked); to->page_sleep += WT_STAT_READ(from, page_sleep); + to->page_del_rollback_blocked += + WT_STAT_READ(from, page_del_rollback_blocked); + to->child_modify_blocked_page += + WT_STAT_READ(from, child_modify_blocked_page); + to->page_index_slot_blocked += + WT_STAT_READ(from, page_index_slot_blocked); + to->tree_descend_blocked += WT_STAT_READ(from, tree_descend_blocked); to->txn_snapshots_created += WT_STAT_READ(from, txn_snapshots_created); to->txn_snapshots_dropped += diff --git a/src/third_party/wiredtiger/src/support/time.c b/src/third_party/wiredtiger/src/support/time.c new file mode 100644 index 00000000000..0e4562c0234 --- /dev/null +++ b/src/third_party/wiredtiger/src/support/time.c @@ -0,0 +1,89 @@ +/*- + * Public Domain 2014-2017 MongoDB, Inc. + * Public Domain 2008-2014 WiredTiger, Inc. + * + * This is free and unencumbered software released into the public domain. + * + * Anyone is free to copy, modify, publish, use, compile, sell, or + * distribute this software, either in source code form or as a compiled + * binary, for any purpose, commercial or non-commercial, and by any + * means. + * + * In jurisdictions that recognize copyright laws, the author or authors + * of this software dedicate any and all copyright interest in the + * software to the public domain. We make this dedication for the benefit + * of the public at large and to the detriment of our heirs and + * successors. We intend this dedication to be an overt act of + * relinquishment in perpetuity of all present and future rights to this + * software under copyright law. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "wt_internal.h" + +/* + * __time_check_monotonic -- + * Check and prevent time running backward. If we detect that it has, we + * set the time structure to the previous values, making time stand still + * until we see a time in the future of the highest value seen so far. + */ +static void +__time_check_monotonic(WT_SESSION_IMPL *session, struct timespec *tsp) +{ + /* + * Detect time going backward. If so, use the last + * saved timestamp. + */ + if (session == NULL) + return; + + if (tsp->tv_sec < session->last_epoch.tv_sec || + (tsp->tv_sec == session->last_epoch.tv_sec && + tsp->tv_nsec < session->last_epoch.tv_nsec)) { + WT_STAT_CONN_INCR(session, time_travel); + *tsp = session->last_epoch; + } else + session->last_epoch = *tsp; +} + +/* + * __wt_epoch -- + * Return the time since the Epoch, adjusted so it never appears to go + * backwards. + */ +void +__wt_epoch(WT_SESSION_IMPL *session, struct timespec *tsp) + WT_GCC_FUNC_ATTRIBUTE((visibility("default"))) +{ + struct timespec tmp; + + /* + * Read into a local variable so that we're comparing the correct + * value when we check for monotonic increasing time. There are + * many places we read into an unlocked global variable. + */ + __wt_epoch_raw(session, &tmp); + __time_check_monotonic(session, &tmp); + *tsp = tmp; +} + +/* + * __wt_seconds -- + * Return the seconds since the Epoch. + */ +void +__wt_seconds(WT_SESSION_IMPL *session, time_t *timep) +{ + struct timespec t; + + __wt_epoch(session, &t); + + *timep = t.tv_sec; +} diff --git a/src/third_party/wiredtiger/src/txn/txn.c b/src/third_party/wiredtiger/src/txn/txn.c index ea7faa2e966..7665ba56adc 100644 --- a/src/third_party/wiredtiger/src/txn/txn.c +++ b/src/third_party/wiredtiger/src/txn/txn.c @@ -503,13 +503,17 @@ __wt_txn_commit(WT_SESSION_IMPL *session, const char *cfg[]) WT_CONNECTION_IMPL *conn; WT_DECL_RET; WT_TXN *txn; + WT_TXN_GLOBAL *txn_global; WT_TXN_OP *op; u_int i; - bool did_update; + bool did_update, locked; txn = &session->txn; conn = S2C(session); + txn_global = &conn->txn_global; did_update = txn->mod_count != 0; + locked = false; + WT_ASSERT(session, !F_ISSET(txn, WT_TXN_ERROR) || !did_update); if (!F_ISSET(txn, WT_TXN_RUNNING)) @@ -580,6 +584,14 @@ __wt_txn_commit(WT_SESSION_IMPL *session, const char *cfg[]) * This is particularly important for checkpoints. */ __wt_txn_release_snapshot(session); + /* + * We hold the visibility lock for reading from the time + * we write our log record until the time we release our + * transaction so that the LSN any checkpoint gets will + * always reflect visible data. + */ + __wt_readlock(session, &txn_global->visibility_rwlock); + locked = true; ret = __wt_txn_log_commit(session, cfg); } @@ -590,6 +602,9 @@ __wt_txn_commit(WT_SESSION_IMPL *session, const char *cfg[]) * Nothing can fail after this point. */ if (ret != 0) { + if (locked) + __wt_readunlock(session, + &txn_global->visibility_rwlock); WT_TRET(__wt_txn_rollback(session, cfg)); return (ret); } @@ -600,6 +615,8 @@ __wt_txn_commit(WT_SESSION_IMPL *session, const char *cfg[]) txn->mod_count = 0; __wt_txn_release(session); + if (locked) + __wt_readunlock(session, &txn_global->visibility_rwlock); return (0); } @@ -770,6 +787,7 @@ __wt_txn_global_init(WT_SESSION_IMPL *session, const char *cfg[]) &txn_global->id_lock, "transaction id lock")); WT_RET(__wt_rwlock_init(session, &txn_global->scan_rwlock)); WT_RET(__wt_rwlock_init(session, &txn_global->nsnap_rwlock)); + WT_RET(__wt_rwlock_init(session, &txn_global->visibility_rwlock)); txn_global->nsnap_oldest_id = WT_TXN_NONE; TAILQ_INIT(&txn_global->nsnaph); @@ -801,6 +819,7 @@ __wt_txn_global_destroy(WT_SESSION_IMPL *session) __wt_spin_destroy(session, &txn_global->id_lock); __wt_rwlock_destroy(session, &txn_global->scan_rwlock); __wt_rwlock_destroy(session, &txn_global->nsnap_rwlock); + __wt_rwlock_destroy(session, &txn_global->visibility_rwlock); __wt_free(session, txn_global->states); } diff --git a/src/third_party/wiredtiger/src/txn/txn_log.c b/src/third_party/wiredtiger/src/txn/txn_log.c index 2931dc1ce82..09a8c4d9663 100644 --- a/src/third_party/wiredtiger/src/txn/txn_log.c +++ b/src/third_party/wiredtiger/src/txn/txn_log.c @@ -289,16 +289,20 @@ int __wt_txn_checkpoint_log( WT_SESSION_IMPL *session, bool full, uint32_t flags, WT_LSN *lsnp) { + WT_CONNECTION_IMPL *conn; WT_DECL_ITEM(logrec); WT_DECL_RET; WT_ITEM *ckpt_snapshot, empty; WT_LSN *ckpt_lsn; WT_TXN *txn; + WT_TXN_GLOBAL *txn_global; uint8_t *end, *p; size_t recsize; uint32_t i, rectype = WT_LOGREC_CHECKPOINT; const char *fmt = WT_UNCHECKED_STRING(IIIIu); + conn = S2C(session); + txn_global = &conn->txn_global; txn = &session->txn; ckpt_lsn = &txn->ckpt_lsn; @@ -320,6 +324,15 @@ __wt_txn_checkpoint_log( txn->full_ckpt = true; WT_ERR(__wt_log_flush_lsn(session, ckpt_lsn, true)); /* + * We take and immediately release the visibility lock. + * Acquiring the write lock guarantees that any transaction + * that has written to the log has also made its transaction + * visible at this time. + */ + __wt_writelock(session, &txn_global->visibility_rwlock); + __wt_writeunlock(session, &txn_global->visibility_rwlock); + + /* * We need to make sure that the log records in the checkpoint * LSN are on disk. In particular to make sure that the * current log file exists. @@ -363,20 +376,20 @@ __wt_txn_checkpoint_log( txn->ckpt_nsnapshot, ckpt_snapshot)); logrec->size += (uint32_t)recsize; WT_ERR(__wt_log_write(session, logrec, lsnp, - F_ISSET(S2C(session), WT_CONN_CKPT_SYNC) ? + F_ISSET(conn, WT_CONN_CKPT_SYNC) ? WT_LOG_FSYNC : 0)); /* * If this full checkpoint completed successfully and there is - * no hot backup in progress and this is not recovery, tell - * the logging subsystem the checkpoint LSN so that it can - * archive. Do not update the logging checkpoint LSN if this - * is during a clean connection close, only during a full - * checkpoint. A clean close may not update any metadata LSN - * and we do not want to archive in that case. + * no hot backup in progress and this is not an unclean + * recovery, tell the logging subsystem the checkpoint LSN so + * that it can archive. Do not update the logging checkpoint + * LSN if this is during a clean connection close, only during + * a full checkpoint. A clean close may not update any + * metadata LSN and we do not want to archive in that case. */ - if (!S2C(session)->hot_backup && - !F_ISSET(S2C(session), WT_CONN_RECOVERING) && + if (!conn->hot_backup && + !FLD_ISSET(conn->log_flags, WT_CONN_LOG_RECOVER_DIRTY) && txn->full_ckpt) __wt_log_ckpt(session, ckpt_lsn); diff --git a/src/third_party/wiredtiger/src/txn/txn_recover.c b/src/third_party/wiredtiger/src/txn/txn_recover.c index 30932195b1e..29f4dee5199 100644 --- a/src/third_party/wiredtiger/src/txn/txn_recover.c +++ b/src/third_party/wiredtiger/src/txn/txn_recover.c @@ -20,6 +20,7 @@ typedef struct { } *files; size_t file_alloc; /* Allocated size of files array. */ u_int max_fileid; /* Maximum file ID seen. */ + WT_LSN max_lsn; /* Maximum checkpoint LSN seen. */ u_int nfiles; /* Number of files in the metadata. */ WT_LSN ckpt_lsn; /* Start LSN for main recovery loop. */ @@ -342,6 +343,10 @@ __recovery_setup_file(WT_RECOVERY *r, const char *uri, const char *config) "Recovering %s with id %" PRIu32 " @ (%" PRIu32 ", %" PRIu32 ")", uri, fileid, lsn.l.file, lsn.l.offset); + if ((!WT_IS_MAX_LSN(&lsn) && !WT_IS_INIT_LSN(&lsn)) && + (WT_IS_MAX_LSN(&r->max_lsn) || __wt_log_cmp(&lsn, &r->max_lsn) > 0)) + r->max_lsn = lsn; + return (0); } @@ -428,6 +433,7 @@ __wt_txn_recover(WT_SESSION_IMPL *session) WT_RET(__wt_open_internal_session(conn, "txn-recover", false, WT_SESSION_NO_LOGGING, &session)); r.session = session; + WT_MAX_LSN(&r.max_lsn); F_SET(conn, WT_CONN_RECOVERING); WT_ERR(__wt_metadata_search(session, WT_METAFILE_URI, &config)); @@ -443,9 +449,29 @@ __wt_txn_recover(WT_SESSION_IMPL *session) */ if (!FLD_ISSET(S2C(session)->log_flags, WT_CONN_LOG_EXISTED) || WT_IS_MAX_LSN(&metafile->ckpt_lsn)) { + /* + * Detect if we're going from logging disabled to enabled. + * We need to know this to verify LSNs and start at the correct + * log file later. If someone ran with logging, then disabled + * it and removed all the log files and then turned logging back + * on, we have to start logging in the log file number that is + * larger than any checkpoint LSN we have from the earlier time. + */ WT_ERR(__recovery_file_scan(&r)); + /* + * The array can be re-allocated in recovery_file_scan. Reset + * our pointer after scanning all the files. + */ + metafile = &r.files[WT_METAFILE_ID]; conn->next_file_id = r.max_fileid; - goto done; + + if (FLD_ISSET(conn->log_flags, WT_CONN_LOG_ENABLED) && + WT_IS_MAX_LSN(&metafile->ckpt_lsn) && + !WT_IS_MAX_LSN(&r.max_lsn)) { + WT_ERR(__wt_log_reset(session, r.max_lsn.l.file)); + goto ckpt; + } else + goto done; } /* @@ -488,6 +514,11 @@ __wt_txn_recover(WT_SESSION_IMPL *session) /* Scan the metadata to find the live files and their IDs. */ WT_ERR(__recovery_file_scan(&r)); + /* + * Clear this out. We no longer need it and it could have been + * re-allocated when scanning the files. + */ + metafile = NULL; /* * We no longer need the metadata cursor: close it to avoid pinning any @@ -535,6 +566,8 @@ __wt_txn_recover(WT_SESSION_IMPL *session) * this is not a read-only connection. * We can consider skipping it in the future. */ + if (needs_rec) + FLD_SET(conn->log_flags, WT_CONN_LOG_RECOVER_DIRTY); if (WT_IS_INIT_LSN(&r.ckpt_lsn)) WT_ERR(__wt_log_scan(session, NULL, WT_LOGSCAN_FIRST | WT_LOGSCAN_RECOVER, @@ -554,11 +587,12 @@ __wt_txn_recover(WT_SESSION_IMPL *session) * open is fast and keep the metadata up to date with the checkpoint * LSN and archiving. */ - WT_ERR(session->iface.checkpoint(&session->iface, "force=1")); +ckpt: WT_ERR(session->iface.checkpoint(&session->iface, "force=1")); done: FLD_SET(conn->log_flags, WT_CONN_LOG_RECOVER_DONE); err: WT_TRET(__recovery_free(&r)); __wt_free(session, config); + FLD_CLR(conn->log_flags, WT_CONN_LOG_RECOVER_DIRTY); if (ret != 0) __wt_err(session, ret, "Recovery failed"); diff --git a/src/third_party/wiredtiger/test/mciproject.yml b/src/third_party/wiredtiger/test/mciproject.yml index 6456475aa00..50a910d9e58 100644 --- a/src/third_party/wiredtiger/test/mciproject.yml +++ b/src/third_party/wiredtiger/test/mciproject.yml @@ -157,20 +157,6 @@ buildvariants: - name: unit-test - name: fops -- name: solaris - display_name: Solaris - run_on: - - solaris - expansions: - make_command: PATH=/opt/mongodbtoolchain/bin:$PATH gmake - test_env_vars: LD_LIBRARY_PATH=`pwd`/.libs - smp_command: -j $(kstat cpu | sort -u | grep -c "^module") - configure_env_vars: PATH=/opt/mongodbtoolchain/bin:$PATH CFLAGS="-m64" - tasks: - - name: compile - - name: unit-test - - name: fops - - name: windows-64 display_name: Windows 64-bit run_on: diff --git a/src/third_party/wiredtiger/test/recovery/random-abort.c b/src/third_party/wiredtiger/test/recovery/random-abort.c index febe6530534..b53383e5730 100644 --- a/src/third_party/wiredtiger/test/recovery/random-abort.c +++ b/src/third_party/wiredtiger/test/recovery/random-abort.c @@ -47,9 +47,9 @@ static bool inmem; #define RECORDS_FILE "records-%" PRIu32 #define ENV_CONFIG_DEF \ - "create,log=(file_max=10M,archive=false,enabled)" + "create,log=(file_max=10M,enabled)" #define ENV_CONFIG_TXNSYNC \ - "create,log=(file_max=10M,archive=false,enabled)," \ + "create,log=(file_max=10M,enabled)," \ "transaction_sync=(enabled,method=none)" #define ENV_CONFIG_REC "log=(recover=on)" #define MAX_VAL 4096 diff --git a/src/third_party/wiredtiger/test/suite/test_bug018.py b/src/third_party/wiredtiger/test/suite/test_bug018.py new file mode 100644 index 00000000000..7d20ebcaacb --- /dev/null +++ b/src/third_party/wiredtiger/test/suite/test_bug018.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# +# Public Domain 2014-2017 MongoDB, Inc. +# Public Domain 2008-2014 WiredTiger, Inc. +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +from helper import copy_wiredtiger_home +import os +import wiredtiger, wttest + +# test_bug018.py +# JIRA WT-3590: if writing table data fails during close then tables +# that were updated within the same transaction could get out of sync with +# each other. +class test_bug018(wttest.WiredTigerTestCase): + '''Test closing/reopening/recovering tables when writes fail''' + + conn_config = 'log=(enabled)' + + def setUp(self): + # This test uses Linux-specific code so skip on any other system. + if os.name != 'posix' or os.uname()[0] != 'Linux': + self.skipTest('Linux-specific test skipped on ' + os.name) + super(test_bug018, self).setUp() + + def create_table(self, uri): + self.session.create(uri, 'key_format=S,value_format=S') + return self.session.open_cursor(uri) + + def test_bug018(self): + '''Test closing multiple tables''' + basename = 'bug018.' + baseuri = 'file:' + basename + c1 = self.create_table(baseuri + '01.wt') + c2 = self.create_table(baseuri + '02.wt') + + self.session.begin_transaction() + c1['key'] = 'value' + c2['key'] = 'value' + self.session.commit_transaction() + + # Simulate a write failure by closing the file descriptor for the second + # table out from underneath WiredTiger. We do this right before + # closing the connection so that the write error happens during close + # when writing out the final data. Allow table 1 to succeed and force + # an erorr writing out table 2. + # + # This is Linux-specific code to figure out the file descriptor. + for f in os.listdir('/proc/self/fd'): + try: + if os.readlink('/proc/self/fd/' + f).endswith(basename + '02.wt'): + os.close(int(f)) + except OSError: + pass + + # Expect an error and messages, so turn off stderr checking. + with self.expectedStderrPattern(''): + try: + self.close_conn() + except wiredtiger.WiredTigerError: + self.conn = None + + # Make a backup for forensics in case something goes wrong. + backup_dir = 'BACKUP' + copy_wiredtiger_home('.', backup_dir, True) + + # After reopening and running recovery both tables should be in + # sync even though table 1 was successfully written and table 2 + # had an error on close. + self.open_conn() + c1 = self.session.open_cursor(baseuri + '01.wt') + c2 = self.session.open_cursor(baseuri + '02.wt') + self.assertEqual(list(c1), list(c2)) + +if __name__ == '__main__': + wttest.run() diff --git a/src/third_party/wiredtiger/test/suite/test_inmem01.py b/src/third_party/wiredtiger/test/suite/test_inmem01.py index 388485db29b..d280642942a 100644 --- a/src/third_party/wiredtiger/test/suite/test_inmem01.py +++ b/src/third_party/wiredtiger/test/suite/test_inmem01.py @@ -108,12 +108,15 @@ class test_inmem01(wttest.WiredTigerTestCase): cursor.reset() # Spin inserting to give eviction a chance to reclaim space + sleeps = 0 inserted = False for i in range(1, 1000): try: cursor[ds.key(1)] = ds.value(1) except wiredtiger.WiredTigerError: cursor.reset() + sleeps = sleeps + 1 + self.assertLess(sleeps, 60 * 5) sleep(1) continue inserted = True diff --git a/src/third_party/wiredtiger/test/suite/test_las.py b/src/third_party/wiredtiger/test/suite/test_las.py new file mode 100644 index 00000000000..d0bd1d108fa --- /dev/null +++ b/src/third_party/wiredtiger/test/suite/test_las.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# +# Public Domain 2014-2017 MongoDB, Inc. +# Public Domain 2008-2014 WiredTiger, Inc. +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +import wiredtiger, wttest +from wtdataset import SimpleDataSet + +# test_las.py +# Smoke tests to ensure lookaside tables are working. +class test_las(wttest.WiredTigerTestCase): + # Force a small cache. + def conn_config(self): + return 'cache_size=1GB' + + @wttest.longtest('lookaside table smoke test') + def test_las(self): + # Create a small table. + uri = "table:test_las" + nrows = 100 + ds = SimpleDataSet(self, uri, nrows, key_format="S") + ds.populate() + + # Take a snapshot. + self.session.snapshot("name=xxx") + + # Insert a large number of records, we'll hang if the lookaside table + # isn't doing its thing. + c = self.session.open_cursor(uri) + bigvalue = "abcde" * 100 + for i in range(1, 1000000): + c.set_key(ds.key(nrows + i)) + c.set_value(bigvalue) + self.assertEquals(c.insert(), 0) + +if __name__ == '__main__': + wttest.run() diff --git a/src/third_party/wiredtiger/test/suite/test_txn02.py b/src/third_party/wiredtiger/test/suite/test_txn02.py index 01626057b9e..76a325743e9 100644 --- a/src/third_party/wiredtiger/test/suite/test_txn02.py +++ b/src/third_party/wiredtiger/test/suite/test_txn02.py @@ -169,7 +169,6 @@ class test_txn02(wttest.WiredTigerTestCase, suite_subprocess): try: session = backup_conn.open_session() finally: - session.checkpoint("force") self.check(backup_conn.open_session(), None, committed) # Sleep long enough so that the archive thread is guaranteed # to run before we close the connection. diff --git a/src/third_party/wiredtiger/test/suite/test_txn05.py b/src/third_party/wiredtiger/test/suite/test_txn05.py index 7aaff221ba4..7099bc972aa 100644 --- a/src/third_party/wiredtiger/test/suite/test_txn05.py +++ b/src/third_party/wiredtiger/test/suite/test_txn05.py @@ -134,12 +134,12 @@ class test_txn05(wttest.WiredTigerTestCase, suite_subprocess): session = backup_conn.open_session() finally: self.check(session, None, committed) - # Force a checkpoint because we don't record the recovery - # checkpoint as available for archiving. - session.checkpoint("force") # Sleep long enough so that the archive thread is guaranteed # to run before we close the connection. time.sleep(1.0) + if count == 0: + first_logs = \ + fnmatch.filter(os.listdir(self.backup_dir), "*Log*") backup_conn.close() count += 1 # @@ -149,6 +149,11 @@ class test_txn05(wttest.WiredTigerTestCase, suite_subprocess): # cur_logs = fnmatch.filter(os.listdir(self.backup_dir), "*Log*") for o in orig_logs: + # Creating the backup was effectively an unclean shutdown so + # even after sleeping, we should never archive log files + # because a checkpoint has not run. Later opens and runs of + # recovery will detect a clean shutdown and allow archiving. + self.assertEqual(True, o in first_logs) if self.archive == 'true': self.assertEqual(False, o in cur_logs) else: diff --git a/src/third_party/wiredtiger/test/suite/test_txn09.py b/src/third_party/wiredtiger/test/suite/test_txn09.py index 768d714e248..b8a3d7f38ae 100644 --- a/src/third_party/wiredtiger/test/suite/test_txn09.py +++ b/src/third_party/wiredtiger/test/suite/test_txn09.py @@ -26,8 +26,8 @@ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # -# test_txn02.py -# Transactions: commits and rollbacks +# test_txn09.py +# Transactions: recovery toggling logging # import fnmatch, os, shutil, time diff --git a/src/third_party/wiredtiger/test/suite/test_txn16.py b/src/third_party/wiredtiger/test/suite/test_txn16.py new file mode 100644 index 00000000000..929da2291c7 --- /dev/null +++ b/src/third_party/wiredtiger/test/suite/test_txn16.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +# +# Public Domain 2014-2017 MongoDB, Inc. +# Public Domain 2008-2014 WiredTiger, Inc. +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# test_txn16.py +# Recovery: Test that toggling between logging and not logging does not +# continue to generate more log files. +# + +import fnmatch, os, shutil, time +from suite_subprocess import suite_subprocess +import wttest + +class test_txn16(wttest.WiredTigerTestCase, suite_subprocess): + t1 = 'table:test_txn16_1' + t2 = 'table:test_txn16_2' + t3 = 'table:test_txn16_3' + nentries = 1000 + create_params = 'key_format=i,value_format=i' + # Set the log file size small so we generate checkpoints + # with LSNs in different files. + conn_config = 'config_base=false,' + \ + 'log=(archive=false,enabled,file_max=100K),' + \ + 'transaction_sync=(method=dsync,enabled)' + conn_on = 'config_base=false,' + \ + 'log=(archive=false,enabled,file_max=100K),' + \ + 'transaction_sync=(method=dsync,enabled)' + conn_off = 'config_base=false,log=(enabled=false)' + + def populate_table(self, uri): + self.session.create(uri, self.create_params) + c = self.session.open_cursor(uri, None, None) + # Populate with an occasional checkpoint to generate + # some varying LSNs. + for i in range(self.nentries): + c[i] = i + 1 + if i % 900 == 0: + self.session.checkpoint() + c.close() + + def copy_dir(self, olddir, newdir): + ''' Simulate a crash from olddir and restart in newdir. ''' + # with the connection still open, copy files to new directory + shutil.rmtree(newdir, ignore_errors=True) + os.mkdir(newdir) + for fname in os.listdir(olddir): + fullname = os.path.join(olddir, fname) + # Skip lock file on Windows since it is locked + if os.path.isfile(fullname) and \ + "WiredTiger.lock" not in fullname and \ + "Tmplog" not in fullname and \ + "Preplog" not in fullname: + shutil.copy(fullname, newdir) + # close the original connection. + self.close_conn() + + def run_toggle(self, homedir): + loop = 0 + # Record original log files. There should never be overlap + # with these even after they're removed. + orig_logs = fnmatch.filter(os.listdir(homedir), "*Log*") + while loop < 3: + # Reopen with logging on to run recovery first time + on_conn = self.wiredtiger_open(homedir, self.conn_on) + on_conn.close() + if loop > 0: + # Get current log files. + cur_logs = fnmatch.filter(os.listdir(homedir), "*Log*") + scur = set(cur_logs) + sorig = set(orig_logs) + # There should never be overlap with the log files that + # were there originally. Mostly this checks that after + # opening with logging disabled and then re-enabled, we + # don't see log file 1. + self.assertEqual(scur.isdisjoint(sorig), True) + if loop > 1: + # We should be creating the same log files each time. + for l in cur_logs: + self.assertEqual(l in last_logs, True) + for l in last_logs: + self.assertEqual(l in cur_logs, True) + last_logs = cur_logs + loop += 1 + # Remove all log files before opening without logging. + cur_logs = fnmatch.filter(os.listdir(homedir), "*Log*") + for l in cur_logs: + path=homedir + "/" + l + os.remove(path) + off_conn = self.wiredtiger_open(homedir, self.conn_off) + off_conn.close() + + def test_recovery(self): + ''' Check log file creation when toggling. ''' + + # Here's the strategy: + # - With logging populate 4 tables. Checkpoint + # them at different times. + # - Copy to a new directory to simulate a crash. + # - Close the original connection. + # On both a "copy" to simulate a crash and the original (3x): + # - Record log files existing. + # - Reopen with logging to run recovery. Close connection. + # - Record log files existing. + # - Remove all log files. + # - Open connection with logging disabled. + # - Record log files existing. Verify we don't keep adding. + # + self.populate_table(self.t1) + self.populate_table(self.t2) + self.populate_table(self.t3) + self.copy_dir(".", "RESTART") + self.run_toggle(".") + self.run_toggle("RESTART") + +if __name__ == '__main__': + wttest.run() diff --git a/version.json b/version.json index aeb6f194474..16ecc44565a 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "githash": "cf38c1b8a0a8dca4a11737581beafef4fe120bcd", - "version": "3.4.7" + "githash": "fd954412dfc10e4d1e3e2dd4fac040f8b476b268", + "version": "3.4.14" }
\ No newline at end of file |
